Python Descriptors (Survey)
A descriptor is an object that controls access to an attribute on a class.
A descriptor in action
Upper lives on the Person class. Its methods run when name is read or stored.
class Upper:
def __get__(self, obj, objtype=None):
return obj._name.upper()
def __set__(self, obj, value):
obj._name = value
class Person:
name = Upper()
def __init__(self, name):
self.name = name
print(Person("ada").name)Properties are descriptors
The property object lives in the class dictionary. That is why type reports the name property.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
print(type(Circle.__dict__["radius"]).__name__)
print(Circle(3).radius)One check, many fields
Positive guards width and height here. Each field keeps its own storage name.
class Positive:
def __init__(self, storage):
self.storage = storage
def __get__(self, obj, objtype=None):
return getattr(obj, self.storage)
def __set__(self, obj, value):
if value <= 0:
raise ValueError("must be positive")
setattr(obj, self.storage, value)
class Box:
width = Positive("_width")
height = Positive("_height")
def __init__(self, width, height):
self.width = width
self.height = height
box = Box(3, 4)
print(box.width, box.height)Non-data can be shadowed
Greeting defines only __get__. Storing an instance value shadows it.
class Greeting:
def __get__(self, obj, objtype=None):
return "hello"
class Host:
greeting = Greeting()
host = Host()
print(host.greeting)
host.greeting = "hi"
print(host.greeting)Example
class Positive:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError("value must be positive")
setattr(obj, self.storage_name, value)
class Product:
price = Positive("_price")
stock = Positive("_stock")
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
product = Product("Mug", 12, 30)
print(product.name, product.price, product.stock)
product.price = 15
print(product.price)
try:
product.stock = -1
except ValueError as error:
print(f"ValueError: {error}")
print(product.stock)Key points
- Descriptor: an object that controls access to an attribute on a class
- Use __get__ to control what reading an attribute returns
- Use __set__ to control what writing an attribute stores
- Data descriptor: a descriptor with __set__, so it beats instance values
- Non-data descriptor: a descriptor with only __get__, so instance values can shadow it
- Use one descriptor class to reuse the same check on many fields
In this example, Positive is a descriptor class. It stores the name of a private storage slot. __get__ reads that slot from the instance. __set__ rejects zero and negative values. It stores good values in the slot instead. Product places one Positive on price. It places another Positive on stock. Making a Product stores 12 and 30 through __set__. Changing price to 15 passes the same check. Setting stock to -1 raises ValueError. The stored stock stays at 30.
Note
A property is a ready-made data descriptor.
Properties appeared in the Properties lesson.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Python Playground
Exercise
Why use one Positive descriptor instead of two separate properties?