My mental, and technically not fully correct, model of getattr() is that it implements the “.” delimiter-acting-as-kind-of-operator. Then setattr() is the corresponding assignment variant “.=”.
Therefore, I mentally connect __getattr__() and __setattr__() methods to tailor the dotted member read and write access.
Indeed, assuming PEP 562’s first motivating example’s lib.py, and then disassembling:
>>> import dis, lib
>>> dis.dis('lib.old_function')
shows the same bytecode:
0 0 RESUME 0
1 2 LOAD_NAME 0 (lib)
4 LOAD_ATTR 1 (old_function)
14 RETURN_VALUE
as:
>>> class X:
... def __getattr__(self, name):
... return 1
>>> x = X()
>>> dis.dis('x.field')
i.e.:
0 0 RESUME 0
1 2 LOAD_NAME 0 (x)
4 LOAD_ATTR 1 (field)
14 RETURN_VALUE
The compiler generates the LOAD_ATTR instruction for both module and class instance member access. Then, the bytecode interpreter/virtual machine apparently calls the module’s or class’s __getattr__() method with the expected behaviour.
If we don’t use dotted access (lib.old_function), but rather import the member, it still works (which is specified in PEP 562, but nonetheless surprised me somewhat because of above mental model).
Disassembling:
>>> dis.dis('from lib import old_function')
yields:
0 0 RESUME 0
1 2 LOAD_CONST 0 (0)
4 LOAD_CONST 1 (('old_function',))
6 IMPORT_NAME 0 (lib)
8 IMPORT_FROM 1 (old_function)
10 STORE_NAME 1 (old_function)
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Apparently, the bytecode interpreter/virtual machine executes IMPORT_FROM equivalently as LOAD_ATTR, i.e. calls the __getattr__() as needed.
For the proposed PEP 562 extension, and with lib enhanced with CONSTANT = 3.14, the dotted access can work similarly for module as for class instance member, i.e.:
>>> dis.dis('x.field = 10')
>>> dis.dis('lib.CONSTANT = 42')
show:
0 0 RESUME 0
1 2 LOAD_CONST 0 (10)
4 LOAD_NAME 0 (x)
6 STORE_ATTR 1 (field)
16 LOAD_CONST 1 (None)
18 RETURN_VALUE
0 0 RESUME 0
1 2 LOAD_CONST 0 (42)
4 LOAD_NAME 0 (lib)
6 STORE_ATTR 1 (CONSTANT)
16 LOAD_CONST 1 (None)
18 RETURN_VALUE
Similar to for the class instance, the bytecode interpreter/virtual machine could be changed to call the module’s __setattr__() method with the expected behaviour.
However, without dots:
>>> dis.dis('from lib import CONSTANT; CONSTANT = 42')
disassembles into:
0 0 RESUME 0
1 2 LOAD_CONST 0 (0)
4 LOAD_CONST 1 (('CONSTANT',))
6 IMPORT_NAME 0 (lib)
8 IMPORT_FROM 1 (CONSTANT)
10 STORE_NAME 1 (CONSTANT)
12 POP_TOP
14 LOAD_CONST 2 (42)
16 STORE_NAME 1 (CONSTANT)
18 LOAD_CONST 3 (None)
20 RETURN_VALUE
I fail to see how STORE_NAME 1 (CONSTANT) could possibly “know” it must call __setattr__() (while it must not do so for STORE_NAME 2 (other_global)).
Very possibly, this is just my limited understanding. But I’d like to understand, and therefore indeed suggest documenting it in the extension PEP.