7

I'm trying to use a cached property decorator that can take arguments.

I looked at this implementation: http://www.daniweb.com/software-development/python/code/217241/a-cached-property-decorator

from functools import update_wrapper 

def cachedProperty (func ,name =None ):
  if name is None :
    name =func .__name__ 
  def _get (self ):
    try :
      return self .__dict__ [name ]
    except KeyError :
      value =func (self )
      self .__dict__ [name ]=value 
      return value 
  update_wrapper (_get ,func )
  def _del (self ):
    self .__dict__ .pop (name ,None )
  return property (_get ,None ,_del )

But the problem I have is that I cannot call the decorator with the @ syntax if I want to use the parameter:

@cachedProperty(name='test') # This does NOT work
def my_func(self):
    return 'ok'

# Only this way works
cachedProperty(my_func, name='test')

How to use the @ syntax with decorators arguments?

Thanks

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Michael
  • 8,357
  • 20
  • 58
  • 86

1 Answers1

9

You need a decorator factory, another wrapper that produces the decorator:

from functools import wraps 

def cachedProperty(name=None):
    def decorator(func):
        if decorator.name is None:
            decorator.name = func.__name__ 
        @wraps(func)
        def _get(self):
            try:
                return self.__dict__[decorator.name]
            except KeyError:
                value = func(self)
            self.__dict__[decorator.name] = value 
            return value 
        def _del(self):
            self.__dict__.pop(decorator.name, None)
        return property(_get, None, _del)
    decorator.name = name
    return decorator

Use this as:

@cachedProperty(name='test')
def my_func(self):
    return 'ok'

A decorator is really just syntactic sugar for:

def my_func(self):
    return 'ok'
my_func = cachedProperty(name='test')(my_func)

so as long as the expression after @ returns your decorator [*] it doesn't matter what the expression itself actually does.

In the above example, the @cachedProperty(name='test') part first executes cachedProperty(name='test'), and the return value of that call is used as the decorator. In the above example, decorator is returned, so the my_func function is decorated by calling decorator(my_func), and the return value of that call is property object, so that is what'll replace my_func.


[*] The @ expression syntax is deliberately limited in how much it is allowed to do. You can do attribute lookups and calls, that's it, the decorator grammar rule only allows an optional call with arguments at the end of a dotted name (where dots are optional):

decorator               ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE)

This is a deliberate limitation of the syntax.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Yes that does not work, name is not referenced. How can I have access to it then? Also, would it be easier to define the decorator with a class? – Michael Mar 08 '14 at 16:41
  • is there a difference between using `(set|get|del)attr` and `__dict__`? – Brave Sir Robin Mar 08 '14 at 16:44
  • @rmartinjak: it would bypass any data descriptors, for one. So if there is a `type(self).test` property, `self.__dict__['test']` would access the instance attribute directly without triggering the property. – Martijn Pieters Mar 08 '14 at 16:46
  • once I defined a descriptor with arguments, do I always have to call it with the parentheses even if I want the default values for the arguments ``@cached_property()``? Doing ``@cached_property`` returns the decorator function. – Michael Mar 08 '14 at 18:10
  • @YAmikep: yes, because otherwise the `cached_property()` factory would be called with the function-to-decorate. – Martijn Pieters Mar 08 '14 at 18:11
  • Ok that's what I thought. Thanks. – Michael Mar 08 '14 at 19:25
  • Can I go back in time and vote against this decorator syntax? Requiring functions within functions was already bad enough - functions within functions within functions is horrible, not to mention ridiculously non-intuitive. I would never have guessed this syntax for defining a decorator that takes an argument. Ever. – ArtOfWarfare Jul 10 '15 at 02:19