From parsing a class to code object to class to mappingproxy to object (oh my!)

adam.preble at gmail.com adam.preble at gmail.com
Sun Mar 31 11:11:27 EDT 2019


I have been mimicking basic Python object constructs successfully until I started trying to handle methods as well in my hand-written interpreter. At that point, I wasn't sure where to stage all the methods before they get shuffled over to an actual instance of an object. I'm having to slap this out here partially to rubber duck it and partially because I really don't know what's going on.

Here's something tangible we can use:
>>> def construct():
...   class Meow:
...     def __init__(self):
...       self.a = 1
...     def change_a(self, new_a):
...       self.a = new_a
...   return Meow()
...
>>> dis(construct)
  2           0 LOAD_BUILD_CLASS
              2 LOAD_CONST               1 (<code object Meow at 0x0000021BD59170C0, file "<stdin>", line 2>)
              4 LOAD_CONST               2 ('Meow')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               2 ('Meow')
             10 CALL_FUNCTION            2
             12 STORE_FAST               0 (Meow)

  7          14 LOAD_FAST                0 (Meow)
             16 CALL_FUNCTION            0
             18 RETURN_VALUE

I've wrapped my class in a function so I could more readily poke it with a stick. I understand LOAD_BUILD_CLASS will invoke builtins.__build_class__(). By the way, why the special opcode for that? Anyways, it takes that code object, which looks to be particularly special.

Note that I'm inserting some newlines and extra junk for sanity:

>>> import ctypes
>>> c = ctypes.cast(0x0000021BD59170C0, ctypes.py_object).value
>>> c.co_consts
('construct.<locals>.Meow',
<code object __init__ at 0x0000021BD5908C90, file "<stdin>", line 3>,
'construct.<locals>.Meow.__init__',
<code object change_a at 0x0000021BD5917030, file "<stdin>", line 5>,
'construct.<locals>.Meow.change_a',
None)

>>> c.co_names
('__name__', '__module__', '__qualname__', '__init__', 'change_a')

>>> dis(c.co_code)
          0 LOAD_NAME                0 (0) -> __name__ ...
          2 STORE_NAME               1 (1) -> ... goes into __module__
          4 LOAD_CONST               0 (0) -> Name of the class = 'construct.<locals>.Meow' ...
          6 STORE_NAME               2 (2) -> ... goes into __qualname__
          8 LOAD_CONST               1 (1) -> __init__ code object
         10 LOAD_CONST               2 (2) -> The name "__init__" ...
         12 MAKE_FUNCTION            0     -> ... Made into a function
         14 STORE_NAME               3 (3) -> Stash it
         16 LOAD_CONST               3 (3) -> The name "change_a" ...
         18 LOAD_CONST               4 (4) -> The __change_a__ code object ...
         20 MAKE_FUNCTION            0     -> ... Made into a function
         22 STORE_NAME               4 (4) -> Stash it
         24 LOAD_CONST               5 (5) -> Returns None
         26 RETURN_VALUE

I'm not too surprised to see stuff like this since this kind of thing is what I expect to find in flexible languages that do object-oriented programming by basically blessing a variable and heaping stuff on it. It's just that I'm trying to figure out where this goes in the process of language parsing to class declaration to object construction.

What I'm assuming is that when builtins.__build_class__() is invoked, it does all the metaclass/subclass chasing first, and then ultimately invokes this code object to populate the class. If I look at the __dict__ for the class afterwards, I see:

mappingproxy(
{'__module__': '__main__',
'__init__': <function construct.<locals>.Meow.__init__ at 0x0000021BD592AAE8>,
'change_a': <function construct.<locals>.Meow.change_a at 0x0000021BD5915F28>,
'__dict__': <attribute '__dict__' of 'Meow' objects>,
'__weakref__': <attribute '__weakref__' of 'Meow' objects>,
'__doc__': None})

What is the plumbing taking the result of that code object over to this proxy? I'm assuming __build_class__ runs that code object and then starts looking for new names and see to create this. Is this mapping proxy the important thing for carrying the method declaration?

Is this also where prepare (and __prepare__) comes into play? Running into that was where I felt the need to start asking questions because I got six layers deep and my brain melted.

I'm then assuming that in object construction, this proxy is carried by some reference to the final object in such a way that if the class's fields are modified, all instances would see the modification unless the local object itself was overridden.



More information about the Python-list mailing list