13

The following code troubles me:-

class mytest:
    name="test1"
    tricks=list()
    def __init__(self,name):
        self.name=name
        #self.tricks=[name]
        self.tricks.append(name)

t1=mytest("hello world")
t2=mytest("bye world")
print t1.name,t2.name
print t1.tricks,t2.tricks

The output is:-

hello world bye world
['hello world', 'bye world'] ['hello world', 'bye world']

meaning that the list tricks is shared by the two instances t1 and t2, which has been explained in the section 9.3.5 of https://docs.python.org/3/tutorial/classes.html

However, if I execute the following code:-

class mytest:
    name="test1"
    tricks=list()
    def __init__(self,name):
        self.name=name
        self.tricks=[name]
        self.tricks.append(name)

t1=mytest("hello world")
t2=mytest("bye world")
x=t1.tricks
if type(x) is list:
    print 'a list'
elif type(x) is tuple:
    print 'a tuple'
else:
    print 'neither a tuple or a list'
print t1.name,t2.name
print t1.tricks,t2.tricks

The output is the following:-

a list
hello world bye world
['hello world', 'hello world'] ['bye world', 'bye world']

Now it seems that the list tricks is no longer shared by those two instances t1 and t2. My question is, what are the mechanics here?

Peter Haddad
  • 78,874
  • 25
  • 140
  • 134
Yicun Zhen
  • 139
  • 1
  • 10
  • It isn't really about the class attribute in the second case. It's the underlying object. Every call to `__init__` creates a new list object: `[name]`. Therefore, the list referenced by the attribute `tricks` of each instance is independent. – Moses Koledoye Jul 24 '17 at 15:46
  • @MosesKoledoye And the newly created list `[name]` covers the old list `tricks` ? Is the old object `tricks` deleted then? – Yicun Zhen Jul 24 '17 at 15:47
  • 3
    If you write `self.tricks = [name]`, then `self` receives an attribute `tricks` which will shadow the class member `myclass.tricks`. – Right leg Jul 24 '17 at 15:49
  • 3
    The second code creates an instance attribute `self.tricks` that shadows the class attribute `mytest.tricks`. So if you want to access `mytest.tricks` in the second code you need to use the `mytest.tricks` name. – PM 2Ring Jul 24 '17 at 15:50
  • 3
    You might want to conform to [class naming conventions](https://www.python.org/dev/peps/pep-0008/#class-names), even for toy examples, just to stay in practice. – wwii Jul 24 '17 at 15:57

6 Answers6

10

The difference is that in your second example you are creating a new list, self.tricks, as an attribute of the object:

def __init__(self,name):
    self.name=name
    self.tricks=[name]    # <-- this is creating the new attribute for the object
    self.tricks.append(name)

The first example works because of Python's way of resolving the names: If self.tricks cannot be found in the object (because it hasn't been created), then it tries to find it as a member of the class. Since tricks is there, then you can access it.

It may become clear to you if you try to use mytest.tricks in your second example:

def __init__(self,name):
    self.name=name
    mytest.tricks=[name]    # <-- this is accesing the class attribute instead
    self.tricks.append(name)

That will output what you are actually expecting.

carrdelling
  • 1,675
  • 1
  • 17
  • 17
3

Your first case creates a class variable, and the second creates an instance variable.

When you do refer self.foo, Python first checks for a foo element in the instance's namespace dictionary, and then checks for a foo element in the class's namespace dictionary.

In the first case, since you created a class variable named tricks with a mutable type (a list), and didn't re-assign it specifically on the method, modifications to that list are available to every instance of the class.

In your second case, things are identical except that you hid the class variable with an instance variable of the same name, so from that point on, all references to self.tricks refer to the instance variable instead of the class variable.

The second case illustrated:

mytest.__dict__ = {
    'name': 'test1',
    'tricks': [],  # this doesn't get updated
}

t1.__dict__ = {
    'name': 'some_passed_name'
    'tricks': ['some_passed_name'],
}
Daniel Fackrell
  • 8,338
  • 2
  • 11
  • 10
2

In the first case, you didn't create a tricks attribute on the object scope, so Python used the one from the class; in the second case, you created a new list and associated it with the object itself, so Python used that.

For a way more thorough explanation, please, take a look at: Python Class Attributes: An Overly Thorough Guide

Haroldo_OK
  • 6,612
  • 3
  • 43
  • 80
2

In your first example, the class mytest has a tricks member, shared by all its instances:

class mytest:
    name = "test1"
    tricks = list()
    def __init__(self,name):
        ...
        self.tricks.append(name)

In your second example, however, mytest instances additionnally have a tricks member:

class mytest:
    name = "test1"
    tricks = list()
    def __init__(self,name):
        ...
        self.tricks = [name]
        ...

The self.tricks = [name] statement gives an attribute named tricks to self, that is, the mytest instance. The class still has a common tricks member.

When calling instance.tricks, Python first looks for a tricks member in instance.__dict__. If it does not find any, it looks for a tricks member in type(instance).__dict__.

Therefore, instances of your first example have no tricks attribute, but Python will give you the mytest.tricks they all share. On the other hand, instances of your second example do have their own tricks attribute, and Python will return this one.

Right leg
  • 16,080
  • 7
  • 48
  • 81
1

There's a little thing to note here in this problem.

When you pass in the name and append it to the existing shared tricks list, it is, as you saw, shared by all the values, because it is that list.

However, when you do self.tricks=[name] in your second example, you are erasing that objects instance of self.tricks and are replacing it with the list [name]

This is similar to having a parent and child class; when the child class doesn't give a different definition for an existing function, calling it calls the parent's function. But if you do, it calls the child's function.

Davy M
  • 1,697
  • 4
  • 20
  • 27
0

In the both case, you replacing the self.name to a new value.

In the first case you're mutating the self.tricks list, mutating a list don't replace it. so during the whole execution you have a single list, being mutated.

In the second case, the line self.tricks=[name] is changing the list, creating a new list object.

You can easily introspect this like:

class mytest:
    name = "test1"
    tricks = list()
    print("tricks id during class definition is: {}".format(id(tricks)))

    def __init__(self, name):
        self.name = name
        print("self.tricks id at the beginning of __init__ is: {}".format(id(self.tricks)))
        self.tricks = [name]
        print("self.tricks id after being replaced is: {}".format(id(self.tricks)))
        print("but mytest.tricks id is still: {}".format(id(mytest.tricks)))
        self.tricks.append(name)

t1=mytest("hello world")
t2=mytest("bye world")

Giving:

tricks id during class definition is: 140547174832328
self.tricks id at the beginning of __init__ is: 140547174832328
self.tricks id after being replaced is: 140547174831432
but mytest.tricks id is still: 140547174832328
self.tricks id at the beginning of __init__ is: 140547174832328
self.tricks id after being replaced is: 140547174830600
but mytest.tricks id is still: 140547174832328
Julien Palard
  • 8,736
  • 2
  • 37
  • 44