Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Python Metaclasses & Descriptors

Python Metaclasses & Descriptors

Practical Python metaclasses & descriptors, with a django orm-like example. Athens Python Users Meetup @ hackerspacegr. https://github.com/alexpeits/metaclasses-pythonmeetup-hsgr

Avatar for Alex Peitsinis

Alex Peitsinis

June 11, 2017
Tweet

Other Decks in Programming

Transcript

  1. 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
  2. # class parent class # name classes __dict__ type( 'MyClass',

    (), {} ) # returns __main__.MyClass 2 . 4
  3. 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
  4. 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
  5. __PREPARE__ returns dict-like object (empty or not) if not dict,

    must be converted to dict before __new__ returns 2 . 10
  6. 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
  7. __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
  8. 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
  9. >>> 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
  10. 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
  11. EXAMPLE: METACLASS IS A CALLABLE >>> class MyClass(metaclass=print): ... a

    = 1 ... MyClass () {'__qualname__': 'MyClass', '__module__': '__main__', 'a': 1} >>> MyClass is None True 2 . 18
  12. 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
  13. 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
  14. 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
  15. 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
  16. DESCRIPTOR API descr.__get__(self, obj, cls) # -> value descr.__set__(self, obj,

    value) # -> None descr.__delete__(self, obj) # -> None 4 . 2
  17. 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
  18. >>> 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
  19. EXAMPLE use descriptors to indirectly set values on instance.__dict__ if

    called from class, just return the descriptor class 4 . 6
  20. 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
  21. >>> 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
  22. 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
  23. 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
  24. >>> MyClass.attr <__main__.Descriptor at 0x312....> >>> >>> a = MyClass()

    >>> a.attr = 1 set attr >>> a.attr get attr 1 >>> a.__dict__ {'attr': 1} 5 . 4
  25. EXAMPLE: USING ANNOTATIONS FOR TYPING (PYTHON 3.6) >>> class MyClass:

    ... a: int ... b: str ... >>> MyClass.__annotations__ {'a': int, 'b': str} 5 . 5
  26. >>> class MyClass(Typed): ... a: int ... b: str ...

    >>> obj = MyClass() >>> obj.a = 1 >>> obj.a = 'foo' ... ... TypeError: 'foo' is not of type 'int' 5 . 6
  27. 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
  28. 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
  29. 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