Michael Ojoko
Michael Ojoko

Reputation: 1

Python Class Variables Vs Instance Variables

class Animal:
    x = {}
    y = 0
    def __init__(self):
        pass


animal1 = Animal()
animal2 = Animal()

animal1.x['num'] = 14
animal1.y = 14

print(animal2.x)
print(animal2.y)

Output:

{'num': 14}

0

Why is a dictionary treated as a class variable but an int is unique for each instance? I'm still pretty new to Objects and Classes I was just messing around with things and noticed this.

I expected both to remain unchanged

Upvotes: 0

Views: 660

Answers (3)

ShadowRanger
ShadowRanger

Reputation: 155604

It's not about the types, it's about rebinding and name shadowing. When you assign directly to an attribute of an instance, you bind an attribute of that name on the instance. So animal1.y = 14 is saying "for this specific instance, it should have an attribute named y with the value 14". It doesn't matter that the class had an attribute of the same name, those are only found as a fallback when reading an attribute (there's some weirdness with the descriptor protocol I'm glossing over here, but that's the general rule).

By contrast, when you say animal1.x['num'] = 14 you are not writing to x. You are reading from animal1.x (which, since the instance lacks that attribute, it's read from the class), then writing into the thing you just read. The behavior is exactly the same as if you said:

xalias = animal1.x
xalias['num'] = 14

animal1.x is clearly not being written in that broken up version, and it's not being written to in the compact version either, you just modified the dict through an alias. You've told Python "please load from animal1.x, then assign 14 to the key 'num' within whatever you just loaded."

If you had instead said animal1.x = {'num': 14} ("for this specific instance, it should have an attribute named x with the value {'num': 14}"), it would behave just like it did for animal1.y = 14; the instance would get its own shadowing attribute named x that hides the x from the class, with it's own separate dictionary, because you bound a new object ("wrote") to the attribute on the instance.

This behavior shares certain similarities with the scoping rules for functions. If you have a function that never writes to a variable of a given name, only reads from it, it will read that variable from an outer (enclosing, global, or builtin) scope. If you assign to that variable though (without using global or nonlocal declarations to override the default behavior), it becomes a local, and its not possible to read the variable of that name from outer scopes without jumping through hoops. In the case of function scope, this choice is statically determined when the function is compiled (the variable is either local or nonlocal for the entire body of the function), the only difference with instance attributes shadowing class attributes is that it occurs dynamically (the instance attribute can be created lazily and only begins shadowing the class attribute when it's actually created).

It's also easier to work around the issue for instance/class attributes than for local/nonlocal variables; if you wanted the assignment animal1.y = 14 to replace the class attribute instead, even if you didn't know what class animal1 came from, you could do type(animal1).y = 14. That will show the change in animal2.y just fine, because you modified the original class (type(animal1) is returning Animal itself here), not an instance of it.

Upvotes: 1

EarthCow
EarthCow

Reputation: 67

Class variables can be difficult to understand at first. They are essentially variables that are shared between all instances of a class. Two practical applications of this is are as an instance counter, or as default settings for all objects. If they are changed through the definition itself (Animal.y = 14) they will be updated for all instances and future instances.

This contrasts entirely with instance variables which are always specific to and only accessible through their respective object.

However, the problem with the example you provided is that you are confusing assignment with mutation. You mutated the dictionary whereas you assigned the integer. To assign the dictionary the same way you assigned 14 to the y variable you would have to create a new dictionary like so:

animal1.x = {'num': 14}

This way it will not affect the animal2 object.

Upvotes: 1

aRTy
aRTy

Reputation: 131

Defining the variables like you did makes them class variables. If you want to guarantee a fresh copy for every instance, you'd do something like

class Animal:
    def __init__(self):
        self.x = {}
        self.y = 0

In your case the definition

class Animal:
    x = {}
    y = 0

means that every instance of the class Animal gets the identical reference to an empty dictionary, there are not multiple dictionaries. If you add to that dictionary, every instance will know. If you wanted to give an instance it's own version, you could technically do animal1.x = {'num': 14}. This way you create a new dictionary and assign it. This won't affect animal2.

Upvotes: 1

Related Questions