0

I'm a novice Python programmer utterly confused by OOP and the need for self in classes. I've read the many SO posts, blogs* etc that attempt to demystify it, but could use some help on this particular scenario.

I am basing the below scenarios off the Python2 tutorial on Class and Instance Variables. I'll lead with my examples, followed by explicit questions beneath.

First, I'll directly set tricks, and then use the add_trick method:

class Dog:

    tricks = []      

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

    def print_self_tricks(self):
        print(self.tricks)

a = Dog('spencer')
b = Dog('rex')

a.tricks = ['foo'] # How is this even possible? Where did I define self.tricks? 

a.print_self_tricks()
['foo']    # Ok, the below results make sense
b.print_self_tricks()
[]
a.tricks
['foo']
b.tricks
[]

a.add_trick('jump')

a.print_self_tricks()
['foo', 'jump']
b.print_self_tricks()
[]          # Great, as expected
a.tricks
['foo', 'jump']
b.tricks
[]          # Great, as expected

Now, the exact same setup, but executed in a different order: add_trick before direct setting of trick.

class Dog:

    tricks = []      

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

    def print_self_tricks(self):
        print(self.tricks)

a = Dog('spencer')
b = Dog('rex')

a.add_trick('jump')

a.print_self_tricks()
['jump']
b.print_self_tricks() 
['jump']    # Welp, that's not expected. 
a.tricks
['jump']
b.tricks
['jump']    # Welp, that's not expected. 

a.tricks = ['foo']

a.print_self_tricks()
['foo']
b.print_self_tricks() 
['jump']    # Great, but inconsistent with the behavior above resulting from a.add_trick
a.tricks
['foo']
b.tricks
['jump']    # Great, but inconsistent with the behavior above resulting from a.add_trick
  • Why does self.tricks exist? It was never defined in __init__ (e.g. self.tricks = tricks). I wouldn't expect it to be possible to set self.tricks since it wasn't previously defined.
  • Why does my approach not result in a different answer? That is, why does a.tricks = ['foo'] seem to behave as intended compared a.add_trick('roll over')?
  • It seems like order of operations matters. Why does calling add_trick before setting a.trick result in a different outcome?

*Here's what I've read so far, are there better explanations?

Peter
  • 1,065
  • 14
  • 29
  • "How is this even possible? Where did I define self.tricks?" - right there, on that line, with that assignment. – user2357112 Mar 17 '18 at 01:49
  • `# How is this even possible? Where did I define self.tricks?` Literally on that line. But you are getting confused because you are shadowing a class-level variable with an instance variable. – juanpa.arrivillaga Mar 17 '18 at 01:50
  • 1
    `tricks` in this case is a class variable shared across all instances of `Dog`s – avigil Mar 17 '18 at 01:51

1 Answers1

4
class Dog:
    tricks = []

This defines tricks as a property on the class, not on any instance. A class property is in a sense "shared" between all instances of the class. It should ideally be accessed via the class: Dog.tricks, but Python also allows access via an instance, e.g. a.tricks or self.tricks. The rule is that if x is an instance, x.y will refer to the attribute y of the instance x if it exists, otherwise the attribute y of x's class.

a.tricks = ['foo']

Python is a dynamically-typed language which does not require advance declarations of classes, attributes, or variables. This statement creates an attribute on the Dog instance that a refers to, and makes it refer to a list with one string. Before this, a.tricks would resolve to the still-empty list that the class attribute refers to, but after this, a.tricks will resolve to ['foo']. tricks has not been set on b, so b.tricks still resolves to the same thing as Dog.tricks.

So the behavior you're seeing is because until you explicitly set a.tricks to something, a and b are sharing the class attribute. You probably didn't intend this - try the below, which will give each instance its own tricks:

class Dog:
    def __init__(self, name):
        self.name = name
        self.tricks = []
Aasmund Eldhuset
  • 37,289
  • 4
  • 68
  • 81