8

Python provides us many possibilities on instance/class attribute, for example:

class A(object):
    def __init__(self):
        self.foo = "hello"

a = A()

There are many ways to access/change the value of self.foo:

  1. direct access a.foo
  2. inner dict a.__dict__['foo']
  3. get and set a.__get__ and a.__set__,of course there two are pre-defined methods.
  4. getattribute a.__getattribute__
  5. __getattr__ and __setattr__
  6. maybe more.

While reading source code, I always get lost of what's their ultimate access order? When I use a.foo, how do I know which method/attribute will get called actually?

cizixs
  • 12,931
  • 6
  • 48
  • 60
  • 1
    1. `foo` is an *instance attribute*, not a class attribute. 2. As it isn't a `@property` (or other descriptor) `__get__` and `__set__` won't be involved. – jonrsharpe Jun 21 '15 at 05:56
  • For e.g. `__getattr__` vs. `__getattribute__`, see http://stackoverflow.com/q/3278077/3001761 – jonrsharpe Jun 21 '15 at 06:05

2 Answers2

17

I found out this great post that has a detailed explanation on object/class attribute lookup.

For object attribute lookup:

Assuming Class is the class and instance is an instance of Class, evaluating instance.foobar roughly equates to this:

  • Call the type slot for Class.__getattribute__ (tp_getattro). The default does this:
    • Does Class.__dict__ have a foobar item that is a data descriptor ?
      • If yes, return the result of Class.__dict__['foobar'].__get__(instance, Class).
    • Does instance.__dict__ have a 'foobar' item in it?
      • If yes, return instance.__dict__['foobar'].
    • Does Class.__dict__ have a foobar item that is not a data descriptor [9]?
      • If yes, return the result of Class.__dict__['foobar'].__get__(instance, klass). [6]
  • If the attribute still wasn't found, and there's a Class.__getattr__, call Class.__getattr__('foobar').

There is an illustrated image for this:

enter image description here

Please do check out the original blog if interested which gives a outstanding explanation on python class, attribute lookup, and metaclass.

Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
cizixs
  • 12,931
  • 6
  • 48
  • 60
  • how confusing - one route checks class dict then instance dict then class dict again ?!! – joel Oct 08 '19 at 12:13
  • This chart is just what I needed. I'm curious tho how `__slots__` members fit in there. I'm guessing in the same spot as instance dict, but using `__slots__`, instance dict doesn't actually exist. – Jason Forbes Mar 07 '21 at 03:58
  • @JasonForbes Slots are actually just data descriptors. Try `class Demo: __slots__ = ['foo']`, and then look at the value of `Demo.foo`. It's an object with a `__get__` and a `__set__` method. – Aran-Fey Jul 24 '22 at 07:54
  • There are a few inaccuracies in this answer: 1) "Does `Class.__dict__` have a foobar item" is incorrect because python actually looks for `foobar` in the `__dict__`s of all classes in the [MRO](https://docs.python.org/3/glossary.html#term-method-resolution-order) of `Class`. Or to put it differently: `foobar` can also come from a parent class. 2) If `foobar` is a class attribute but not a descriptor, python won't call `__get__` on it. (Obviously, that wouldn't work because the method doesn't exist.) – Aran-Fey Jul 24 '22 at 08:08
7

bar = a.foo...

  1. invokes a.__getattribute__('foo')
  2. which in turn by default looks up a.__dict__['foo']
  3. or invokes foo's .__get__() if defined on A.

The returned value would then be assigned to bar.


a.foo = bar...

  1. invokes a.__getattribute__('foo')
  2. which in turn by default looks up a.__dict__['foo']
  3. or invokes foo's .__set__(bar) if defined on A.
Amber
  • 507,862
  • 82
  • 626
  • 550
  • Well explained. great. – Fawzan Jun 21 '15 at 04:05
  • 2
    This isn't quite accurate; `__get__` and `__set__` are for descriptors, which are stored on the *class*, so wouldn't be found in `a.__dict__`. – jonrsharpe Jun 21 '15 at 06:00
  • @jonrsharpe tweaked to be more technically correct. – Amber Jun 21 '15 at 06:29
  • I also think this is still not completely correct. First `_getattribute__` will check if it is a descriptor by looking for `__get__()` in the class definition `A.foo` (when `a = A()`). If it is found then it is used, if not the next step is to look in the instance dict `a.__dict__['foo']` – Joe Oct 13 '20 at 09:20