简体   繁体   English

带参数的Python装饰器

[英]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. 这很好用,但是当我想使用@myDecorator('foo')语法时,它变得更复杂,无法确定__call__方法应该返回什么以及如何将属性附加到它的类。

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' 它返回: AttributeError: 'myClass' object has no attribute 'foo'

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    

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. 如果要使用@decorator语法,则无法将该属性重新映射到该类的其他名称。 That means your compute_x methods are going to have to be renamed the same as the attribute. 这意味着您的compute_x方法必须重命名为与属性相同。

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 没有类装饰器,name参数唯一相关的时候就是你的内部变量名与函数名不同,所以你可以这样

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

and it would look for _foobar instead of _foo , but the attribute name would still be foo . 它会寻找_foobar而不是_foo ,但属性名称仍然是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. 这将遍历类的所有属性并找到任何MyProperty实例,并检查集合名称是否与映射名称相同,否则,它会将属性重新绑定到传递给myproperty装饰器的名称。

I ended up with a metaclass, to make the subclassing easier. 我最终得到了一个元类,使子类更容易。 Thanks to Brendan Abel for hinting in this direction. 感谢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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM