Demystifying
Descriptors
AkulMehra
About Me
AGENDA
- What is a Descriptor ?
What is a Class ?
A class is simply a logical grouping of data and functions.
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = list(marks)
def percentage(self):
return sum(self.marks) / len(self.marks)
student = Student('Sam', [95, 86, 90, 78, 83])
print(student.percentage())
# 86.4
Let's consider another example:
A class used to organize the data about
bulk items
class Item:
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
def total(self):
return self.weight * self.price
biscuits = Item('biscuits', 10, 4.5)
print(biscuits.total())
# 45
But, what if we assign a negative value to weight or price ?
Our total would have been negative.
biscuits = Item('biscuits', 10, 4.5)
print(biscuits.total())
# 45.0
biscuits.weight = -5
print(biscuits.total())
# -22.5
But that is just a toy example, no one would enter a negative value, right ?
Early days of Amazon
We found that customers could order a negative quantity of books! And we would credit their credit card with the price and, I assume, wait around for them to ship the books.
— Jeff Bezos
founder and CEO of Amazon.com
Validating the Attributes
class Item:
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
if price < 0:
raise ValueError('Negative value is not allowed')
self.price = price
def total(self):
return self.weight * self.price
biscuits = Item('biscuits', 10, -4.5)
# ValueError: Negative value is not allowed
The first way to solve this is raise exception during initialisation.
biscuits = Item('biscuits', 10, 4.5)
print(biscuits.total())
# 45.0
biscuits.price = -5
print(biscuits.total())
# -50
But, what if we update it with negative value ?
It doesn't raise an exception !!
Decorators to the rescue
class Item:
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if value > 0:
self.__price = value
else:
raise ValueError('Negative value is not allowed')
def total(self):
return self.weight * self.price
biscuits = Item('biscuits', 10, 4.5)
print(biscuits.total())
# 45
biscuits.price = -5
print(biscuits.total())
# ValueError: Negative value is not allowed
Property
Property decorator sets a function as a class property
Properties disguise function calls as attributes
But, properties are not reusable
class Item:
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if value > 0:
self.__price = value
else:
raise ValueError('Negative value is not allowed')
# repetition of property for weight also
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('Negative value is not allowed')
def total(self):
return self.weight * self.price
If we add more attributes like rating, attribute validation is required and this causes repetition of code.
When I see patterns in my programs, I consider it a sign of trouble.
Paul Graham
Revenge of the Nerds
What is Descriptor
Has any combination of the following methods:
- __get__
- __set__
- __delete__
descriptors are reusable properties
A certain type of attribute
class Foo:
x = SomeDescriptor(with_args) # x is the attribute
Descriptor Definition
class Descriptor:
def __init__(self, *args, **kwargs):
# do something
def __get__(self, instance, owner):
# do something
return value
def __set__(self, instance, value):
# do something
return None
def __delete__(self, instance):
# do something
return None
class NonNegative:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner):
return instance.__dict__[self.storage_name]
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('Negative value is not allowed')
class Item:
weight = NonNegative('weight')
price = NonNegative('price')
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
def total(self):
return self.weight * self.price
Two types of descriptors
1. Data Descriptor:
- __get__
- __set__/__delete__
2. Non Data Descriptor:
- __get__
What exactly happens, when I look up an attribute ?
Looking for foo.x
Python Stores all the object attributes in a Dictionary. So look the item in the object dictionary: foo.__dict__['x']
Not exactly
Looking for foo.x
Steps
- Check class dict for data descriptor
- Check instance dict
- Check class dict for non data descriptor
- Not Found
Example
type(foo).__dict__['x']
foo.__dict__['x']
type(foo).__dict__['x']
AttributeError
Effect of combining functions and attribute calls
- Call instance.f(x)
- Check type(instance).__dict__['f'] for data descriptor
- Check instance.__dict__['f'] for object
- Check type(instance).__dict__['f'] for non data descriptor
- Call f_descriptor.__get__(instance, owner)
- Call f(instance, x)
ORM's in Python
Object Relational Mapper
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
birthday = models.DateField()
Example:
import datetime
p = Person(first_name='Akul', birthday=datetime.datetime.now())
p.save()
p.name
# Akul
p.name = 'A really really long name having more than 30 chars...'
p.save()
# ValidationError
And where the magic begins:
Why Custom Descriptors
Why not Custom Descriptors
Confusing class and instance variables
Infinite recursion problem
Other Resources
Thanks
Demystifying Descriptors
By Akul Mehra
Demystifying Descriptors
- 577