According to (already mentioned) [Python.Docs]: Data model (emphasis is mine):
Custom classes
Custom class types are typically created by class definitions (see section Class definitions). A class has a namespace implemented by a dictionary object. Class attribute references are translated to lookups in this dictionary, e.g., C.x
is translated to C.__dict__["x"]
(although there are a number of hooks which allow for other means of locating attributes). When the attribute name is not found there, the attribute search continues in the base classes.
...
Class instances
A class instance is created by calling a class object (see above). A class instance has a namespace implemented as a dictionary which is the first place in which attribute references are searched. When an attribute is not found there, and the instance’s class has an attribute by that name, the search continues with the class attributes.
...
Invoking Descriptors
...
The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x
has a lookup chain starting with a.__dict__['x']
, then type(a).__dict__['x']
, and continuing through the base classes of type(a)
excluding metaclasses.
However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined and how they were called.
Attributes defined inside a class definition (but outside the initializer (or other methods)) are called class attributes, and are bound to the class itself rather than its instances. It's like static members from C++ or Java. [Python.Docs]: Compound statements - Class definitions states (emphasis still mine):
Programmer’s note: Variables defined in the class definition are class attributes; they are shared by instances. Instance attributes can be set in a method with self.name = value
. Both class and instance attributes are accessible through the notation “self.name
”, and an instance attribute hides a class attribute with the same name when accessed in this way. Class attributes can be used as defaults for instance attributes, but using mutable values there can lead to unexpected results. Descriptors can be used to create instance variables with different implementation details.
So, the attribute lookup order can be summarized like below (traverse in ascending order, when attribute name found simply return its value (therefore ignoring the remaining entries)). The first steps performed by the (builtin) __getattribute__ method:
Descriptors (if any - note that their presence could also be triggered indirectly (by other features))
Instance namespace (foo.__dict__)
Instance class namespace (Foo.__dict__)
Instance class base classes namespaces (e.__dict__ for e in Foo.__mro__
)
Anything that a custom __getattr__ method might return
The above is what typically happens, as Python being highly customizable that can be altered (e.g. __slots__).
For an exact behavior, you could check the source code ([GitHub]: python/cpython - (main) cpython/Objects):
typeobject.c: type_getattro (optionally: super_getattro, slot_tp_getattro)
object.c: _PyObject_GenericGetAttrWithDict
Here's an example that will clear things up (hopefully).
code00.py:
#!/usr/bin/env python
import sys
from pprint import pformat as pf
def print_dict(obj, header="", indent=0, filterfunc=lambda x, y: not x.startswith("__")):
if not header:
header = getattr(obj, "__name__", None)
if header:
print("{:}{:}.__dict__:".format(" " * indent, header))
lines = pf({k: v for k, v in getattr(obj, "__dict__", {}).items() if filterfunc(k, v)}, sort_dicts=False).split("\n")
for line in lines:
print("{:}{:}".format(" " * (indent + 1), line))
print()
class Descriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
print("{:s}.__get__".format(self.name))
def __set__(self, instance, value):
print("{:s}.__set__ - {:}".format(self.name, value))
def __delete__(self, instance):
print("{:s}.__delete__".format(self.name))
class Demo:
cls_attr0 = 3.141593
cls_attr1 = Descriptor("cls_attr1")
'''
def __getattribute__(self, name):
print("__getattribute__:", self, name)
return super().__getattribute__(name)
'''
'''
def __getattr__(self, name):
print("__getattr__:", self, name)
return "something dummy"
'''
def __init__(self):
self.inst_attr0 = 2.718282
def main(*argv):
print("ORIGINAL")
demos = [Demo() for _ in range(2)]
demo0 = demos[0]
demo1 = demos[1]
print_dict(Demo)
print_dict(demo0, header="demo0")
print("\ndemo0 attrs:", demo0.cls_attr0, demo0.cls_attr1, demo0.inst_attr0)
print_dict(demo1, header="\ndemo1")
print("\ndemo1 attrs:", demo1.cls_attr0, demo1.cls_attr1, demo1.inst_attr0)
print("\nALTER 1ST INSTANCE OBJECT")
demo0.inst_attr0 = -3
demo0.cls_attr0 = -5
print_dict(Demo)
print_dict(demo0, header="demo0")
print("\ndemo0 attrs:", demo0.cls_attr0, demo0.cls_attr1, demo0.inst_attr0)
print_dict(demo1, header="\ndemo1")
print("\ndemo1 attrs:", demo1.cls_attr0, demo1.cls_attr1, demo1.inst_attr0)
print("\nALTER CLASS")
Demo.cls_attr0 = -7
Demo.cls_attr1 = -9
print_dict(Demo, header="Demo")
print_dict(demo1, header="demo0")
print("\ndemo0 attrs:", demo0.cls_attr0, demo0.cls_attr1, demo0.inst_attr0)
print_dict(demo1, header="\ndemo1")
print("\ndemo1 attrs:", demo1.cls_attr0, demo1.cls_attr1, demo1.inst_attr0)
if __name__ == "__main__":
print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
64 if sys.maxsize > 0x100000000 else 32, sys.platform))
rc = main(*sys.argv[1:])
print("\nDone.")
sys.exit(rc)
Output:
[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q072399556]> "e:\Work\Dev\VEnvs\py_pc064_03.09_test0\Scripts\python.exe" code00.py
Python 3.9.9 (tags/v3.9.9:ccb0e6a, Nov 15 2021, 18:08:50) [MSC v.1929 64 bit (AMD64)] 064bit on win32
ORIGINAL
Demo.__dict__:
{'cls_attr0': 3.141593,
'cls_attr1': <__main__.Descriptor object at 0x00000171B0B24FD0>}
demo0.__dict__:
{'inst_attr0': 2.718282}
cls_attr1.__get__
demo0 attrs: 3.141593 None 2.718282
demo1.__dict__:
{'inst_attr0': 2.718282}
cls_attr1.__get__
demo1 attrs: 3.141593 None 2.718282
ALTER 1ST INSTANCE OBJECT
Demo.__dict__:
{'cls_attr0': 3.141593,
'cls_attr1': <__main__.Descriptor object at 0x00000171B0B24FD0>}
demo0.__dict__:
{'inst_attr0': -3, 'cls_attr0': -5}
cls_attr1.__get__
demo0 attrs: -5 None -3
demo1.__dict__:
{'inst_attr0': 2.718282}
cls_attr1.__get__
demo1 attrs: 3.141593 None 2.718282
ALTER CLASS
Demo.__dict__:
{'cls_attr0': -7, 'cls_attr1': -9}
demo0.__dict__:
{'inst_attr0': 2.718282}
demo0 attrs: -5 -9 -3
demo1.__dict__:
{'inst_attr0': 2.718282}
demo1 attrs: -7 -9 2.718282
Done.