在Python中重写属性

71 投票
3 回答
72788 浏览
提问于 2025-04-16 23:20

我正在想办法找到一种最优雅、代码量最少的方法,来允许在Python中重写属性的特定函数(比如只重写获取器、只重写设置器等等)。我比较喜欢下面这种定义属性的方式,因为它把所有相关的方法都放在同一个缩进的代码块里,这样更容易看出处理一个属性的函数在哪里结束,处理下一个属性的函数又从哪里开始:

@apply
def foo():
    """A foobar"""
    def fget(self):
        return self._foo
    def fset(self, val):
        self._foo = val
    return property(**locals())

不过,如果我想从一个以这种方式定义属性的类继承,然后重写foo的设置函数,这就变得有点棘手。我查了一些资料,发现大多数答案都是在基类中定义单独的函数(比如getFoosetFoo),然后从这些函数显式创建一个属性定义(比如foo = property(lambda x: x.getFoo(), lambda x, y: x.setFoo(y), lambda x: x.delFoo())),接着根据需要重写getFoosetFoodelFoo

我不喜欢这个解决方案,因为这意味着我必须为每个属性都定义一个lambda表达式,然后写出每个函数调用(而之前我可以直接用property(**locals()))。而且我也失去了最初的封装性。

理想情况下,我希望能够做到类似这样的事情:

class A(object):
    def __init__(self):
        self.foo = 8
    @apply
    def foo():
        """A foobar"""
        def fget(self):
            return self._foo
        def fset(self, val):
            self._foo = val
        return property(**locals())

class ATimesTwo(A):
    @some_decorator
    def foo():
        def fset(self, val):
            self._foo = val * 2
        return something

然后输出看起来会像这样:

>>> a = A()
>>> a.foo
8
>>> b = ATimesTwo()
>>> b.foo
16

基本上,ATimesTwoA继承了获取器函数,但重写了设置器函数。有没有人知道怎么做到这一点(以一种看起来和上面例子相似的方式)?some_decorator应该是什么样的函数,foo函数又应该返回什么?

3 个回答

12

stderr的答案适用于大多数情况。

我想补充一个解决方案,适用于你想扩展 gettersetter 和/或 deleter 的情况。有两种方法可以做到这一点:

1. 子类化 property

第一种方法是通过创建一个新的类,继承内置的 property,并添加一些装饰器,这些装饰器是 gettersetter 和/或 deleter 的变种,它们可以“扩展”当前的获取、设置和删除功能。

下面是一个支持在设置函数中添加方法的属性示例:

class ExtendableProperty(property):
    def append_setter(self, fset):
        # Create a wrapper around the new fset that also calls the current fset
        _old_fset = self.fset

        def _appended_setter(obj, value):
            _old_fset(obj, value)
            fset(obj, value)
        # Use that wrapper as setter instead of only the new fset
        return self.setter(_appended_setter)

用法和普通属性一样,只不过现在可以在属性的设置器中添加方法了:

class A(object):
    @ExtendableProperty
    def prop(self):
        return self._prop

    @prop.setter
    def prop(self, v):
        self._prop = v


class B(A):
    @A.prop.append_setter
    def prop(self, v):
        print('Set', v)
>>> a = A()
>>> a.prop = 1
>>> a.prop
1

>>> b = B()
>>> b.prop = 1
Set 1
>>> b.prop
1

2. 重写 getter、setter 和/或 deleter

使用普通属性,重写 getter、setter 或 deleter,然后在父类的属性中添加对 fgetfsetfdel 的调用。

下面是与第一种示例相同类型的属性示例:

class A(object):
    @property
    def prop(self):
        return self._prop

    @prop.setter
    def prop(self, v):
        self._prop = v


class B(A):
    @A.prop.setter
    def prop(self, v):
        A.prop.fset(self, v)  # This is the call to the original set method
        print('Set {}'.format(v))

我觉得第一种选择看起来更好,因为不需要调用父类属性的 fset。

85

Python的文档中提到,使用property装饰器时,可以这样写:

class C(object):
    def __init__(self):
        self._x = None
    @property
    def x(self):
        return self._x
    @x.setter
    def x(self, value):
        self._x = value
    @x.deleter
    def x(self):
        del self._x

然后,子类可以像这样重写一个单独的设置器/获取器:

class C2(C):
    @C.x.getter
    def x(self):
        return self._x * -1

这样做有点麻烦,因为重写多个方法似乎需要你做一些像这样的事情:

class C3(C):
    @C.x.getter
    def x(self):
        return self._x * -1
    # C3 now has an x property with a modified getter
    # so modify its setter rather than C.x's setter.
    @x.setter 
    def x(self, value):
        self._x = value * 2

当然,当你重写获取器、设置器和删除器的时候,你可能可以直接为C3重新定义这个属性。

62

我相信你之前一定听说过,apply这个东西已经被淘汰有八年了,从Python 2.3开始就不推荐使用了。别用它。你用locals()的做法也不符合Python的哲学——明确的比隐含的要好。如果你真的喜欢那种增加缩进的方式,其实没必要创建一个临时对象,直接这样做就行了:

if True:
    @property
    def foo(self):
        return self._foo
    @foo.setter
    def foo(self, val):
        self._foo = val

这样做既不会滥用locals,也不需要用到apply,更不需要额外创建一个对象,也不用在后面再加一行foo = foo(),这样看起来更清晰。对于你传统的使用property的方式,这样做同样有效——直接用foo = property(fget, fset)就可以了。

如果你想在某个子类中重写一个属性,可以参考这样的做法

如果子类知道这个属性是在哪里定义的,那就直接这样做:

class ATimesTwo(A):
    @A.foo.setter
    def foo(self, val):
        self._foo = val * 2

撰写回答