Search code examples
pythonscopepython-class

scope strangeness in python class attributes


Came across what seems to be a weird case of 'spooky action at a distance' using python class attributes.

If I define X as:

class X():
    a = list()
    b = int
    def __init__(self, value):
        self.a.append(value)
        self.b = value

Then instantiate:

u = X(0)
v = X(1)

It outputs the following strangeness:

u.a == [0,1]
u.b == 0  
v.a == [0,1]
v.b == 1

As if the list() from "a" is acting as a shared class attribute while the int from "b" only as an instance attribute. I mean, why would types affect the scope of an attribute ? what am i missing here?


Solution

  • The difference isn't the type, it's the way you're rebinding the attributes (or not).

        def __init__(self, value):
            self.a.append(value)
            self.b = value
    

    In this code, self.a is not being rebound; the object it references is being mutated. Hence self.a remains a reference to the (shared) class attribute, X.a.

    self.b is being rebound, and so a new instance attribute is created that shadows the class attribute X.b. Note that X.b is the int type, not an actual int!

    >>> X.b
    <class 'int'>
    >>> u.b
    0
    >>> v.b
    1
    

    The id function can be used to verify that there is only a single X.a object:

    >>> id(X.a)
    2052257864960
    >>> id(u.a)
    2052257864960
    >>> id(v.a)
    2052257864960
    

    whereas obviously the ids for the b attributes are all different:

    >>> id(X.b)
    140711576840944
    >>> id(u.b)
    2052256170192
    >>> id(v.b)
    2052256170224
    

    If you wanted a class that mutates its class attributes every time a new instance is created (note: this is kind of a weird thing to do), you can do that by making sure you're modifying the type of self rather than the self instance:

    class X():
        # Initializing a and b with literal list and int values.
        # This is more idiomatic than doing stuff like list() and int().
        a = []
        b = 0
        def __init__(self, value):
            # Using type(self) as a way to reference X (or a subclass) explicitly.
            # As discussed, this isn't necessary when we're doing self.a.append,
            # but using type() consistently makes it obvious that we're
            # dealing with class attributes, not instance attributes.
            type(self).a.append(value)
            type(self).b = value
    

    Trying it out:

    >>> X(0)
    <__main__.X object at 0x000001DDD416AE90>
    >>> X(1)
    <__main__.X object at 0x000001DDD416B970>
    >>> X.b
    1
    >>> X.a
    [0, 1]