56

I think I have some misconception about class and instance variables. Here is an example code:

class Animal(object):
    energy = 10
    skills = []

    def work(self):
        print 'I do something'
        self.energy -= 1

    def new_skill(self, skill):
        self.skills.append(skill)


if __name__ == '__main__':

    a1 = Animal()
    a2 = Animal()

    a1.work()
    print a1.energy  # result:9
    print a2.energy  # result:10


    a1.new_skill('bark')
    a2.new_skill('sleep')
    print a1.skills  # result:['bark', 'sleep']
    print a2.skills  # result:['bark', 'sleep']

I thought that energy and skill were class variables, because I declared them out of any method. I modify its values inside the methods in the same way (with self in his declaration, maybe incorrect?). But the results show me that energy takes different values for each object (like a instance variable), while skills seems to be shared (like a class variable). I think I've missed something important...

neogurb
  • 690
  • 5
  • 15
  • 4
    This question is more subtle than that duplicate, since it's asking about the difference between behavior of two class attributes. I'm sure a duplicate still exists, but not that one. – BrenBarn Mar 03 '16 at 08:36
  • 4
    Yeah, you did. Energy is immutable and assigning to it replaces the variable, but on the instance, leaving the class alone. On the other hand you arent replacing skills, you are adding to the shared instance on the class. – JL Peyret Mar 03 '16 at 08:36
  • 2
    As to the answer, you did not "modify the values in the same way" as you claim. You modified energy with `self.energy -= 1`, an assignment; you modified `skills` with `self.skills.append(...)` a method call. Those are different. – BrenBarn Mar 03 '16 at 08:37
  • @BrenBarn: I was looking for additional posts to link to when I hit the London tunnel system. – Martijn Pieters Mar 03 '16 at 09:00
  • @BrenBarn: I was looking for [Why does += behave unexpectedly on lists?](http://stackoverflow.com/q/2347265); it is sort-of the opposite to what is happening here (an immutable value vs a mutable). – Martijn Pieters Mar 03 '16 at 09:35

5 Answers5

38

The trick here is in understanding what self.energy -= 1 does. It's really two expressions; one getting the value of self.energy - 1, and one assigning that back to self.energy.

But the thing that's confusing you is that the references are not interpreted the same way on both sides of that assignment. When Python is told to get self.energy, it tries to find that attribute on the instance, fails, and falls back to the class attribute. However, when it assigns to self.energy, it will always assign to an instance attribute, even though that hadn't previously existed.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • Which is also a neat trick for easily overriding default values. Have a good default on class, but assigning on an instance overwrites it just for that instance. – JL Peyret Mar 03 '16 at 08:54
  • Should probably mention the `new_skill()` ghosting between the classes in this example. – Torxed Mar 03 '16 at 08:58
  • Can you suggest the proper way to write the `new_skill` method? I'm trying my best to come at it from the C family with an open mind, but things like this make me wonder how python got so popular. (Not that C and C derivatives are without their faults, but this is some pretty basic class/encapsulation stuff.) – Bmo Mar 03 '16 at 12:59
  • I'm not sure what you're asking. `new_skill` is fine as it is, although you could just as easily drop it altogether and call `Animal.skills.append('whatever')` from outside the class. – Daniel Roseman Mar 03 '16 at 13:12
  • @Bmo That method is fine. You simply have to define `skills` as an instance attribute by putting `self.skills = []` in the `__init__` method (if this is what you were asking...) – Bakuriu Mar 03 '16 at 13:41
  • @Bakuriu so basically it is constructor-less as written? Adding `_init_` will attach those fields to the instance then? – Bmo Mar 03 '16 at 13:47
  • @Bmo The `__init__` *is* the constructor; in python there are no declarations only definitions. Even the class "declaration" is actually a definition because it creates an *object* (that is the class). So declaration of instance fields coincides with initialization of themselves. That's why it's called `__init__` for *initialize* instead of other names. Anyway using `__slots__` you can declare instance fields at the class level, and using descriptors you can even create *typed* fields. However these aren't native notions but can be implemented using metaprogramming. – Bakuriu Mar 03 '16 at 16:13
  • @Bakuriu Thanks, I think I am starting to get it. Appears to work almost like a static field. (but not exactly). I'll do some research on it, now that I have a better understanding. Thank you for taking the time. – Bmo Mar 03 '16 at 16:16
  • `energy` exists as a local variable at creation, not when `work` is executed : `a3=Animal(); a3.work.__func__.__code__.co_names` is `('print', 'energy')` – B. M. Mar 03 '16 at 16:59
33

You are running into initialization issues based around mutability.

First, the fix. skills and energy are class attributes. It is a good practice to consider them as read only, as initial values for instance attributes. The classic way to build your class is:

class Animal(object):
    energy = 10
    skills = []
    def __init__(self,en=energy,sk=None):
        self.energy = en
        self.skills = [] if sk is None else sk 

   ....

Then each instance will have its own attributes, all your problems will disappear.

Second, what's happening with this code? Why is skills shared, when energy is per-instance?

The -= operator is subtle. it is for in-place assignation if possible. The difference here is that list types are mutable so in-place modification often occurs:

In [6]: 
   b=[]
   print(b,id(b))
   b+=['strong']
   print(b,id(b))

[] 201781512
['strong'] 201781512

So a1.skills and a2.skills are the same list, which is also accessible as Animal.skills. But energy is a non-mutable int, so modification is impossible. In this case a new int object is created, so each instance manages its own copy of the energy variable:

In [7]: 
     a=10
     print(a,id(a))
     a-=1
     print(a,id(a))

10 1360251232
9 1360251200
B. M.
  • 18,243
  • 2
  • 35
  • 54
24

Upon initial creation both attributes are the same object:

>>> a1 = Animal()
>>> a2 = Animal()
>>> a1.energy is a2.energy
True
>>> a1.skills is a2.skills
True
>>> a1 is a2
False

When you assign to a class attribute, it is made local to the instance:

>>> id(a1.energy)
31346816
>>> id(a2.energy)
31346816
>>> a1.work()
I do something
>>> id(a1.energy)
31346840  # id changes as attribute is made local to instance
>>> id(a2.energy)
31346816

The new_skill() method does not assign a new value to the skills array, but rather it appends which modifies the list in place.

If you were to manually add a skill, then the skills list would be come local to the instance:

>>> id(a1.skills)
140668681481032
>>> a1.skills = ['sit', 'jump']
>>> id(a1.skills)
140668681617704
>>> id(a2.skills)
140668681481032
>>> a1.skills
['sit', 'jump']
>>> a2.skills
['bark', 'sleep']

Finally, if you were to delete the instance attribute a1.skills, the reference would revert back to the class attribute:

>>> a1.skills
['sit', 'jump']
>>> del a1.skills
>>> a1.skills
['bark', 'sleep']
>>> id(a1.skills)
140668681481032
ChrisFreeman
  • 5,831
  • 4
  • 20
  • 32
6

Access the class variables through the class, not through self:

class Animal(object):
    energy = 10
    skills = []

    def work(self):
        print 'I do something'
        self.__class__.energy -= 1

    def new_skill(self, skill):
        self.__class__.skills.append(skill)
Periodic Maintenance
  • 1,698
  • 4
  • 20
  • 32
3

Actually in you code a1.work(); print a1.energy; print a2.energy

when you are calling a1.work() an instance variable for a1 object is getting created with the same name that is 'energy'. And When interpreter comes to 'print a1.energy' it execute the instance variable of object a1. And when interpreter comes to 'print a2.energy' it execute the class variable, and since you have not changed the value of class variable it shows 10 as output.

Mahendra Gaur
  • 380
  • 2
  • 11