Classes and Metaclasses

James Powell

Themes

  • static ignorance of the interpreter
    • the interpreter doesn't know what it's looking at
    • eg., a name followed by () could be a function, class, generator, etc.
  • metaclasses are a tool for enforcing constraints from a base class to a derived class
  • CPython as a reference implementation of Python
    • Guidance for how the language is supposed to work
    • Python is a language without a spec (unlike, say, C++)
    • If something that works in CPython doesn't work in PyPy, Jython, etc., the other interpreters are not necessarily broken
  • CPython code as a source for understanding
    • simple, straight-forward
    • there exists a natural starting point
  • Python as a system language with no boundary between system and app

Order of the talk

  • contours of classes
  • class construction
  • what is a metaclass?
  • interesting things in Python 3

In [5]:
from sys import version_info
print(version_info)


sys.version_info(major=3, minor=3, micro=2, releaselevel='final', serial=0)

Contours of Classes


In [41]:
from math import sqrt

EUCLIDEAN, STREETS = object(), object()

ARENA_TYPE = STREETS

# in python3, you don't need to derive from object, since there are no old-style classes
class Mob:
    """
    Any mobile object in the game.
    """
# not sure why this was breaking...
#    def __new__(cls, *args, **kwargs):
#        # runs before instantiation
#        return object.__new__(cls, *args, **kwargs)
    
    def __init__(self, x, y):
        self._x, self._y = x, y
    
    def __repr__(self):
        return '{0.__name__}({1.x}, {1.y})'.format(type(self), self)
    
    def __str__(self):
        return repr(self)
    
    def move(self):
        pass
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    @x.setter
    def x(self, value):
        if value < 0:
            raise ValueError("can't move outside of the arena")
        self._x = value
        
    @y.setter
    def y(self, value):
        if value < 0:
            raise ValueError("can't move outside of the arena")
        self._y = value

    if ARENA_TYPE == EUCLIDEAN:
        @staticmethod
        def distance(mob1, mob2):
            return sqrt((mob1.x + mob2.x)**2 + (mob1.y + mob2.y)**2)
    else:
        @staticmethod
        def distance(mob1, mob2):
            return abs(mob1.x - mob2.x) + abs(mob1.y - mob2.y)

mob1 = Mob(0, 0)
mob2 = Mob(12, 3)

Mob.distance(mob1, mob2)


Out[41]:
15

Instance methods are created on the fly; this does not apply to class methods or properties.


In [47]:
mob3 = Mob(1, 1)
assert Mob is Mob
assert mob3 is mob3
print(list(map(id, [mob3.x, mob3.x])))
print(list(map(id, [mob3.move, mob3.move])))

Mob.__dict__


[140227895093824, 140227895093824]
[140227747113584, 140227326980896]
Out[47]:
mappingproxy({'move': <function Mob.move at 0x7f89239b4320>, '__dict__': <attribute '__dict__' of 'Mob' objects>, 'x': <property object at 0x7f89239a6998>, '__init__': <function Mob.__init__ at 0x7f89239b4170>, 'y': <property object at 0x7f89239a66d8>, '__doc__': '\n    Any mobile object in the game.\n    ', '__repr__': <function Mob.__repr__ at 0x7f89239b4200>, 'distance': <staticmethod object at 0x7f892399e1d0>, '__weakref__': <attribute '__weakref__' of 'Mob' objects>, '__str__': <function Mob.__str__ at 0x7f89239b4290>, '__module__': '__main__'})

James likes dis


In [48]:
from dis import dis

def create_mob():
    return Mob(1,1)

dis(create_mob)


  4           0 LOAD_GLOBAL              0 (Mob) 
              3 LOAD_CONST               1 (1) 
              6 LOAD_CONST               1 (1) 
              9 CALL_FUNCTION            2 (2 positional, 0 keyword pair) 
             12 RETURN_VALUE         

If you want to find the hooks for function calls in the CPython code, just grep for CALL_FUNCTION now.

Here's where metaclasses come in... (somehow)


In [55]:
from dis import dis

def create_monster():
    class Monster(Mob):
        def __init__(self, hp, *args, **kwargs):
            self.hp = hp
            Mob.__init__(*args, **kwargs)

dis(create_monster)


  4           0 LOAD_BUILD_CLASS     
              1 LOAD_CONST               1 (<code object Monster at 0x7f895109f540, file "<ipython-input-55-b7f99bfca70d>", line 4>) 
              4 LOAD_CONST               2 ('Monster') 
              7 MAKE_FUNCTION            0 
             10 LOAD_CONST               2 ('Monster') 
             13 LOAD_GLOBAL              0 (Mob) 
             16 CALL_FUNCTION            3 (3 positional, 0 keyword pair) 
             19 STORE_FAST               0 (Monster) 
             22 LOAD_CONST               0 (None) 
             25 RETURN_VALUE         

Looking at the CPython code, you'll see that you only use the first metaclass you find in the inheritance chain. Or is it only the metaclass of the first class (as in, the class itself)?

You can create your own object sytem in python...but probably don't.

What would you use metaclasses for?


In [71]:
class Monster(Mob):
        def __init__(self, hp, *args, **kwargs):
            self._hp = hp
            Mob.ty__init__(*args, **kwargs)
            
        @property
        def hp(self):
            return self._hp

class Boss(Monster):
    def __init__(self, prize, *args, **kwargs):
        self.prize = prize
        Monster.__init__(*arg, **kwargs)
    
# you can ensure that classes you inherit from have certain attributes
assert hasattr(Monster, 'hp')
assert issubclass(Monster, Mob)

# metaclasses allow you contrain derived classes at the parent class level
# --> we don't want subclasses of Monster to be able to move outside the arena
class metaclass(type):
    def __init__(self, name, bases, body):
        print(self, name, bases, body)
        # place contraints here...
        if name == 'Derived':
            raise ValueError('no!')
        return type.__init__(self, name, bases, body)

class Base(metaclass=metaclass):
    pass

class Derived(Base):
    pass


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-71-fdda81cbdfa8> in <module>()
     30     pass
     31 
---> 32 class Derived(Base):
     33     pass

<ipython-input-71-fdda81cbdfa8> in __init__(self, name, bases, body)
     24         # place contraints here...
     25         if name == 'Derived':
---> 26             raise ValueError('no!')
     27         return type.__init__(self, name, bases, body)
     28 

ValueError: no!
<class '__main__.Base'> Base () {'__qualname__': 'Base', '__module__': '__main__'}
<class '__main__.Derived'> Derived (<class '__main__.Base'>,) {'__qualname__': 'Derived', '__module__': '__main__'}