Python offers the method decorator @cached_property since version 3.9. The Django implementation seems to be older, but it nevertheless offers a similar functionality. Is there any specific reason for this? Otherwise, and for consistency, I would propose to just use the builtin version from Python itself.
There was a concern about performance and that it might possibly disappear from the standard library. We could take another look now, but the benefit seems slim?
Now after 6 Python releases, this decorator won’t go away.
The benefit might be slim, but it’s confusing to have the same decorator twice, doing (almost?) the same thing.
How about replacing django.utils.functional.cached_property against functools.cached_property and check if all unit tests works as expected? And if so, we can start a discussion with a wider audience.
Django's implementation
class cached_property:
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
A cached property can be made out of an existing method:
(e.g. ``url = cached_property(get_absolute_url)``).
"""
name = None
@staticmethod
def func(instance):
raise TypeError(
"Cannot use cached_property instance without calling __set_name__() on it."
)
def __init__(self, func):
self.real_func = func
self.__doc__ = func.__doc__
def __set_name__(self, owner, name):
if self.name is None:
self.name = name
self.func = self.real_func
elif name != self.name:
raise TypeError(
"Cannot assign the same cached_property to two different names "
"(%r and %r)." % (self.name, name)
)
def __get__(self, instance, cls=None):
"""
Call the function and put the return value in instance.__dict__ so that
subsequent attribute access on the instance returns the cached value
instead of calling cached_property.__get__().
"""
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
Python's implementation
from types import GenericAlias
_NOT_FOUND = object()
class cached_property:
def init(self, func):
self.func = func
self.attrname = None
self.doc = func.doc
self.module = func.module
def __set_name__(self, owner, name):
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
def __get__(self, instance, owner=None):
if instance is None:
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without calling __set_name__ on it."
)
try:
cache = instance.__dict__
except (
AttributeError
): # not all objects have __dict__ (e.g. class defines slots)
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val
__class_getitem__ = classmethod(GenericAlias)
Python’s implementation contains a few more guards around weird class edge cases, like no __dict__ or an unwriteable __dict__. It also provides a typing hook in __class_getitem__. Other than those things, it’s basically the same. It seems Python even copied the error message wording from Django.
I would say let’s migrate, which in practice means deprecating our version while moving all internal usage to Python’s version. Django won here and got one of its functions merged upstream (albeit with some drama around adding then removing locking), so let’s use it ![]()