Slide 1

Slide 1 text

Discovering Descriptors Mariano Anaya Prague - PyCon CZ - June 2017 rmariano rmarianoa

Slide 2

Slide 2 text

def “Learning about descriptors not only provides access to a larger toolset, it creates a deeper understanding of how Python works and an appreciation for the elegance of its design”. - Raymond Hettinger

Slide 3

Slide 3 text

Introduction In general: >>> obj = DomainModel() >>> obj.x = 'value' >>> obj.x 'value'

Slide 4

Slide 4 text

Control Access to Data But what if… When doing “obj.x” we could run arbitrary code?

Slide 5

Slide 5 text

Control Access to Data But what if… When doing “obj.x” we could run arbitrary code? By another object.

Slide 6

Slide 6 text

Control Access to Data But what if… When doing “obj.x” we could run arbitrary code? By another object (of a different class).

Slide 7

Slide 7 text

A First Look at Descriptors

Slide 8

Slide 8 text

Introduction Descriptors enable control over core operations (get, set, delete), of an attribute in an object.

Slide 9

Slide 9 text

Descriptor Methods __get__(self, instance, owner) __set__(self, instance, value) __delete__(self, instance) __set_name__(self, owner, name) * * Python 3.6

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Types of Descriptors ● Non-data descriptors (a.k.a “non-overriding”) ○ Don’t implement __set__ ○ Instance attributes take precedence ● Data descriptors (a.k.a. “overriding”) ○ Implement __get__, __set__ ○ Override instance’s __dict__

Slide 12

Slide 12 text

__get__ Problem: automatically format date values of other attributes. Two classes: Descriptor + Managed class

Slide 13

Slide 13 text

Descriptor

Slide 14

Slide 14 text

__get__: Default Value class DateFormatter: FORMAT = "%Y-%m-%d %H:%M" def __init__(self, name=None): self.name = name def __get__(self, instance, owner): if instance is None: return self date_value = getattr(instance, self.name) if date_value is None: return '' return date_value.strftime(self.FORMAT)

Slide 15

Slide 15 text

Managed Class

Slide 16

Slide 16 text

__get__: Managed Class class FileStat: """Stats of a file in a virtual file system""" str_created_at = DateFormatter('created_at') str_updated_at = DateFormatter('updated_at') str_removed_at = DateFormatter() def __init__(self, fname, created, updated=None, removed=None): self.filename = fname self.created_at = created self.updated_at = updated self.removed_at = removed

Slide 17

Slide 17 text

>>> created = updated = datetime(2017, 6, 9, 11, 15, 19) >>> f1 = FileStat('/home/mariano/file1', created, updated) >>> f1.str_created_at '2017-06-09 11:15' >>> f1.str_updated_at '2017-06-09 11:15' >>> f1.str_removed_at ''

Slide 18

Slide 18 text

Resolution Order

Slide 19

Slide 19 text

>>> f1 = FileStat(...) >>> f1.str_created_at Statement f1.__dict__ { 'created_at': ... 'filename': '/home/...', 'removed_at': ..., 'updated_at': ... }

Slide 20

Slide 20 text

>>> f1 = FileStat(...) >>> f1.str_created_at Statement FileStat.__dict__ mappingproxy({'__dict__': ..., '__doc__': "...", '__init__': ..., 'str_created_at': , 'str_removed_at': , 'str_updated_at': })

Slide 21

Slide 21 text

>>> f1 = FileStat(...) >>> f1.str_created_at Statement >>> hasattr(FileStat.__dict__['str_created_at'], '__get__') True

Slide 22

Slide 22 text

__get__: Syntax Sugar >>> f1 = FileStat(...) >>> f1.str_created_at Translates into: FileStat.str_created_at.__get__(f1, FileStat)

Slide 23

Slide 23 text

__get__(self, instance, owner) When called like . instance is None >>> FileStat.str_created_at <__main__.DateFormatter object at 0x...> Access Through the Class

Slide 24

Slide 24 text

Name of the Descriptor

Slide 25

Slide 25 text

class FileStat: """Stats of a file in a virtual file system""" str_created_at = DateFormatter('created_at') str_updated_at = DateFormatter('updated_at') str_removed_at = DateFormatter()

Slide 26

Slide 26 text

Before __set_name__ Some techniques to have an “automatic configuration”: Class decorator or metaclass

Slide 27

Slide 27 text

__set_name__(self, owner, name) Called automatically with the name of the attribute, on the LHS. class owner: name = Descriptor()

Slide 28

Slide 28 text

__set_name__ class DateFormatter: def __init__(self, name=None): self.name = name ... def __set_name__(self, owner, name): if self.name is None: _, _, self.name = name.partition('_')

Slide 29

Slide 29 text

__set__ Problem: Given an attribute of an object, keep count of how many times its value was changed.

Slide 30

Slide 30 text

Data Descriptor: __set__ Some strategies: 1. Properties (with setter) 2. Override __setattr__() 3. Descriptors!

Slide 31

Slide 31 text

class TracedProperty: """Count how many times an attribute changed its value""" def __set_name__(self, owner, name): self.name = name self.count_name = f'count_{name}' def __set__(self, instance, value): ...

Slide 32

Slide 32 text

class TracedProperty: ... def __set__(self, instance, value): try: current_value = instance.__dict__[self.name] except KeyError: instance.__dict__[self.count_name] = 0 else: if current_value != value: instance.__dict__[self.count_name] += 1 instance.__dict__[self.name] = value

Slide 33

Slide 33 text

class Traveller: city = TracedProperty() country = TracedProperty() def __init__(self, name): self.name = name

Slide 34

Slide 34 text

>>> tourist = Traveller('John Smith') >>> tourist.city = 'Barcelona' >>> tourist.country = 'Spain' >>> tourist.count_city 0 >>> tourist.count_country 0 >>> tourist.city = 'Stockholm' >>> tourist.country = 'Sweden' >>> tourist.count_city 1 >>> tourist.count_country 1 >>> tourist.city = 'Gothenburg' >>> tourist.count_city 2 >>> tourist.count_country 1 >>> tourist.country = 'Sweden' >>> tourist.count_country 1

Slide 35

Slide 35 text

tourist = Traveller() tourist.city = 'Stockholm' Traveller.city.__set__(tourist, 'Stockholm') __set__: Syntax sugar Translates to:

Slide 36

Slide 36 text

__delete__ Called when deleting an attribute by using the descriptor, like: del .

Slide 37

Slide 37 text

__delete__ class ProtectedAttribute: """Attribute that is protected against deletion""" def __set_name__(self, owner, name): self.name = name def __delete__(self, instance): raise AttributeError(f"Can't delete {self.name} for {instance!s}") def __set__(self, instance, value): ...

Slide 38

Slide 38 text

class ProtectedUser: username = ProtectedAttribute() def __init__(self, username, location): self.username = username self.location = location def __str__(self): return f"{self.__class__.__name__}[{self.username}]"

Slide 39

Slide 39 text

>>> usr = ProtectedUser('jsmith', '127.0.0.1') >>> usr.username 'jsmith' >>> del usr.username Traceback (most recent call last): ... AttributeError: Can't delete username for ProtectedUser[jsmith] >>> usr.location '127.0.0.1' >>> del usr.location >>> usr.location Traceback (most recent call last): ... AttributeError: 'ProtectedUser' object has no attribute 'location'

Slide 40

Slide 40 text

What makes a good descriptor?

Slide 41

Slide 41 text

What makes a good descriptor? The same thing that makes any good Python object: consistency with Python itself (to be Pythonic).

Slide 42

Slide 42 text

Descriptors are deployed in the language infrastructure. ● @property, @classmethod, @staticmethod ● Methods (functions) Descriptors in CPython

Slide 43

Slide 43 text

Functions are Descriptors They have a __get__ method. That’s why they can work as instance methods! .__get__ returns the function bound to an object.

Slide 44

Slide 44 text

class Class: def method(self, *args): return f'{self!s} got {args}' >>> Class.__dict__ mappingproxy({'__dict__': ... 'method': }) >>> isinstance(Class.__dict__['method'], types.FunctionType) True

Slide 45

Slide 45 text

>>> instance = Class() >>> instance.method('arg1', 'arg2') "instance got ('arg1', 'arg2')" Method Call >>> Class.method.__get__(instance, Class)('arg1', 'arg2') "instance got ('arg1', 'arg2')" It’s actually...

Slide 46

Slide 46 text

Extended Uses

Slide 47

Slide 47 text

Improve decorators that change the signature.

Slide 48

Slide 48 text

Apply to Functions & Methods as well Problem: A decorator that changes the signature, has to work both for functions and methods. E.g. abstract away repeated code.

Slide 49

Slide 49 text

def resolver_function(root, args, context, info): helper = DomainObject(root, args, context, info) ... helper.process() helper.task1() helper.task2() return helper.task1()

Slide 50

Slide 50 text

class DomainArgs: def __init__(self, func): self.func = func wraps(func)(self) def __call__(self, root, args, context, info): helper = DomainObject(root, args, context, info) return self.func(helper) @DomainArgs def resolver_function(helper): helper.task1() ...

Slide 51

Slide 51 text

class ViewResolver: @DomainArgs def resolve_method(self, helper): response = helper.process() return f"Method: {response}" Try to Decorate a Method

Slide 52

Slide 52 text

>>> vr1.resolve_method('root', 'args', 'context', 'info') ------------------------------------ TypeError Traceback (most recent call last) 39 def __call__(self, root, args, context, info): 40 helper = DomainObject(root, args, context, info) ---> 41 return self.func(helper) 42 TypeError: resolve_method() missing 1 required positional argument: 'helper' Doesn’t handle self!

Slide 53

Slide 53 text

class DomainArgs: ... def __get__(self, instance, owner): mapped = self.func.__get__(instance, owner) return self.__class__(mapped) >>> vr = ViewResolver() >>> vr.method_resolver('root', 'args', 'context', 'info') 'Method resolver: root, args, context, info' Fix: __get__

Slide 54

Slide 54 text

It’s possible to have object-oriented design with descriptors.

Slide 55

Slide 55 text

Work as generalized properties.

Slide 56

Slide 56 text

Can help on debugging.

Slide 57

Slide 57 text

Closing Remarks

Slide 58

Slide 58 text

Implement the minimum required interface.

Slide 59

Slide 59 text

Use for general-purpose solutions.

Slide 60

Slide 60 text

Thanks! @rmarianoa