First of all lets understand how list.count
works. From the cpython source code the list.count
has the following definition.
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);
}
So when you perform some_list.count(some_element)
, Python will iterate over every object in the list, and perform a rich comparison(ie, PyObject_RichCompareBool
).
From the C-API documentation the rich comparison(ie, PyObject_RichCompareBool(PyObject *o1, PyObject *o2, int opid)
) will
Compare the values of o1
and o2
using the operation specified by opid
, which must be one of Py_LT
, Py_LE
, Py_EQ
, Py_NE
, Py_GT
, or Py_GE
, corresponding to <
, <=
, ==
, !=
, >
, or >=
respectively. Returns -1
on error, 0
if the result is false, 1
otherwise.
So if the value is 1
(ie, true
) a counter will be incremented. After the iteration the counter will be return back to the caller.
list_count
in CPython roughly equivalent to the following in python layer,
def list_count(list_, item_to_count):
counter = 0
for iterm in list_:
if item == item_to_count:
counter += 1
return counter
Now lets get back to your question.
While it works pretty well in a test code, it doesnt anymore when the
counted class inherits the flask_login UserMixin class.
Lets take a sample class(Without inheriting from 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))
This will print 1
as we expected. But how does python perform the comparison here???. By default python will compare the id
(ie, memory address of the object) of p1
with ids of p1
, p2
, p3
. Since each new object have different ids , count method will return 1
.
Ok, So what if we want to count the person as one if there names are equal???
Let take the same example.
p1 = Person("Person1")
p2 = Person("Person1")
print([p1, p2].count(p1)) # I want this to be return 2
But This still return 1
as python still comparing with its object ids. So how can I customize this?. You can override __eq__
of the object. ie,
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))
Wow now it return 2
as expected.
Now lets consider the class which inherit from 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))
This will print 2
. Why?. If the comparison was performed based on ids
it would have been 1
. So there will be an __eq__
implemented somewhere. So if you look at the UserMixin
class implementation it implement __eq__
method.
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`')
As you can see the comparison is performed based on its id
attribute. In this case Element
class set the id
attribute on the class level hence it will be same for all instances.
How to fix this,
From the logical perspective every object will have unique ids. Hence id
should be a instance level attribute. See one example from the flask-login
code base itself.
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