Slide 1

Slide 1 text

PYTHON METACLASSES & DESCRIPTORS ATHENS PYTHON USERS MEETUP 1

Slide 2

Slide 2 text

METACLASSES 2 . 1

Slide 3

Slide 3 text

TYPE class MyClass(object): pass MyClass # __main__.MyClass MyClass.__class__ # type type(MyClass()) # __main__.MyClass type(MyClass) # type type(object) # type type(type) # type 2 . 2

Slide 4

Slide 4 text

>>> print(type.__doc__) type(object_or_name, bases, dict) type(object) -> the object's type type(name, bases, dict) -> a new type 2 . 3

Slide 5

Slide 5 text

# class parent class # name classes __dict__ type( 'MyClass', (), {} ) # returns __main__.MyClass 2 . 4

Slide 6

Slide 6 text

Klass = type('Klass', (), {}) class Klass(object): pass 2 . 5

Slide 7

Slide 7 text

Klass2 = type('Klass2', (Klass, ), {}) class Klass2(Klass): pass 2 . 6

Slide 8

Slide 8 text

Klass3 = type('Klass3', (Klass, Klass2), {'a': 42}) class Klass3(Klass, Klass2): a = 42 2 . 7

Slide 9

Slide 9 text

Klass4 = type( 'Klass4', (), {'a': 42, 'method': lambda self, num: self.a + num} ) class Klass4(object): a = 42 def method(self, x): return self.a + x 2 . 8

Slide 10

Slide 10 text

METACLASS API Metaclass.__prepare__(mcls, name, bases) # classmethod Metaclass.__new__(mcls, name, bases, attrs, **kwargs) Metaclass.__init__(cls, name, bases, attrs, **kwargs) Metaclass.__call__(cls, *args, **kwargs) 2 . 9

Slide 11

Slide 11 text

__PREPARE__ returns dict-like object (empty or not) if not dict, must be converted to dict before __new__ returns 2 . 10

Slide 12

Slide 12 text

EXAMPLE: ALLOW ONLY UPPERCASE ATTRIBUTE NAMES class OnlyUppercase(dict): def __setitem__(self, key, value): if isinstance(key, str) and key.isupper(): super().__setitem__(key, value) class MyMeta(type): @classmethod def __prepare__(mcls, name, bases): return OnlyUppercase() def __new__(mcls, name, bases, attrs): return super().__new__(mcls, name, bases, dict(attrs)) class MyClass(metaclass=MyMeta): lowercase = 1 UPPERCASE = 2 MyClass.__dict__ # {'UPPERCASE': 2} 2 . 11

Slide 13

Slide 13 text

__NEW__ class constructor (class MyClass ... -> __new__ runs) most useful 2 . 12

Slide 14

Slide 14 text

__INIT__ class initializer (after __new__) not generally useful 2 . 13

Slide 15

Slide 15 text

__CALL__ object instantiation (before Class.__new__ & object.__init__) class MyMeta(type): def __call__(cls, *args, **kwargs): print('In metaclass', args, kwargs) return super().__call__(*args, **kwargs) class MyClass(metaclass=MyMeta): def __init__(cls, *args, **kwargs): print('In class', args, kwargs) >>> obj = MyClass(1, 2, foo=42) In metaclass (1, 2), {'foo': 42} In class (1, 2) {'foo': 42} 2 . 14

Slide 16

Slide 16 text

EXAMPLE: CALL ORDER class MyMeta(type): @classmethod def __prepare__(mcls, name, bases): print('Meta __prepare__') return super().__prepare__(mcls, name, bases) def __new__(mcls, name, bases, attrs): print('Meta __new__') return super().__new__(mcls, name, bases, attrs) def __init__(cls, name, bases, attrs): print('Meta __init__') return super().__init__(name, bases, attrs) def __call__(cls, *args, **kwargs): print('Meta __call__') return super().__call__(*args, **kwargs) 2 . 15

Slide 17

Slide 17 text

>>> class MyClass(metaclass=MyMeta): ... def __new__(cls, *args, **kwargs): ... print('Class __new__') ... return super().__new__(cls) ... def __init__(self, *args, **kwargs): ... print('Class __init__') ... Meta __prepare__ Meta __new__ Meta __init__ >>> >>> obj = MyClass() Meta __call__ Class __new__ Class __init__ 2 . 16

Slide 18

Slide 18 text

EXAMPLE: SINGLETON class SingletonMeta(type): def __call__(cls, *args, **kwargs): if not hasattr(cls, '_inst'): obj = super(SingletonMeta, cls).__call__(*args, **kwargs) cls._inst = obj return cls._inst class MyClass(metaclass=SingletonMeta): pass >>> a = MyClass() >>> b = MyClass() >>> a is b True 2 . 17

Slide 19

Slide 19 text

EXAMPLE: METACLASS IS A CALLABLE >>> class MyClass(metaclass=print): ... a = 1 ... MyClass () {'__qualname__': 'MyClass', '__module__': '__main__', 'a': 1} >>> MyClass is None True 2 . 18

Slide 20

Slide 20 text

ATTRIBUTE LOOKUP 3 . 1

Slide 21

Slide 21 text

OBJECT-LEVEL (INSTANCE.ATTR) attr in Class.__dict__ and attr is data descriptor -> Class.__dict__['attr'].__get__(instance, Class) attr in instance.__dict__ -> instance.__dict__['attr'] attr in Class.__dict__ and attr is not a data descriptor -> Class.__dict__['attr'].__get__(instance, Class) attr in Class.__dict__ -> Class.__dict__['attr'] Class.__getattr__ exists -> Class.__getattr__('attr') 3 . 2

Slide 22

Slide 22 text

CLASS-LEVEL (CLASS.ATTR) attr in Metaclass.__dict__ and attr is data desciptor -> Metaclass.__dict__['attr'].__get__(Class, Metaclass) attr in Class.__dict__ and attr is descriptor -> Class.__dict__['attr'].__get__(None, Class) attr in Class.__dict__ -> Class.__dict__['attr'] 3 . 3

Slide 23

Slide 23 text

CLASS-LEVEL (CONT.) attr in Metaclass.__dict__ and attr is not a data descriptor -> Metaclass.__dict__['attr'].__get__(Class, Metaclass) attr in Metaclass.__dict__ -> Metaclass.__dict__['attr'] Metaclass.__getattr__ exists -> Metaclass.__getattr__('attr') 3 . 4

Slide 24

Slide 24 text

DESCRIPTORS only de ned in class-level (not in __init__ etc.) objects with __get__, __set__ & __delete__ methods __get__ & __set__ = data descriptors only __get__ = non-data descriptors e.g. property decorator (getter & setter) 4 . 1

Slide 25

Slide 25 text

DESCRIPTOR API descr.__get__(self, obj, cls) # -> value descr.__set__(self, obj, value) # -> None descr.__delete__(self, obj) # -> None 4 . 2

Slide 26

Slide 26 text

EXAMPLE class Descriptor(object): def __init__(self, initval=None, name='var'): self.val = initval self.name = name def __get__(self, obj, cls): print('get', self.name) return self.val def __set__(self, obj, val): print('set', self.name) self.val = val class MyClass(object): attr = Descriptor(initval=10, name='attr') 4 . 3

Slide 27

Slide 27 text

>>> MyClass.attr get attr 10 >>> MyClass.attr = 11 >>> MyClass.attr 11 >>> # oops 4 . 4

Slide 28

Slide 28 text

>>> a = MyClass() >>> a.attr get attr 10 >>> a.attr = 11 set attr >>> a.attr get attr 11 >>> b = MyClass() >>> b.attr get attr 11 >>> # wat 4 . 5

Slide 29

Slide 29 text

EXAMPLE use descriptors to indirectly set values on instance.__dict__ if called from class, just return the descriptor class 4 . 6

Slide 30

Slide 30 text

class Descriptor(object): def __init__(self, name): self.name = name def __get__(self, obj, cls): if obj is None: return self try: print('get', self.name) return obj.__dict__[self.name] except KeyError: raise AttributeError() def __set__(self, obj, val): print('set', self.name) obj.__dict__[self.name] = val class MyClass(object): attr = Descriptor('attr') 4 . 7

Slide 31

Slide 31 text

>>> MyClass.attr <__main__.Descriptor at 0x312....> >>> >>> a = MyClass() >>> a.attr get attr Traceback (most recent call last) .... AttributeError: ... >>> a.attr = 1 set attr >>> a.attr get attr 1 >>> >>> b = MyClass() >>> b.attr = 2 set attr >>> b.attr get attr 2 >>> a.attr get attr 1 4 . 8

Slide 32

Slide 32 text

METACLASSES & DESCRIPTORS attr = Descriptor('attr') -> attr = Descriptor() 5 . 1

Slide 33

Slide 33 text

EXAMPLE class Descriptor(object): def __init__(self): self.name = None def __get__(self, obj, cls): if obj is None: return self try: print('get', self.name) return obj.__dict__[self.name] except KeyError: raise AttributeError() def __set__(self, obj, val): print('set', self.name) obj.__dict__[self.name] = val 5 . 2

Slide 34

Slide 34 text

class MyMeta(type): def __new__(mcls, name, bases, attrs): for k, v in attrs.items(): if isinstance(v, Descriptor): v.name = k return super().__new__(mcls, name, bases, attrs) class MyClass(metaclass=MyMeta): attr = Descriptor() 5 . 3

Slide 35

Slide 35 text

>>> MyClass.attr <__main__.Descriptor at 0x312....> >>> >>> a = MyClass() >>> a.attr = 1 set attr >>> a.attr get attr 1 >>> a.__dict__ {'attr': 1} 5 . 4

Slide 36

Slide 36 text

EXAMPLE: USING ANNOTATIONS FOR TYPING (PYTHON 3.6) >>> class MyClass: ... a: int ... b: str ... >>> MyClass.__annotations__ {'a': int, 'b': str} 5 . 5

Slide 37

Slide 37 text

>>> class MyClass(Typed): ... a: int ... b: str ... >>> obj = MyClass() >>> obj.a = 1 >>> obj.a = 'foo' ... ... TypeError: 'foo' is not of type 'int' 5 . 6

Slide 38

Slide 38 text

class TypedDescriptor: def __init__(self, name, tp): self.name = name self.tp = tp def __get__(self, obj, cls): if obj is None: return self try: return obj.__dict__[self.name] except KeyError: raise AttributeError() def __set__(self, obj, val): if not isinstance(val, self.tp): raise TypeError() obj.__dict__[self.name] = val 5 . 7

Slide 39

Slide 39 text

class TypedMeta(type): def __new__(mcls, name, bases, attrs): ann = attrs.get('__annotations__', {}) for k, v in ann.items(): attrs[k] = TypedDescriptor(name=k, tp=v) return super().__new__(mcls, name, bases, attrs) class Typed(metaclass=TypedMeta): pass 5 . 8

Slide 40

Slide 40 text

LINKS Understanding Python metaclasses Descriptor HowTo Guide Dave Beazley: Python 3 Metaprogramming PEP 3115: Metaclasses in Python 3 Python 3.6 NamedTuple class (source code) Presentation git repo 6 . 1