Прежде всего давайте разберемся, как работает list.count
. Из исходного кода cpython list.count
имеет следующее определение.
static PyObject *
list_count(PyListObject *self, PyObject *value)
{
Py_ssize_t count = 0;
Py_ssize_t i;
for (i = 0; i < Py_SIZE(self); i++) {
int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ);
if (cmp > 0)
count++;
else if (cmp < 0)
return NULL;
}
return PyLong_FromSsize_t(count);
}
Таким образом, когда вы выполняете some_list.count(some_element)
, Python будет перебирать каждый объект в списке и выполнять расширенное сравнение (то есть PyObject_RichCompareBool
).
Из документации C-API расширенное сравнение (т. Е. PyObject_RichCompareBool(PyObject *o1, PyObject *o2, int opid)
) будет
Сравните значения o1
и o2
, используя операцию, указанную в opid
, которая должна быть одной из Py_LT
, Py_LE
, Py_EQ
, Py_NE
, Py_GT
или Py_GE
, соответствует <
, <=
, ==
, !=
, >
или >=
соответственно. Возвращает -1
при ошибке, 0
, если результат ложен, 1
в противном случае.
Таким образом, если значение равно 1
(то есть true
), счетчик будет увеличиваться. После итерации счетчик будет возвращен вызывающей стороне.
list_count
в CPython примерно эквивалентно следующему в слое python,
def list_count(list_, item_to_count):
counter = 0
for iterm in list_:
if item == item_to_count:
counter += 1
return counter
Теперь вернемся к вашему вопросу.
Хотя в тестовом коде он работает довольно хорошо, он больше не работает, когда
подсчитанный класс наследует класс UserMixin flask_login.
Давайте возьмем образец класса (без наследования от UserMixin
)
class Person
def __init__(self, name):
self.name = name
p1 = Person("Person1")
p2 = Person("Person2")
p3 = Person("Person3")
print([p1, p2, p3].count(p1))
Это напечатает 1
, как мы и ожидали. Но как Python выполняет сравнение здесь ??? По умолчанию python сравнивает id
(то есть адрес памяти объекта) p1
с идентификаторами p1
, p2
, p3
. Поскольку каждый новый объект имеет разные идентификаторы, метод count вернет 1
.
Хорошо, а что если мы хотим считать человека одним, если там имена равны ???
Давайте возьмем тот же пример.
p1 = Person("Person1")
p2 = Person("Person1")
print([p1, p2].count(p1)) # I want this to be return 2
Но это все еще возвращает 1
как python, по-прежнему сравниваемый с его идентификаторами объектов. Так как я могу настроить это? Вы можете переопределить __eq__
объекта. т.е.
class Person(object):
def __init__(self, name):
self.name = name
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.name == other.name
return NotImplemented
p1 = Person("Person1")
p2 = Person("Person1")
print([p1, p2].count(p1))
Ух ты, теперь он возвращает 2
, как и ожидалось.
Теперь давайте рассмотрим класс, который наследуется от UserMixin
.
class Element(UserMixin):
id=1
def __init__(self, name):
self.name=name
elementsList=[]
elt1=Element(name="1")
elt2=Element(name="2")
elt3=Element(name="3")
elementsList.append(elt1)
elementsList.append(elt2)
print(elementsList.count(elt2))
Будет напечатано 2
. Зачем?. Если бы сравнение было выполнено на основе ids
, это было бы 1
. Так что где-то будет реализовано __eq__
. Поэтому, если вы посмотрите на реализацию класса UserMixin
, он реализует метод __eq__
.
def __eq__(self, other):
'''
Checks the equality of two `UserMixin` objects using `get_id`.
'''
if isinstance(other, UserMixin):
return self.get_id() == other.get_id()
return NotImplemented
def get_id(self):
try:
return text_type(self.id)
except AttributeError:
raise NotImplementedError('No `id` attribute - override `get_id`')
Как видите, сравнение выполняется по атрибуту id
. В этом случае Element
class устанавливает атрибут id
на уровне класса, следовательно, он будет одинаковым для всех экземпляров.
Как это исправить,
С логической точки зрения каждый объект будет иметь уникальные идентификаторы. Следовательно, id
должен быть атрибутом уровня экземпляра. См. Один пример из самой кодовой базы flask-login
.
class User(UserMixin):
def __init__(self, name, id, active=True):
self.id = id
self.name = name
self.active = active
def get_id(self):
return self.id
@property
def is_active(self):
return self.active