Jacques Gaudin
Jacques Gaudin

Reputation: 16958

Python decorator with arguments

I have a class with a lot of very similar properties:

class myClass(object):

    def compute_foo(self):
        return 3

    def compute_bar(self):
        return 4

    @property
    def foo(self):
        try:
            return self._foo
        except AttributeError:
            self._foo = self.compute_foo()
            return self._foo

    @property
    def bar(self):
        try:
            return self._bar
        except AttributeError:
            self._bar = self.compute_bar()
            return self._bar
    ...   

So thought I would write a decorator to do the property definition work.

class myDecorator(property):
    def __init__(self, func, prop_name):
        self.func = func
        self.prop_name = prop_name
        self.internal_prop_name = '_' + prop_name

    def fget(self, obj):
        try:
            return obj.__getattribute__(self.internal_prop_name)
        except AttributeError:
            obj.__setattr__(self.internal_prop_name, self.func(obj))
            return obj.__getattribute__(self.internal_prop_name)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.func is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)


class myClass(object):

    def compute_foo(self):
        return 3
    foo = myDecorator(compute_foo, 'foo')

    def compute_bar(self):
        return 4
    bar = myDecorator(compute_bar, 'bar')

This works well, but when I want to use the @myDecorator('foo') syntax, it gets more complicated and cannot figure what the __call__ method should return and how to attach the property to its class.

For the moment I have :

class myDecorator(object):
    def __init__(self, prop_name):
        self.prop_name = prop_name
        self.internal_prop_name = '_' + prop_name

    def __call__(self, func):
        self.func = func
        return #???

    def fget(self, obj):
        try:
            return obj.__getattribute__(self.internal_prop_name)
        except AttributeError:
            obj.__setattr__(self.internal_prop_name, self.func(obj))
            return obj.__getattribute__(self.internal_prop_name)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.func is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

class myClass(object):
    @myDecorator('foo')
    def compute_foo(self):
        return 3

c = myClass()
print(c.foo)

and it returns: AttributeError: 'myClass' object has no attribute 'foo'

Upvotes: 1

Views: 138

Answers (3)

Jacques Gaudin
Jacques Gaudin

Reputation: 16958

I ended up with a metaclass, to make the subclassing easier. Thanks to Brendan Abel for hinting in this direction.

import types

class PropertyFromCompute(property):

    def __init__(self, func):
        self.func = None
        self.func_name = func.__name__
        self.internal_prop_name = self.func_name.replace('compute', '')

    def fget(self, obj):
        try:
            return obj.__getattribute__(self.internal_prop_name)
        except AttributeError:
            obj.__setattr__(self.internal_prop_name, self.func())
            return obj.__getattribute__(self.internal_prop_name)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.func is None:
            try:
                self.func =  obj.__getattribute__(self.func_name)
            except AttributeError:
                raise AttributeError("unreadable attribute")
        return self.fget(obj)

class WithPropertyfromCompute(type):

    def __new__(cls, clsname, bases, dct):
        add_prop = {}
        for name, obj in dct.items():
            if isinstance(obj, types.FunctionType) and name.startswith('compute_'):
                add_prop.update({name.replace('compute_',''): PropertyFromCompute(obj)})
        dct.update(add_prop)
        return super().__new__(cls, clsname, bases, dct)


class myClass(object, metaclass=WithPropertyfromCompute):

    def compute_foo(self):
        raise NotImplementedError('Do not instantiate the base class, ever !')

class myChildClass(myClass):

    def compute_foo(self):
        return 4

base = myClass()
try:
    print(base.foo)
except NotImplementedError as e:
    print(e)
print(myClass.foo)
child = myChildClass()
print(child.foo)

Upvotes: 0

Spade
Spade

Reputation: 2280

You could always use the wraps trick to pass arguments to your decorator as follows:

from functools import wraps

class myDecorator(property):
    def __init__(self, prop_name):
        self.prop_name = prop_name

    def __call__(self, wrappedCall):
        @wraps(wrappedCall)
        def wrapCall(*args, **kwargs):
            klass = args[0]
            result = wrappedCall(*args, **kwargs)
            setattr(klass, self.prop_name, result)
        return wrapCall

class myClass(object):
    @myDecorator('foo')
    def compute_foo(self):
        return 3

c = myClass()
c.compute_foo()
print c.foo    

Upvotes: 2

Brendan Abel
Brendan Abel

Reputation: 37499

If you want to use the @decorator syntax you're not going to be able to remap the property to a different name on the class. That means your compute_x methods are going to have to be renamed the same as the attribute.

EDIT: It is possible to remap the names, but you'd need to use a class decorator as well.

class MyProperty(property):
    def __init__(self, name, func):
        super(MyProperty, self).__init__(func)
        self.name = name
        self.internal_prop_name = '_' + name
        self.func = func

    def fget(self, obj):
        try:
            return obj.__getattribute__(self.internal_prop_name)
        except AttributeError:
            obj.__setattr__(self.internal_prop_name, self.func(obj))
            return obj.__getattribute__(self.internal_prop_name)

    def __get__(self, obj, objtype=None)
        if obj is None:
            return self
        if self.func is None:
            raise AttributeError('unreadable')
        return self.fget(obj)

def myproperty(*args)
    name = None
    def deco(func):
        return MyProperty(name, func)

    if len(args) == 1 and callable(args[0]):
        name = args[0].__name__
        return deco(args[0])
    else:
        name = args[0]
        return deco


class Test(object):

    @myproperty
    def foo(self):
        return 5

Without the class decorator, the only time the name argument would be relevant would be if your internal variable name was different from the function name, so you could so something like

@myproperty('foobar')
def foo(self):
    return 5

and it would look for _foobar instead of _foo, but the attribute name would still be foo.

However, there is a way you could remap the attribute names, but you'd have to use a class decorator as well.

def clsdeco(cls):
    for k, v in cls.__dict__.items():
        if isinstance(v, MyProperty) and v.name != k:
            delattr(cls, k)
            setattr(cls, v.name, v)
    return cls


@clsdeco
class Test(...)

    @myproperty('foo')
    def compute_foo(self):
        pass

This will go through all the attributes on the class and find any that are MyProperty instances and check if the set name is the same as the mapped name, if not, it will rebind the property to the name passed into the myproperty decorator.

Upvotes: 1

Related Questions