I'm using the list.count() method to check if a relationship has an element.
While it works pretty well in a test code, it doesnt anymore when the counted class inherits the flask_login UserMixin class.
Why , and how to fix it ?
class Element(UserMixin):
id=1
name="default"
def __init__(self, name):
name=name
elementsList=[]
elt1=Element(name="1")
elt2=Element(name="2")
elt3=Element(name="3")
elementsList.append(elt1)
elementsList.append(elt2)
print("Counting Element2 should return 1: ", elementsList.count(elt2)) # returns 2
print("Counting Element3 should return 0: ", elementsList.count(elt3)) # returns 2
I should get the number of elements in the list (1 or 0 ).
Instead I get the whole list length (2, even if I append more integers).
It is as if it was counting class occurrences in the list, not the object.
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
This 'id' issue is the key point.
Back to the sqlalchemy context, the list holds objects with an id as primarykey ... set to 'None' at first for all objects.
And it will be updated only after a session.add() and session.commit(), the final fix.
Thanks.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.