ktzr
ktzr

Reputation: 1645

Class variables, difference between instance.var and instance.__class__.var

I see this was flagged as a duplicate of "What is the difference between class and instance variables?" However I don't believe I'm using any instance variables in my example, neither of my classes has a __init__ I'm editing class variables in two different ways and trying to understand the difference between them, not the difference between a class and instance variable.

I'm trying to understand the difference between calling a class variable just with .var and with .__class__.var. I thought it was to do with subclassing so I wrote the following code.

class Foo:
    foo = 0

class Bar(Foo):
    bar = 1

def print_class_vals(f, b):
    """ prints both instant.var and instant.__class__.var for foo and bar"""
    print("f.foo: {}, {} "
          "b.foo: {}, {} "
          "b.bar: {}, {} "
          "".format(f.foo, f.__class__.foo,
                    b.foo, b.__class__.foo,
                    b.bar, b.__class__.bar))

f = Foo()
b = Bar()
print_class_vals(f, b)

Foo.foo += 1
print_class_vals(f, b)

Bar.foo += 1
print_class_vals(f, b)

Bar.bar += 1
print_class_vals(f, b)

This outputs the following:

f.foo: 0, 0, b.foo: 0, 0, b.bar: 1, 1 
f.foo: 1, 1, b.foo: 1, 1, b.bar: 1, 1 
f.foo: 1, 1, b.foo: 2, 2, b.bar: 1, 1 
f.foo: 1, 1, b.foo: 2, 2, b.bar: 2, 2 

I can't seem to find any difference between calling inst.var and inst.__class__.var. How are they different and when should I use one over the other?

Upvotes: 1

Views: 137

Answers (2)

abarnert
abarnert

Reputation: 365617

While Gabriel Reis's answer explains this particular situation perfectly, there actually is a difference between f.foo and f.__class__.foo even if foo isn't shadowed by an instance attribute.

Compare:

>>> class Foo:
...     foo = 1
...     def bar(self): pass
...     baz = lambda self: None
>>> f = Foo()
>>> f.foo
1
>>> f.__class__.foo
1
>>> f.bar
<bound method Foo.bar of <__main__.Foo object at 0x11948cb00>>
>>> f.__class__.bar
<function __main__.Foo.bar(self)>
>>> f.bar()
>>> f.__class__.bar()
TypeError: bar() missing 1 required positional argument: 'self'

And the same is true for f.baz.

The difference is that, by directly accessing f.__class__.foo, you're making an end-run around the descriptor protocol, which is the thing that makes methods, @property, and similar things work.

If you want the full details, read the linked HOWTO, but the short version is that there's a bit more to it than Gabriel's answer says:

Python will look up for a name (attribute) first in the instance namespace/dict. If it doesn't find there, then it will look up in the class namespace. If it still doesn't find there, then it will walk through the base classes respecting the MRO (method resolution order).

But if it finds it in the class namespace (or any base class), and what it finds is a descriptor (a value with a __get__ method), it does an extra step. The details depend on whether it's a data or non-data descriptor (basically, whether it also has a __set__ method), but the short version is that instead of giving you the value, it calls __get__ on the value and gives you what that value returns. Functions have a __get__ method that returns a bound method; properties have a __get__ method that calls the property get method; etc.

Upvotes: 2

Gabriel Reis
Gabriel Reis

Reputation: 79

Python will look up for a name (attribute) first in the instance namespace/dict. If it doesn't find there, then it will look up in the class namespace. If it still doesn't find there, then it will walk through the base classes respecting the MRO (method resolution order).

What you've done there, is to define the class attributes Foo.foo and Bar.bar. You never modified any instance namespace.

Try this:

class Foo:
    foo = 1

f = Foo()
f.foo = 2

print('f.foo = {!r}'.format(f.foo))
print('f.__class__.foo = {!r}'.format(f.__class__.foo))

And you will be able to understand the difference.

Upvotes: 2

Related Questions