$30 off During Our Annual Pro Sale. View Details »
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Discovering Descriptors
Search
Mariano Anaya
June 09, 2017
Programming
0
180
Discovering Descriptors
Presented at PyCon CZ 2017 on June 9th
Mariano Anaya
June 09, 2017
Tweet
Share
More Decks by Mariano Anaya
See All by Mariano Anaya
Demystifying Coroutines and Asynchronous Programming in Python
rmariano
1
170
Demystifying coroutines and asynchronous programming in Pyhon
rmariano
1
360
Exploring Generators & Coroutines
rmariano
1
810
Discovering-Descriptors_ep.pdf
rmariano
1
380
Beyond Coverage
rmariano
0
190
Clean Code in Python
rmariano
2
2.3k
Other Decks in Programming
See All in Programming
UIデザインに役立つ 2025年の最新CSS / The Latest CSS for UI Design 2025
clockmaker
17
6.6k
20251127_ぼっちのための懇親会対策会議
kokamoto01_metaps
2
400
宅宅自以為的浪漫:跟 AI 一起為自己辦的研討會寫一個售票系統
eddie
0
470
CSC305 Lecture 15
javiergs
PRO
0
250
関数の挙動書き換える
takatofukui
4
770
Microservices Platforms: When Team Topologies Meets Microservices Patterns
cer
PRO
1
920
S3 VectorsとStrands Agentsを利用したAgentic RAGシステムの構築
tosuri13
5
270
30分でDoctrineの仕組みと使い方を完全にマスターする / phpconkagawa 2025 Doctrine
ttskch
3
730
AIコーディングエージェント(Gemini)
kondai24
0
150
tsgolintはいかにしてtypescript-goの非公開APIを呼び出しているのか
syumai
5
1.2k
複数人でのCLI/Infrastructure as Codeの暮らしを良くする
shmokmt
5
2.1k
ZOZOにおけるAI活用の現在 ~モバイルアプリ開発でのAI活用状況と事例~
zozotech
PRO
8
4.1k
Featured
See All Featured
The Invisible Side of Design
smashingmag
302
51k
Documentation Writing (for coders)
carmenintech
76
5.2k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
9.8k
[RailsConf 2023] Rails as a piece of cake
palkan
58
6.1k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.3k
Scaling GitHub
holman
464
140k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
1.8k
GraphQLの誤解/rethinking-graphql
sonatard
73
11k
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
Six Lessons from altMBA
skipperchong
29
4.1k
For a Future-Friendly Web
brad_frost
180
10k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.5k
Transcript
Discovering Descriptors Mariano Anaya Prague - PyCon CZ - June
2017 rmariano rmarianoa
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
Introduction In general: >>> obj = DomainModel() >>> obj.x =
'value' >>> obj.x 'value'
Control Access to Data But what if… When doing “obj.x”
we could run arbitrary code?
Control Access to Data But what if… When doing “obj.x”
we could run arbitrary code? By another object.
Control Access to Data But what if… When doing “obj.x”
we could run arbitrary code? By another object (of a different class).
A First Look at Descriptors
Introduction Descriptors enable control over core operations (get, set, delete),
of an attribute in an object.
Descriptor Methods __get__(self, instance, owner) __set__(self, instance, value) __delete__(self, instance)
__set_name__(self, owner, name) * * Python 3.6
None
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__
__get__ Problem: automatically format date values of other attributes. Two
classes: Descriptor + Managed class
Descriptor
__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)
Managed Class
__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
>>> 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 ''
Resolution Order
>>> f1 = FileStat(...) >>> f1.str_created_at Statement f1.__dict__ { 'created_at':
... 'filename': '/home/...', 'removed_at': ..., 'updated_at': ... }
>>> f1 = FileStat(...) >>> f1.str_created_at Statement FileStat.__dict__ mappingproxy({'__dict__': ...,
'__doc__': "...", '__init__': ..., 'str_created_at': <DateFormatter at 0x..>, 'str_removed_at': <DateFormatter at 0x..>, 'str_updated_at': <DateFormatter at 0x..>})
>>> f1 = FileStat(...) >>> f1.str_created_at Statement >>> hasattr(FileStat.__dict__['str_created_at'], '__get__')
True
__get__: Syntax Sugar >>> f1 = FileStat(...) >>> f1.str_created_at Translates
into: FileStat.str_created_at.__get__(f1, FileStat)
__get__(self, instance, owner) When called like <class>.<descriptor> instance is None
>>> FileStat.str_created_at <__main__.DateFormatter object at 0x...> Access Through the Class
Name of the Descriptor
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()
Before __set_name__ Some techniques to have an “automatic configuration”: Class
decorator or metaclass
__set_name__(self, owner, name) Called automatically with the name of the
attribute, on the LHS. class owner: name = Descriptor()
__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('_')
__set__ Problem: Given an attribute of an object, keep count
of how many times its value was changed.
Data Descriptor: __set__ Some strategies: 1. Properties (with setter) 2.
Override __setattr__() 3. Descriptors!
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): ...
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
class Traveller: city = TracedProperty() country = TracedProperty() def __init__(self,
name): self.name = name
>>> 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
tourist = Traveller() tourist.city = 'Stockholm' Traveller.city.__set__(tourist, 'Stockholm') __set__: Syntax
sugar Translates to:
__delete__ Called when deleting an attribute by using the descriptor,
like: del <instance>.<descriptor>
__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): ...
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}]"
>>> 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'
What makes a good descriptor?
What makes a good descriptor? The same thing that makes
any good Python object: consistency with Python itself (to be Pythonic).
Descriptors are deployed in the language infrastructure. • @property, @classmethod,
@staticmethod • Methods (functions) Descriptors in CPython
Functions are Descriptors They have a __get__ method. That’s why
they can work as instance methods! <function>.__get__ returns the function bound to an object.
class Class: def method(self, *args): return f'{self!s} got {args}' >>>
Class.__dict__ mappingproxy({'__dict__': ... 'method': <function Class.method>}) >>> isinstance(Class.__dict__['method'], types.FunctionType) True
>>> 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...
Extended Uses
Improve decorators that change the signature.
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.
def resolver_function(root, args, context, info): helper = DomainObject(root, args, context,
info) ... helper.process() helper.task1() helper.task2() return helper.task1()
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() ...
class ViewResolver: @DomainArgs def resolve_method(self, helper): response = helper.process() return
f"Method: {response}" Try to Decorate a Method
>>> 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!
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__
It’s possible to have object-oriented design with descriptors.
Work as generalized properties.
Can help on debugging.
Closing Remarks
Implement the minimum required interface.
Use for general-purpose solutions.
Thanks! @rmarianoa