1

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?
3
  • 1
    This has nothing to do with scope. You've simply assigned the same list, the one that was originally in a, to all the x attributes of your other classes, x = a.x... Commented May 28, 2020 at 9:03
  • The assignment will be to that class variable, the other class variables are not affected by assignment. Commented May 28, 2020 at 9:04
  • You may now think about accepting an answer or comment one to get details ;) to reward those who spent time for you ;) Commented May 31, 2020 at 12:22

4 Answers 4

2

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.

Sign up to request clarification or add additional context in comments.

Comments

2

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

Comments

1

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)

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.