Reputation: 363
I'm trying to get to grips with decorators in Python and trying to implement a version of the CachedProperty
decorator from the botocore library, but keep hitting an error:
TypeError: 'CachedProperty' object is not callable.
I've been Googling this for a while today but the examples I have found don't seem to be a direct equivalent of my issue. They mostly relate to people trying to call objects like int and failing.
When I step through the code the decorator calls __init__
in CachedProperty
ok when I import sum_args()
, but throws an error when I call the function itself from the unit test.
My unit test:
import unittest
from decorators.caching_example import sum_args
class TestCachedProperty(unittest.TestCase):
def test_sum_integers(self):
data = [1, 2, 3]
result = sum_args(data)
self.assertEqual(result, 6)
The function I'm trying to decorate:
from decorators.caching_property import CachedProperty
@CachedProperty
def sum_args(arg):
total = 0
for val in arg:
total += val
return total
The CachedProperty
class I've lifted from botocore:
class CachedProperty(object):
"""A read only property that caches the initially computed value.
This descriptor will only call the provided ``fget`` function once.
Subsequent access to this property will return the cached value.
"""
def __init__(self, fget):
self._fget = fget
def __get__(self, obj, cls):
if obj is None:
return self
else:
computed_value = self._fget(obj)
obj.__dict__[self._fget.__name__] = computed_value
return computed_value
Looking at the program I originally swiped this from, I was expecting it to pass the sum function to the CachedProperty
class – creating an instance of it as it goes – and the instance to store the result in its internal instance variable self._fget
.
What I'm actually getting is:
Error
Traceback (most recent call last):
File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 615, in run
testMethod()
File "/Users/bradley.atkins/PycharmProjects/brad/examples/tests/decorators/test_property_cache.py", line 11, in test_sum_integers
result = sum_args(data)
TypeError: 'CachedProperty' object is not callable
Upvotes: 3
Views: 6595
Reputation: 110271
CachedProperty
, as described by its name, is meant to be used as a decorator on methods in a class body (not standalone functions), that then will behave like ordinary Python "properties", but that are "cached". :-)
In your example code, you are trying to apply it to a module-level function, and that won't work - because this decorator transforms the function in an object that depends on the attribute access mechanisms that only work for class members (that is: the "descriptor protocol", which works for objects that implement one on __get__
, __set__
or __del__
methods).
The idea of a decorator in Python is quite simple, actually - it does not have special cases. The apparent "special case" in your code is due to the nature of the returned object.
So, in short - a decorator is just a callable that takes one sole parameter, which is another callable - usually a function or a class, and returns another object (not necessarily callable), that will replace the first one.
So given a simple decorator such as:
def logcalls(func):
def wrapper(*args, **kw):
print(f"{func} called with {args} and {kw}")
return func(*args, **kw)
return wrapper
it can be used as:
@logcalls
def add(a, b):
return a + b
and that is equivalent to:
def add(a, b):
return a + b
add = logcalls(a + b)
That simple!
Complexity may arise when you want to pass extra parameters for a decorator, then you need to have a "stage" that will accept those configuring parameters, and return a callable that will take the decorated object as its single parameter. In some code bases that leads to a decorator being comprised of 3 levels of nested functions, which may be though to wrap ones mind around.
If the CachedProperty
above would implement a __call__
method, besides __get__
, it would work for module classes as well (provided it had a suitable place to record the class values - descriptors get the instance they are attached to for free). Also, it is worth noting that for caching calls to ordinary functions, Python's standard library does have a decorator in the functools module - functools.lru_cache()
Upvotes: 0
Reputation: 6653
Your sum_args
is evaluated as a CachedProperty
, which does not implement any __call__
method, making it non-callable. That's why python throws this error when you try to call it with sum_args(data)
Try to change your code to:
class CachedProperty(object):
def __init__(self, fget):
self._fget = fget
def __call__(self, obj):
if obj is None:
return obj
else:
computed_value = self._fget(obj)
self.__dict__[self._fget.__name__] = computed_value
return computed_value
@CachedProperty
def sum_args(arg):
total = 0
for val in arg:
total += val
return total
data = [1, 2, 3]
result = sum_args(data)
print(result) # >> 6
Upvotes: 2