Python Class Methods Behaving Strangely

Viewed 59

I've written in Python for some time now, but I want to become proficient and very comfortable with its language so I started some online courses. During the lecture the teacher went over decorators as a means to work with class variables and I wanted to better understand them so I started playing around. Below is my sample script testing instance attributes vs class attributes, but the final print is doing something unexpected. Using the @classmethod decorator, I expected all instances of a class to show the update, however this does not appear to be so with inheritance. Even odder, when I change cls to be Game it works with inheritance. Anyone up to explaining to me why this is, because until testing I thought that cls and Game were the same thing in this context. A copy of the code as it was programmed in an online compiler: https://www.online-python.com/MFCErqQaR4

CODE:

class Game:
    game_title = "FIRST"
    def __init__(self):
        self.game_name = "Game One"
        
    def change_game_name(self, new_name):
        self.game_name = new_name
        
    @classmethod
    def change_game_title(cls, new_title):
        cls.game_title = new_title
        
class Player(Game):
    def __init__(self):
        super().__init__()
        self.player_name = "Bob"
        
game = Game()
player = Player()

print("===Game Obj")
print(game.game_title)
print(game.game_name)
print("===Player Obj")
print(player.game_title)
print(player.game_name)
print()

game.change_game_title("SECOND")

print("===Game Obj")
print(game.game_title)
print(game.game_name)
print("===Player Obj")
print(player.game_title)
print(player.game_name)
print()

player.change_game_name("Game Two")

print("===Game Obj")
print(game.game_title)
print(game.game_name)
print("===Player Obj")
print(player.game_title)
print(player.game_name)
print()

player.change_game_title("THIRD")

print("===Game Obj")
print(game.game_title)
print(game.game_name)
print("===Player Obj")
print(player.game_title)
print(player.game_name)
print()

player.change_game_name("Game Three")

print("===Game Obj")
print(game.game_title)
print(game.game_name)
print("===Player Obj")
print(player.game_title)
print(player.game_name)
print()

OUTPUT:

===Game Obj
FIRST
Game One
===Player Obj
FIRST
Game One

===Game Obj
SECOND
Game One
===Player Obj
SECOND
Game One

===Game Obj
SECOND
Game One
===Player Obj
SECOND
Game Two

===Game Obj
SECOND
Game One
===Player Obj
THIRD
Game Two

===Game Obj
SECOND
Game One
===Player Obj
THIRD
Game Three
2 Answers

Class variables, as opposed to instance variables, exist once for any particular class. The reason that you're seeing behavior you don't expect is that you aren't taking into account the fact that Player and Game are two different classes, and so they have two different sets of variables. Class methods are inherited, but the class object that they are operating on is the true type of the underlying object that the method was called on, regardless of which class that method comes from.

When you make this call:

player.change_game_title("THIRD")

you are making the call on a Player class object, because that is the type of player. So then in the method:

class Game:
    @classmethod
    def change_game_title(cls, new_title):
        cls.game_title = new_title

cls is still a reference to the Player class object, not the Game class object, even though the method is defined in the Game class. The type of the object you're calling methods on won't magically change just because you end up executing a method defined in the parent class of that object.

When you specify Game instead of cls, you get different behavior...the behavior you expected when you used cls...because now you are operating on the Game class's version of the variable instead of the Player class's version.

In Python, inheritance means sharing namespaces. All objects share namespaces with their parent class, and all classes in the method resolution order (MRO) of that class. "Method" resolution order is a bit of a misnomer, it' just attribute resolution order (all methods are attributes, not all attributes are methods).

But in this simple case, an instances shares the state of its class and the state of the parent class, in other words, lookups check the class namespace and the parent class namespace if no instance attribute is found (instance attributes being precisely those that belong to the instance namespace).

The lookups occur in that order. And assignments happen directly to the object that is being assigned to, be it the instance, the class, or some other class in the method resolution order.

Names "lower down" the namespace ladder [I]shadow[/I] the same name in higher namespaces.

The other thing to understand here is that @classmethod provides the type (i.e. class) of the instance of class it is called on.

When you do:

@classmethod
def change_game_title(cls, new_title):
    cls.game_title = new_title

cls gets the class of the instance you've called it upon.

So for the player instance,

cls.game_title = new_title

Means:

Player.game_title = new_title

And for game instances, that means:

Game.game_title = new_title

So when you look up player.game_title, it first checks the instance namespace, doesn't find it, and then it's parent's namespace, doesn't find it, and finally, the grandparent.

Here are the namespaces, retrieved usings vars*:

In [3]: vars(game), *map(vars, type(game).mro())
Out[3]:
({'game_name': 'Game One'},
 mappingproxy({'__module__': '__main__',
               'game_title': 'FIRST',
               '__init__': <function __main__.Game.__init__(self)>,
               'change_game_name': <function __main__.Game.change_game_name(self, new_name)>,
               'change_game_title': <classmethod at 0x7fa5d4cde4d0>,
               '__dict__': <attribute '__dict__' of 'Game' objects>,
               '__weakref__': <attribute '__weakref__' of 'Game' objects>,
               '__doc__': None}),
 mappingproxy({'__repr__': <slot wrapper '__repr__' of 'object' objects>,
               '__hash__': <slot wrapper '__hash__' of 'object' objects>,
               '__str__': <slot wrapper '__str__' of 'object' objects>,
               '__getattribute__': <slot wrapper '__getattribute__' of 'object' objects>,
               '__setattr__': <slot wrapper '__setattr__' of 'object' objects>,
               '__delattr__': <slot wrapper '__delattr__' of 'object' objects>,
               '__lt__': <slot wrapper '__lt__' of 'object' objects>,
               '__le__': <slot wrapper '__le__' of 'object' objects>,
               '__eq__': <slot wrapper '__eq__' of 'object' objects>,
               '__ne__': <slot wrapper '__ne__' of 'object' objects>,
               '__gt__': <slot wrapper '__gt__' of 'object' objects>,
               '__ge__': <slot wrapper '__ge__' of 'object' objects>,
               '__init__': <slot wrapper '__init__' of 'object' objects>,
               '__new__': <function object.__new__(*args, **kwargs)>,
               '__reduce_ex__': <method '__reduce_ex__' of 'object' objects>,
               '__reduce__': <method '__reduce__' of 'object' objects>,
               '__subclasshook__': <method '__subclasshook__' of 'object' objects>,
               '__init_subclass__': <method '__init_subclass__' of 'object' objects>,
               '__format__': <method '__format__' of 'object' objects>,
               '__sizeof__': <method '__sizeof__' of 'object' objects>,
               '__dir__': <method '__dir__' of 'object' objects>,
               '__class__': <attribute '__class__' of 'object' objects>,
               '__doc__': 'The most base type'}))

In [4]: vars(player), *map(vars, type(player).mro())
Out[4]:
({'game_name': 'Game One', 'player_name': 'Bob'},
 mappingproxy({'__module__': '__main__',
               '__init__': <function __main__.Player.__init__(self)>,
               '__doc__': None}),
 mappingproxy({'__module__': '__main__',
               'game_title': 'FIRST',
               '__init__': <function __main__.Game.__init__(self)>,
               'change_game_name': <function __main__.Game.change_game_name(self, new_name)>,
               'change_game_title': <classmethod at 0x7fa5d4cde4d0>,
               '__dict__': <attribute '__dict__' of 'Game' objects>,
               '__weakref__': <attribute '__weakref__' of 'Game' objects>,
               '__doc__': None}),
 mappingproxy({'__repr__': <slot wrapper '__repr__' of 'object' objects>,
               '__hash__': <slot wrapper '__hash__' of 'object' objects>,
               '__str__': <slot wrapper '__str__' of 'object' objects>,
               '__getattribute__': <slot wrapper '__getattribute__' of 'object' objects>,
               '__setattr__': <slot wrapper '__setattr__' of 'object' objects>,
               '__delattr__': <slot wrapper '__delattr__' of 'object' objects>,
               '__lt__': <slot wrapper '__lt__' of 'object' objects>,
               '__le__': <slot wrapper '__le__' of 'object' objects>,
               '__eq__': <slot wrapper '__eq__' of 'object' objects>,
               '__ne__': <slot wrapper '__ne__' of 'object' objects>,
               '__gt__': <slot wrapper '__gt__' of 'object' objects>,
               '__ge__': <slot wrapper '__ge__' of 'object' objects>,
               '__init__': <slot wrapper '__init__' of 'object' objects>,
               '__new__': <function object.__new__(*args, **kwargs)>,
               '__reduce_ex__': <method '__reduce_ex__' of 'object' objects>,
               '__reduce__': <method '__reduce__' of 'object' objects>,
               '__subclasshook__': <method '__subclasshook__' of 'object' objects>,
               '__init_subclass__': <method '__init_subclass__' of 'object' objects>,
               '__format__': <method '__format__' of 'object' objects>,
               '__sizeof__': <method '__sizeof__' of 'object' objects>,
               '__dir__': <method '__dir__' of 'object' objects>,
               '__class__': <attribute '__class__' of 'object' objects>,
               '__doc__': 'The most base type'}))

Here's what happens after mutator methods are run, that set the attribute using the dynamically resolved cls, which is basically just type(instance).

In [5]: game.change_game_title("SECOND")
   ...:

In [6]: player.change_game_title("THIRD")
   ...:

In [7]: vars(game), *map(vars, type(game).mro())
Out[7]:
({'game_name': 'Game One'},
 mappingproxy({'__module__': '__main__',
               'game_title': 'SECOND',
               '__init__': <function __main__.Game.__init__(self)>,
               'change_game_name': <function __main__.Game.change_game_name(self, new_name)>,
               'change_game_title': <classmethod at 0x7fa5d4cde4d0>,
               '__dict__': <attribute '__dict__' of 'Game' objects>,
               '__weakref__': <attribute '__weakref__' of 'Game' objects>,
               '__doc__': None}),
 mappingproxy({'__repr__': <slot wrapper '__repr__' of 'object' objects>,
               '__hash__': <slot wrapper '__hash__' of 'object' objects>,
               '__str__': <slot wrapper '__str__' of 'object' objects>,
               '__getattribute__': <slot wrapper '__getattribute__' of 'object' objects>,
               '__setattr__': <slot wrapper '__setattr__' of 'object' objects>,
               '__delattr__': <slot wrapper '__delattr__' of 'object' objects>,
               '__lt__': <slot wrapper '__lt__' of 'object' objects>,
               '__le__': <slot wrapper '__le__' of 'object' objects>,
               '__eq__': <slot wrapper '__eq__' of 'object' objects>,
               '__ne__': <slot wrapper '__ne__' of 'object' objects>,
               '__gt__': <slot wrapper '__gt__' of 'object' objects>,
               '__ge__': <slot wrapper '__ge__' of 'object' objects>,
               '__init__': <slot wrapper '__init__' of 'object' objects>,
               '__new__': <function object.__new__(*args, **kwargs)>,
               '__reduce_ex__': <method '__reduce_ex__' of 'object' objects>,
               '__reduce__': <method '__reduce__' of 'object' objects>,
               '__subclasshook__': <method '__subclasshook__' of 'object' objects>,
               '__init_subclass__': <method '__init_subclass__' of 'object' objects>,
               '__format__': <method '__format__' of 'object' objects>,
               '__sizeof__': <method '__sizeof__' of 'object' objects>,
               '__dir__': <method '__dir__' of 'object' objects>,
               '__class__': <attribute '__class__' of 'object' objects>,
               '__doc__': 'The most base type'}))

In [8]: vars(player), *map(vars, type(player).mro())
Out[8]:
({'game_name': 'Game One', 'player_name': 'Bob'},
 mappingproxy({'__module__': '__main__',
               '__init__': <function __main__.Player.__init__(self)>,
               '__doc__': None,
               'game_title': 'THIRD'}),
 mappingproxy({'__module__': '__main__',
               'game_title': 'SECOND',
               '__init__': <function __main__.Game.__init__(self)>,
               'change_game_name': <function __main__.Game.change_game_name(self, new_name)>,
               'change_game_title': <classmethod at 0x7fa5d4cde4d0>,
               '__dict__': <attribute '__dict__' of 'Game' objects>,
               '__weakref__': <attribute '__weakref__' of 'Game' objects>,
               '__doc__': None}),
 mappingproxy({'__repr__': <slot wrapper '__repr__' of 'object' objects>,
               '__hash__': <slot wrapper '__hash__' of 'object' objects>,
               '__str__': <slot wrapper '__str__' of 'object' objects>,
               '__getattribute__': <slot wrapper '__getattribute__' of 'object' objects>,
               '__setattr__': <slot wrapper '__setattr__' of 'object' objects>,
               '__delattr__': <slot wrapper '__delattr__' of 'object' objects>,
               '__lt__': <slot wrapper '__lt__' of 'object' objects>,
               '__le__': <slot wrapper '__le__' of 'object' objects>,
               '__eq__': <slot wrapper '__eq__' of 'object' objects>,
               '__ne__': <slot wrapper '__ne__' of 'object' objects>,
               '__gt__': <slot wrapper '__gt__' of 'object' objects>,
               '__ge__': <slot wrapper '__ge__' of 'object' objects>,
               '__init__': <slot wrapper '__init__' of 'object' objects>,
               '__new__': <function object.__new__(*args, **kwargs)>,
               '__reduce_ex__': <method '__reduce_ex__' of 'object' objects>,
               '__reduce__': <method '__reduce__' of 'object' objects>,
               '__subclasshook__': <method '__subclasshook__' of 'object' objects>,
               '__init_subclass__': <method '__init_subclass__' of 'object' objects>,
               '__format__': <method '__format__' of 'object' objects>,
               '__sizeof__': <method '__sizeof__' of 'object' objects>,
               '__dir__': <method '__dir__' of 'object' objects>,
               '__class__': <attribute '__class__' of 'object' objects>,
               '__doc__': 'The most base type'}))
Related