Definition A class that defines any of __get__()
, __set__()
, or __delete__()
is called a descriptor. If it only implements __get__()
it is a non-data descriptor; if it implements both __get__()
and __set__()
it is a data descriptor.
Descriptors intercept attribute access on other classes.
Why use them? Start with @property @property
is the go-to decorator when you need computed attributes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Egg (object ): def __init__ (self ): self .price = 10 @property def usd_price (self ): return self .price / 6.8 @usd_price.setter def usd_price (self, value ): self .price = value * 6.8 @property def hkd_price (self ): return self .price / 0.87 @hkd_price.setter def hkd_price (self, value ): self .price = value * 0.87 egg = Egg() print ("Current price: %s RMB, %s USD, %s HKD" % (egg.price, egg.usd_price, egg.hkd_price))egg.usd_price = 2 print ("Current price: %s RMB, %s USD, %s HKD" % (egg.price, egg.usd_price, egg.hkd_price))egg.hkd_price = 15 print ("Current price: %s RMB, %s USD, %s HKD" % (egg.price, egg.usd_price, egg.hkd_price))
Output:
1 2 3 Current price: 10 RMB, 1.4705882352941178 USD, 11.494252873563218 HKD Current price: 13.6 RMB, 2.0 USD, 15.632183908045977 HKD Current price: 13.05 RMB, 1.9191176470588236 USD, 15.000000000000002 HKD
The USD and HKD properties share almost identical logic. Adding more currencies would repeat the same pattern over and over.
Refactor with descriptors Enter descriptors. First, define a reusable descriptor:
1 2 3 4 5 6 7 8 9 class Price (object ): def __init__ (self, rate ): self .rate = rate def __get__ (self, instance, instype ): return instance.price / self .rate def __set__ (self, instance, value ): instance.price = value * self .rate
Then apply it to the Egg
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Egg (object ): def __init__ (self ): self .price = 10 usd_price = Price(6.8 ) hkd_price = Price(0.87 ) egg = Egg() print ("Current price: %s RMB, %s USD, %s HKD" % (egg.price, egg.usd_price, egg.hkd_price))egg.usd_price = 2 print ("Current price: %s RMB, %s USD, %s HKD" % (egg.price, egg.usd_price, egg.hkd_price))egg.hkd_price = 15 print ("Current price: %s RMB, %s USD, %s HKD" % (egg.price, egg.usd_price, egg.hkd_price))
Instantiating the descriptor captures the logic in a single place and keeps the class definition clean. You can think of it as factoring the three @property
methods into a reusable class.
Python features built on descriptors @property @property
itself is implemented with a descriptor. A simplified pure-Python equivalent looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Property (object ): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__ (self, fget=None , fset=None , fdel=None , doc=None ): self .fget = fget self .fset = fset self .fdel = fdel if doc is None and fget is not None : doc = fget.__doc__ self .__doc__ = doc def __get__ (self, obj, objtype=None ): if obj is None : return self if self .fget is None : raise AttributeError("unreadable attribute" ) return self .fget(obj) def __set__ (self, obj, value ): if self .fset is None : raise AttributeError("can't set attribute" ) self .fset(obj, value) def __delete__ (self, obj ): if self .fdel is None : raise AttributeError("can't delete attribute" ) self .fdel(obj) def getter (self, fget ): return type (self )(fget, self .fset, self .fdel, self .__doc__) def setter (self, fset ): return type (self )(self .fget, fset, self .fdel, self .__doc__) def deleter (self, fdel ): return type (self )(self .fget, self .fset, fdel, self .__doc__)
Applying it:
1 2 3 4 5 6 7 8 9 10 11 class Egg (object ): def __init__ (self ): self .price = 10 @property def usd_price (self ): return self .price / 6.8 @usd_price.setter def usd_price (self, value ): self .price = value * 6.8
__getattribute__
, __get__
, and __getattr__
__getattribute__
runs whenever you access an attribute on a new-style class. It controls the lookup order.
__get__
is the descriptor protocol shown above.
__getattr__
fires only if the attribute was not found elsewhere.
By default the lookup order is: instance dictionary → data descriptors → class dictionary → non-data descriptors → __getattr__
.