Python 带参数的装饰器

Python decorator with arguments

我有一个 class 有很多非常相似的属性:

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
    ...   

所以想我会写一个装饰器来完成 属性 定义工作。

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')

这很好用,但是当我想使用 @myDecorator('foo') 语法时,它变得更加复杂并且无法确定 __call__ 方法应该 return 以及如何附加 属性 到它的 class.

目前我有:

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)

它 returns: AttributeError: 'myClass' object has no attribute 'foo'

如果您想使用 @decorator 语法,您将无法将 属性 重新映射到 class 上的不同名称。这意味着您的 compute_x 方法必须重命名为与属性相同的名称。

编辑:可以重新映射名称,但您还需要使用 class 装饰器。

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

没有 class 装饰器,只有当你的内部变量名与函数名不同时,name 参数才有意义,所以你可以像

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

并且它会寻找 _foobar 而不是 _foo,但属性名称仍然是 foo.

但是, 有一种方法可以重新映射属性名称,但您还必须使用 class 装饰器。

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

这将遍历 class 上的所有属性并找到任何 MyProperty 实例并检查集合名称是否与映射名称相同,如果不相同,它将重新绑定将 属性 传递给 myproperty 装饰器的名称。

您始终可以使用包装技巧将参数传递给装饰器,如下所示:

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    

我最终得到了一个元类,使子类化更容易。感谢 Brendan Abel 在这方面的暗示。

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)