用于属性和方法的Python装饰器?

1 投票
1 回答
6235 浏览
提问于 2025-04-18 05:48

有没有可能创建一个装饰器,让一个方法在用 class.something = 2 赋值时像属性一样工作,而在用 class.something(2, True) 调用时又像方法一样工作呢?

我现在的情况

为了更具体一点,我现在有以下内容:

class attributeSetter(object):
   ''' Makes functions appear as attributes. Takes care of autologging.'''
   def __init__(self, func):
       self.func = func

   def __set__(self, obj, value):
       return self.func(obj, value)

   def __repr__(self):
       return repr(self.__getattribute__)

所以在这个带有装饰器的方法的类中:

class myClass(object):
   @attributeSetter  # decorated!
   def myAttrib(self, value, add=False, subtract=False):
       if add:
           value += 2
       if subtract:
           value -= 2
       self.__dict__['myAttrib'] = value

我可以这样做:

instance = myClass()
instance.myAttrib = 2  # sets the value to 2
instance.myAttrib *= 4  # now 8
print instance.myAttrib  # 8

我想要的

但我还想能够这样做:

instance.myAttrib(3, add=True)  # now 8 + 3 + 2 = 13
instance.myAttrib(0, subtact=True)  # now 13 + 0 - 2 = 11
print instance.myAttrib  # 11

我猜我只需要在 attributeSetter 装饰器中添加一个 __call__,像这样:

def __call__(self, *args, **kwargs):
    self.func(*args, **kwargs)

但这样的话,就不能用 "=" 语法来设置值了。

为什么?

我正在共同开发 PsychoPy(一个用于心理物理学中刺激传递的 Python 模块),我们希望在每个刺激参数被设置时进行一些处理,因此需要这个装饰器来避免使用 setter/getter 方法。我们默认记录每个属性的变化,但在某些情况下,用户希望通过额外的 log=False 参数来禁用这个特定设置的日志记录,或者提供更多的参数。因此在这些情况下,如果能像使用函数/方法一样使用属性,那就太好了,因为它本质上确实是这样。

1 个回答

3

你需要实现一个 __get__ 方法,这个方法要返回一个可以调用的对象。你的装饰器已经提供了一个描述符对象,只需要让它在作为属性访问时正常工作就行。

这其实可以很简单,只需将被包装的函数绑定起来(这样你就得到了一个绑定的方法):

class attributeSetter(object):
    ''' Makes functions appear as attributes. Takes care of autologging.'''
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return self.func.__get__(instance, owner)

    def __set__(self, obj, value):
        return self.func(obj, value)

不过,这样做会导致与简单访问属性不兼容!现在 instance.myAttrib 返回的是一个绑定的方法!你不能两全其美;方法本质上就是绑定的属性(也就是通过描述符协议传递的属性),它们恰好是可以调用的。

当然,你可以从 __get__ 返回一个代理对象;这个代理对象实现一个 __call__ 方法,并尽量表现得像底层的管理实例属性(也就是你在 self.__dict__['myAttrib'] 中存储的内容);但这条路充满了问题,因为代理对象永远无法真正成为底层属性的值。

撰写回答