【问题标题】:Dynamically add/overwrite the setter and getter of property attributes动态添加/覆盖属性属性的setter和getter
【发布时间】:2021-11-06 13:30:47
【问题描述】:

我需要使用/模仿语法糖语法在子类中动态装饰 getter 和 setter 对方法。

我正在为 setter 实现而苦苦挣扎。

class A:

    def __init__(self, x):
        print('init')
        self.__x = x

    @property
    def x(self):
        print('getter')
        return self.__x

    @x.setter
    def x(self, v):
        print('setter')
        self.__x = v


class Dec:
    def __init__(self):
        print('init - dec')

    def __call__(self, cls):
        c = type('A_Dec', (cls,), {})
        # super-init
        setattr(c, '__init__', lambda sub_self, x: super(type(sub_self), sub_self).__init__(x))
        # getter
        setattr(c, 'x', property(lambda sub_self: super(type(sub_self), sub_self).x))
        
        # setter - see below

        return c

dec_A = Dec()(A)
dec_a = dec_A('p')
print(dec_a.x)

输出

init - dec
init
getter
p

如果我尝试在Decdec_a.x = 'p' 中实现 setter 方法,使用以下方法会收集到以下错误:

    # setter-statements of __call__

    # Attempt 1
    setattr(c, 'x', property(fset=lambda sub_self, v: super(type(sub_self), sub_self).x(v)))
    # AttributeError: unreadable attribute
    
    # Attempt 2 - auxiliary function
    def m(sub_self, v):
       print('--> ', sf, super(type(sub_self), sub_self))
       super(type(sub_self), sub_self).x = v
    
    # Attempt 2.A
    setattr(c, 'x', eval('x.setter(m)'))
    # NameError: name 'x' is not defined
    
    # Attempt 2.B
    setattr(c, 'x', property(fset=lambda sf, v: m(sf, v)))
    # AttributeError: unreadable attribute
    
    # Attempt 2.C: !! both at once, `fget`and `fset` so, in case, comment the getter in the above code to avoid conflicts
    setattr(c, 'x', property(fget=lambda sub_self: super(type(sub_self), sub_self).x, fset=m))
    # AttributeError: 'super' object has no attribute 'x'
    
    # Attempt 2.D
    p = property(fget=lambda sub_self: super(type(sub_self), sub_self).x, fset=m)
    setattr(c, 'x', p)
    # AttributeError: 'super' object has no attribute 'x'

Attempt 1 引发错误,因为(我猜)用括号设置属性。所以在尝试2中我使用了一个辅助函数,因为lambda不允许初始化,'='语句,同样没有成功。

  • 有没有办法动态模仿属性 getter/setter 装饰器? (可能没有额外的导入) 还有其他方法吗?

  • 额外:为什么 super 没有属性就不能工作? super().x(v) -> TypeError: super(type, obj): obj must be an instance or subtype of type

编辑:

  • Extra 的答案:来自文档:零参数形式仅适用于类定义[...]
  • 使用 python3.9

【问题讨论】:

  • 你用的是什么python版本?您粘贴的代码(第一个块)没有运行 paiza.io/projects/e/6mSBY6t8rUdx4fgVGih7vA?theme=twilight 或者您可能在那里粘贴了不同的块?
  • super 使用静态出现super 的类基于一些编译器魔法获取其默认参数。在本例中,即为Dec,但您打算使用c。尝试使用class 语句而不是调用type 来定义A_Dec
  • super 也实现了__getattribute__,所以它对属性查找的处理不同于“普通”类。 super().x = v 不触发属性设置器或错误是有意(以及为什么),我不知道。
  • @Niel Godfrey Ponciano 对不起,我忘记了 getter 的属性,__call__ 中的 super,现在应该可以工作了。 Python 3.9

标签: python inheritance properties overwrite


【解决方案1】:

参考文献

重要

  • python 3.9 合作(否则可能与super 或其他人合作)
  • 从文档中调用 零参数形式 [super()] 仅适用于类定义 [...]

简要案例2.3代表问题的解决方案,其他案例研究显示可能的替代方案和(也许)有关如何达到它的有用信息


步骤 0:父类(配备描述符协议的引用类)

class A:

    def __init__(self, x): self.__x = x

    @property
    def x(self): return self.__x

    @x.setter
    def x(self, v): self.__x = v

第 1 步:审核 - 静态覆盖 getter/setter,两种方式

方式 1.1 - 语法糖

class B(A):

    def __init__(self, x): super().__init__(x)

    @property
    def x(self): return super().x # still not sure if there isn't a better way

    @x.setter
    def x(self, v): super(B, B).x.fset(self, v)

b = B('x')
print(b.x)
b.x = 'xx'
print(b.x)

# Output
x
xx

方式 1.2 - 属性作为类属性

class C(A):

    def __init__(self, x): super().__init__(x)

    def x_read(self): return super().x

    def x_write(self, v): super(C, C).x.fset(self, v) # notice the arguments of super!!!

    x = property(x_read, x_write)

c = C('x')
print(c.x)
c.x = 'xx'
print(c.x)

# Output
x
xx

第 2 步:动态覆盖 getter/setter - 两种方式

方式 2.1 静态装饰器(这是解决方案在静态设置中的样子)

class DecStatic:

    def __init__(self):
        print('init - static dec')

    def __call__(self, cls):
        class A_Dec(cls):
            def __init__(sub_self, x):
                super().__init__(x)

            @property
            def x(sub_self):
                return super().x

            @x.setter
            def x(sub_self, v):
                super(type(sub_self), type(sub_self)).x.fset(sub_self, v)

        return A_Dec

dec_A = DecStatic()(A)
dec_a = dec_A('x')
print(dec_a.x)
dec_a.x = 'xx'
print(dec_a.x)

# Output
init - static dec
x
xx

方式 2.2 动态装饰器 - 将属性覆盖为类属性

class DecDynamicClsAttr:
    def __init__(self):
        print('init - dynamic dec - class attr')

    def __call__(self, cls):
        DecA = type('DecA', (cls,), {})
        # super-init
        setattr(DecA, '__init__', lambda sub_self, x: super(type(sub_self), sub_self).__init__(x))
        # property
        setattr(DecA, 'x', property(fget=lambda sub_self: super(type(sub_self), sub_self).x, fset=lambda sub_self, v: super(type(sub_self), type(sub_self)).x.fset(sub_self, v)))

        return DecA

dec_A = DecDynamicClsAttr()(A)
dec_a = dec_A('x')
print(dec_a.x)
dec_a.x = 'xx'
print(dec_a.x)

# Output
init - dynamic dec - class attr
x
xx

方式 2.3 动态装饰器 - (苦涩的)语法糖语法 (

class DecDynamicSS:
    def __init__(self):
        print('init - dynamic dec - syntactic sugar')

    def __call__(self, cls):
        DecA = type('A_Dec', (cls,), {})
        # super-init
        setattr(DecA, '__init__', lambda sub_self, x: super(type(sub_self), sub_self).__init__(x))

        # getter
        setattr(DecA, 'x', property(lambda sub_self: super(type(sub_self), type(sub_self)).x.fget(sub_self)))
        # setter
        setattr(DecA, 'x', DecA.x.setter(lambda sub_self, v: super(type(sub_self), type(sub_self)).x.fset(sub_self, v)))

        return DecA

dec_A = DecDynamicSS()(A)
dec_a = dec_A('x')
print(dec_a.x)
dec_a.x = 'xx'
print(dec_a.x)

# Output
init - dynamic dec - syntactic sugar
x
xx

备注

  • 从 2.2 开始,很明显,以 getter/setter 描述符为特征的属性绑定到类而不是实例,所以在 2.3 中,我们也应该跟踪类:

       `x.setter` --> `cls.x.setter`
    
  • 感谢 @chepner 的评论和@Niel Godfrey Ponciano 的回答,感谢他们提供的有用信息

【讨论】:

    【解决方案2】:

    属性设置器未正确设置。为了可视化这一点,如果没有为属性显式设置 setter,则该属性将变为只读,为 documented

    class Parrot:
        def __init__(self):
            self._voltage = 100000
    
        @property
        def voltage(self):
            """Get the current voltage."""
            return self._voltage
    

    @property 装饰器将 voltage() 方法转换为具有相同名称的 只读属性 的“getter”

    假设我们有这个:

    class A:
        def __init__(self, x):
            self.__x = x
    
        @property
        def x(self):
            return self.__x
    
    a = A(123)
    
    print(a.x)  # will display "123"
    a.x = 456  # will display "AttributeError: can't set attribute"
    

    在您的原始代码中,您创建了一个新类型A_Dec。您明确设置了 getter:

    # getter
    setattr(c, 'x', property(lambda sub_self: super(type(sub_self), sub_self).x))
    

    但是您没有显式设置任何 setter,从而使 x 属性为只读。这会导致这段代码出错:

    dec_a.x = 'new value!'  # will display "AttributeError: can't set attribute"
    

    解决方案 1

    不要明确定义 getter。这样,所有对x 的访问都将委托给实际类A

    解决方案 2

    如果你定义了getter,那么也要定义setter。

    ...
    class Dec:
        ...
        def __call__(self, cls):
            ...
            # setter
            x_property = getattr(c, 'x')
            x_setter = getattr(x_property, 'setter')
            setattr(c, 'x', x_setter(lambda sub_self, v: super(type(sub_self), type(sub_self)).x.fset(sub_self, v)))
            ...
    ...
    
    • c.x.setter的用法如documented

      属性对象具有可用作装饰器的gettersetterdeleter方法

    • .fset的用法如documented

      fset是设置属性值的函数...返回的属性对象还有fgetfset、@987654343属性@对应构造函数参数。

    所以添加下面几行就成功了:

    dec_a.x = 'new value!'
    print(dec_a.x)
    

    输出:

    setter
    getter
    new value!
    

    更多参考资料:

    【讨论】:

    • 感谢您的回答和参考,github 确实很有趣,但是您仍然没有回答问题,如何使用装饰师