Functional Programming Utilities
Django utilizes a set of functional programming utilities to implement lazy evaluation and result caching. These patterns are fundamental to the framework's performance, allowing it to defer expensive operations—such as database lookups or complex string parsing—until the results are strictly necessary.
Instance-Level Caching
The cached_property decorator in django.utils.functional is used to convert a method into a property that computes its value once and then caches it for the lifetime of the instance.
Mechanism and Shadowing
Unlike a standard @property, which executes its function on every access, cached_property leverages the Python descriptor protocol to "shadow" itself. On the first access, the __get__ method is called; it computes the value and stores it directly in the instance's __dict__ using the same name as the property.
# django/utils/functional.py
def __get__(self, instance, cls=None):
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
Because Python's attribute lookup priority checks the instance __dict__ before data descriptors, subsequent accesses find the raw value in the dictionary and bypass the cached_property logic entirely.
Usage in Request Processing
A primary example of this is found in HttpRequest within django/http/request.py. Parsing HTTP headers is an expensive operation that shouldn't happen unless a view actually needs them.
class HttpRequest:
@cached_property
def headers(self):
return HttpHeaders(self.META)
By using cached_property, the headers object is only created if request.headers is accessed. Once created, it is reused for the remainder of the request cycle without re-parsing self.META.
Class-Level Computed Attributes
The classproperty utility allows for properties that are accessible directly from a class rather than an instance. This is useful for metadata or configuration that requires logic but should appear as a simple attribute.
# django/utils/functional.py
class classproperty:
def __init__(self, method=None):
self.fget = method
def __get__(self, instance, cls=None):
return self.fget(cls)
Lazy Proxy Objects
Django provides LazyObject and SimpleLazyObject to create "ghost" objects that act as proxies for another object that has not yet been initialized.
LazyObject for System Components
LazyObject is a base class used for major system components that must be defined at the module level but cannot be initialized until runtime configuration is available. The most prominent example is the settings object in django/conf/__init__.py.
class LazySettings(LazyObject):
def _setup(self, name=None):
settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
# ... logic to find settings ...
self._wrapped = Settings(settings_module)
settings = LazySettings()
Subclasses must implement _setup(), which populates the internal _wrapped attribute. Every attribute access or magic method call on the LazyObject triggers _setup() if it hasn't run, and then proxies the call to the _wrapped object.
SimpleLazyObject for Callables
SimpleLazyObject is a specialized version of LazyObject designed to wrap a simple callable. It is used extensively in middleware to delay database queries. For example, in AuthenticationMiddleware, the request.user attribute is populated lazily:
# django/contrib/auth/middleware.py
request.user = SimpleLazyObject(lambda: get_user(request))
This ensures that the database is never queried for the user session if the view does not actually access request.user.
Implementation Details and Constraints
Proxying Magic Methods
To ensure the proxy is transparent, LazyObject uses a new_method_proxy factory to wrap almost all Python magic methods (e.g., __str__, __getitem__, __len__, __eq__). This allows a SimpleLazyObject to be used in comparisons or iterations as if it were the underlying object.
# django/utils/functional.py
__getitem__ = new_method_proxy(operator.getitem)
__iter__ = new_method_proxy(iter)
__len__ = new_method_proxy(len)
Pickling and Copying
Lazy objects introduce complexity when pickling or copying.
- Pickling: To avoid pickling an uninitialized proxy,
LazyObjectimplements__reduce__, which forces evaluation and pickles the wrapped object instead. - Copying:
SimpleLazyObjecthandles__copy__and__deepcopy__by checking if the object is initialized. If it is not, it copies the setup function; if it is, it returns a copy of the wrapped object.
Attribute Naming and Recursion
The cached_property implementation uses __set_name__ (introduced in Python 3.6) to automatically discover the name of the attribute it is assigned to. This prevents the need to pass the name manually to the constructor but imposes a constraint: a single cached_property instance cannot be assigned to two different names within the same class, as this would cause a TypeError.
Additionally, LazyObject includes safeguards in __getattribute__ and __repr__ to avoid infinite recursion if the setup function itself triggers an attribute access on the lazy object.