Kaliklipper
Kaliklipper

Reputation: 363

Decorator "object is not callable"

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

Answers (2)

jsbueno
jsbueno

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

olinox14
olinox14

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

Related Questions