简体   繁体   中英

UserMixin inheritance disturbs python list.count() method

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM