Chapter 17 · The utility layer
17.3 cached_property, classproperty and classonlymethod
cached_property makes a method an attribute that is worked out at its first read on an instance and kept on that instance; classproperty makes a method of the class an attribute that is worked out at every read; classonlymethod is a classmethod that an instance cannot reach.
Many of Django’s objects answer questions that cost something and that not every use asks: a request’s headers, gathered from META; whether a database supports transactions, found by trying. Answering in __init__ makes every object pay for every answer, and a method pays at each call. A cached property pays once, at the first read. HttpRequest is one (Headers, host and scheme), as are the flags of BaseDatabaseFeatures that ask the database (What a database can do: BaseDatabaseFeatures), BaseConnectionHandler, which reads DATABASES, CACHES or TASKS at the handler’s first use (Connections by alias: DATABASES and ConnectionHandler), and the lists a model’s options derive. The utility layer sets it beside the other ways Django’s code waits.
The descriptor and its name
cached_property keeps the function as cached_property and copies its docstring, and nothing more: the descriptor does not yet know the name under which it will store a value. Until it does, cached_property is the class’s None, and cached_property, which cached_property calls, is a placeholder, a static method that raises TypeError.
The name comes from Python, which calls __set_name__ on each attribute that defines it as a class statement makes the class. cached_property records the name the first time and puts the real function in func. Called again with the same name, as when a subclass assigns the inherited descriptor to the same attribute, it does nothing; with another name it raises TypeError. The name is the key under which every instance’s value is stored, and it is the attribute’s, not the function’s: fleet = cached_property(get_ships) stores under "fleet". A class of the harbour office’s that gives one descriptor two names fails as its class statement runs:
# harbour/office/board.py
class Office:
@cached_property
def ships(self):
return list(Ship.objects.order_by("name"))
fleet = ships
TypeError: Cannot assign the same cached_property to two different names ('ships' and 'fleet').
A cached_property assigned to a class after its class statement is never named, and its first read on an instance raises the placeholder’s TypeError.
Why the second read never reaches __get__
cached_property, read on the class, returns the descriptor itself. Read on an instance, it calls func with the instance, writes the result into the instance’s __dict__ under the name, and returns it.
Later reads are kept away by the order of Python’s lookup of an instance’s attribute. A data descriptor on the class, one that defines __set__ or __delete__, answers before the instance’s __dict__ is looked in; any other descriptor answers only if the name is not in that __dict__. cached_property defines neither method, so the value stored at the first read hides it. An assignment does not reach the descriptor either: with no __set__ to take it, the value goes straight into the instance’s __dict__, and a value put there before the first read is returned without a call. A run with the example’s Harbour, harbour and other being two instances, and the number of SQL statements each line sent:
Harbour.ships.name 0 queries 'ships'
harbour.ships 1 query [<Ship: Beagle>, <Ship: Endeavour>]
harbour.ships 0 queries [<Ship: Beagle>, <Ship: Endeavour>]
del harbour.ships 0 queries
del harbour.ships 0 queries AttributeError: 'Harbour' object has no attribute 'ships'
other.ships = [] 0 queries
other.ships 0 queries []
Forgetting a value, and copying one
Deleting the attribute of a cached_property from an instance removes the value from the instance’s __dict__, and the next read calls the function again. With no __delete__ on the descriptor, del reaches only the dictionary, so deleting a value never computed raises AttributeError. Options deletes a name a model’s options cache only where it finds it in the dictionary (The options: what a model keeps as _meta).
The value is an ordinary entry of the dictionary, so a copy made from the dictionary carries it: in a run, copy.copy of a Harbour and a pickle round trip both kept the list. Query.clone copies the query’s __dict__ and then pops Query.base_table with a default, and the clone works it out again (The query: what a Query holds, and how it is copied).
No lock, and no __dict__
cached_property calls the function and stores its result with no lock. Two threads that read the attribute of one instance before either has stored a value both call the function: in a run whose function waited a fifth of a second, it was called twice, each thread got a list of its own, and the instance kept one of them.
A class whose instances have no __dict__, as with __slots__, cannot use it. The function is called before the store is tried, so it runs, and then the store fails, at every read. Mooring, a class of the example with __slots__ = ("name",) and the same ships:
mooring.ships 1 query AttributeError: 'Mooring' object has no attribute '__dict__'
classproperty
classproperty calls the function with the class it was read through, the class itself or an instance’s class, and keeps nothing, so the function runs at every read. classproperty replaces the function and returns the descriptor. There is no setter and no __set__, so an assignment on an instance stores a value in its __dict__ that hides the descriptor for that instance alone. The example’s Harbour, given berths and a second attribute:
# harbour/office/board.py
class Harbour:
berths = 12
@classproperty
def free_berths(cls):
return cls.berths - Ship.objects.count()
class BigHarbour(Harbour):
berths = 20
Harbour.free_berths 1 query 10
Harbour.free_berths 1 query 10
harbour.free_berths 1 query 10
BigHarbour.free_berths 1 query 18
harbour.free_berths = 0 0 queries
harbour.free_berths 0 queries 0
Harbour.free_berths 1 query 10
Where Django wants the answer kept, the function keeps it. MigrationRecorder defines its model class at the first read and keeps it in MigrationRecorder (What has been applied: MigrationRecorder and the table "django_migrations"). View is worked out at each read (Asynchronous views).
classonlymethod
classonlymethod is a classmethod whose __get__ raises AttributeError when it is read through an instance, “This method is available only on the class, not on instances.”, and otherwise binds to the class. Django’s one use of it is View.as_view (View.as_view and dispatch).