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

  1. Check class dict for data descriptor
  2. Check instance dict
  3. Check class dict for non data descriptor
  4. Not Found

Example

type(foo).__dict__['x']

 

foo.__dict__['x']

type(foo).__dict__['x']

AttributeError

 

Effect of combining functions and attribute calls

  1. Call instance.f(x)
  2. Check type(instance).__dict__['f'] for data descriptor
  3. Check instance.__dict__['f'] for object
  4. Check type(instance).__dict__['f'] for non data descriptor
  5. Call f_descriptor.__get__(instance, owner)
  6. 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