Bintz
Bintz

Reputation: 801

python list scope in class

I am coming up against some unexpected behavior with this code:

from pprint import pprint

class a (object):
    x = ['1']

class b (a):
    x = a.x
    x.append('2')

class c (a):
    x = a.x
    x.append('3')

class d (a):
    x = a.x
    x = 'nothing'

if __name__ == '__main__':
    pprint(a.x)
    pprint(b.x)
    pprint(c.x)
    pprint(d.x)

I receive the output:

['1', '2', '3']
['1', '2', '3']
['1', '2', '3']
'nothing'

But I would expect to receive:

['1']
['1', '2']
['1', '3']
'nothing'

What I don't understand is:

  1. Why does appending to a list in class b also append to the list in class a?
  2. Why does appending to that list in class c append to both b and a?
  3. Why doesn't re-assigning that variable to a string in class d not have an effect on the other 3 classes?

Upvotes: 1

Views: 95

Answers (4)

paxdiablo
paxdiablo

Reputation: 881553

This is probably one of the hardest things for new Python developers to understand and it becomes a real epiphany when they finally do. What you have there is no different to:

>>> x = [1]; print(x)
[1]

>>> y = x; print(x); print(y)
[1]
[1]

>>> y.append(2); print(x); print(y)
[1, 2]
[1, 2]

>>> z = x; z = 'nothing'; print(x); print(y); print(z)
[1, 2]
[1, 2]
nothing

The reason is does this is because, in Python, everything is an object and variables are simply bindings to objects.

So x = [1] creates the object [1] and binds the x variable to it.

When you then do y = x, that doesn't create a new object, it simply binds y to the already-existing object. That means x and y are now both bound to the same object so, when you do an operation that modifies the object, like x.append(), both x and y will be affected.

That doesn't occur with z = x; z = 'nothing' since the second step there is creating a new object and binding z to it, so that x/y and z are now bound to different things.

It also doesn't occur with either of:

z = x[:]
z = [item for item in x]

since both of those also create a new object (a copy of the original(1)), separating x and z.

The effect can be seen by examining the IDs of each variable (probably the only reason you should ever use id), with common prefix removed for readability:

>>> print(id(x)); print(id(y)); print(id(z))
49864
49864
53808

You can see that x and y are the same object, and z is different.


(1) Keep in mind you may need to do a deep copy if your data structures are sufficiently complex, such as (with comments added):

>>> x = [[1,2],[3,4]]; y = x; print(id(x)); print(id(y))
773190920  # same object
773190920

>>> y = x[:]; print(id(x)); print(id(y))
773190920  # different objects
832649864

>>> print(id(x[0])); print(id(y[0]))
773088840  # but the sub-objects are still identical.
773088840

Upvotes: 0

Daniel N.
Daniel N.

Reputation: 116

When you define it this way. The variable x is initialised with the parent class and each child that inherits the class takes a reference to the variable (not a copy). It becomes a global variable under the class name.

To achieve the output you expected:

from pprint import pprint

class a (object):
    def __init__(self):
        self.x = ['1']

class b (a):
    def __init__(self):
        super().__init__()
        self.x.append('2')

class c (a):
    def __init__(self):
        super().__init__()
        self.x.append('3')

class d (a):
    def __init__(self):
        super().__init__()
        self.x = 'nothing'

if __name__ == '__main__':
    pprint(a().x)
    pprint(b().x)
    pprint(c().x)
    pprint(d().x)

Upvotes: 1

azro
azro

Reputation: 54148

1. 2. When doing x = a.x you're just assigning x to point the to the only existing list, the one from a so when operating on x this reflects on a.x because this is the same list, not a copy. This is both correct for class b and class c. To do a copy

x = list(a.x)

3. When doing x='nothing' you're assigning a string into x which doesn't point anymore to the list, just that

Upvotes: 2

Ami Tavory
Ami Tavory

Reputation: 76297

Your line

class b (a):
    x = a.x

creates another "name" for a.x, namely x (in that scope), but they are the same object. If you append to x, you are also appending to a.x - it is appending to the same object.

The only place you do something different is in

x = 'nothing'

where you are now binding x to a different object, a string.


If you would change your code to

class b (a):
    x = a.x.copy()

you would get different behaviour: that says that x is now a "name" for a copy of the list that is a.x.

Upvotes: 2

Related Questions