如何最好地实现一个只能调用一次 setter 的属性?

问题描述 投票:0回答:6

我试图实现一个类属性,其中设置器只能被调用一次,我想知道如何最好地实现这一点?以及如何使其最“Pythonic”?

我考虑过的选项:

  1. 子类化并扩展内置
    property
  2. 装饰属性的 setter。
  3. 添加一个属性,该属性保留每个设置器设置的频率。

还有其他想法吗?

以及如何最好地实施的建议?

python properties python-decorators
6个回答
1
投票

如果您经常使用它以及其他

property
功能,子类化属性是合适的。

由于财产的运作方式,这有点棘手 - 当有人打电话时

@prop.setter
,创建该属性的新实例。下面的子类将起作用。


class FuseProperty(property):
    def setter(self, func):
        def fuse(instance, value):
            name = f"_fuse_{self.fget.__name__}"
            if not getattr(instance, name, False):
                func(instance, value)
            setattr(instance, name, True)
        return super().setter(lambda instance, value: fuse(instance, value))

这是正在使用的。

In [24]: class A:
    ...:     @FuseProperty
    ...:     def a(self):
    ...:         return self._a
    ...:     @a.setter
    ...:     def a(self, value):
    ...:         self._a = value
    ...:

In [25]: a = A()

In [26]: a.a = 23

In [27]: a.a
Out[27]: 23

In [28]: a.a = 5

In [29]: a.a
Out[29]: 23

但是,如果这个“fuse”属性就是您所需要的,并且没有其他代码添加到 getter 和 setter,那么它可以简单得多:您可以创建一个全新的“Descriptor”类,使用与

property
- 这可能会好得多,因为您的“保险丝”属性可以在一行中构建,而不需要 setter 和 getter 方法。

所需要的只是一个具有

__get__
__set__
方法的类 - 我们可以添加
__set_name__
来自动获取新的属性名称(
property
本身没有,所以我们从
fget
获取名称)方法如上)

class FuseAttribute:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, f"_{self.name}")
    def __set__(self, instance, value):
        if not getattr(instance, f"_fuse_{self.name}", False):
            setattr(instance, f"_{self.name}", value)
        # add an else clause for optionally raising an error
        setattr(instance, f"_fuse_{self.name}", True)

并使用它:


In [36]: class A:
    ...:     a = FuseAttribute()
    ...:

In [37]: a = A()

In [38]: a.a = 23

In [39]: a.a
Out[39]: 23

In [40]: a.a = 5

In [41]: a.a
Out[41]: 23

1
投票

Python 中的属性只是描述符,并且相对容易实现自己的来完全实现你想要的功能:

class SetOnceProperty:
    def __init__(self, name):
        self.storage_name = '_' + name

    def __get__(self, obj, owner=None):
        return getattr(obj, self.storage_name)

    def __set__(self, obj, value):
        if hasattr(obj, self.storage_name):
            raise RuntimeError(f'{self.storage_name[1:]!r} property already set.')
        setattr(obj, self.storage_name, value)

    def __delete___(self, obj):
        delattr(obj, self.storage_name)


class Test:
    test_attr = SetOnceProperty('test_attr')

    def __init__(self, value):
        self.test_attr = value*2  # Sets property.


test = Test(21)
print(test.test_attr)  # -> 42

test.test_attr = 13  # -> RuntimeError: 'test_attr' property already set.


1
投票

这很简单,也更通用。函数的装饰器仅被调用一次,并忽略后续调用。

def onlyonce(func):
    @functools.wraps(func)
    def decorated(*args):
        if not decorated.called:
            decorated.called = True
            return self.func(*args)
    decorated.called = False
    return decorated

这样使用

class A:
    @property
    def x(self):
        ...
    @x.setter
    @onlyonce
    def x(self, val):
        ...

或者你可以定义一个描述符:

class Desc:
    def __get__(self, inst, own):
        return self._value

    def __set__(self, inst, value):
        if not hasattr(self, _value):
            self._value = value

  

并像这样使用:

class A:
    x = Desc()

0
投票

我经常更喜欢这种方式; “显式优于隐式”:

class MyError(Exception):
    ...

NOT_SET = object()

class C:

    def set_my_property(self, spam, eggs, cheese):
        """This sets the property.
        If it's already set, you'll get an error. Donna do dat.
        """
        if getattr(self, "_my_property", NOT_SET) is NOT_SET:
            self._my_property = spam, eggs, cheese
            return
        raise MyError("I said, Donna do dat.")

    @property
    def my_property(self):
        return self._my_property

测试:

c=C()
c.set_my_property("spam", "eggs", "cheese")
assert c.my_property == ("spam", "eggs", "cheese")

try:
    c.set_my_property("bacon", "butter", "coffee")
except MyError:
    pass

0
投票

听起来

attrs
项目会对您的用例有所帮助。您可以通过以下方式实现“冻结”属性

import attr


@attr.s
class Test:
    constant = attr.ib(on_setattr=attr.setters.frozen)


test = Test('foo')
test.constant = 'bar'  # raises `attr.exceptions.FrozenAttributeError`

请注意,它还通过

@constant.validator
支持验证器(请参阅
attr.ib
文档
末尾的示例)。


0
投票

我会调用辅助方法来设置值并使该方法替换自身。

@x.setter
def x(self, value):
    self._set_x(value)
def _set_x(self, value):
    self._x = value
    self._set_x = lambda v: 0

你也可以使用这样的装饰器:

def once_setter(property):
    def decorator(func):
        def wrapper(self, value):
            func(self, value)
            setattr(self, func.__name__, lambda v:0)
        property.setter(lambda self, value: getattr(self, func.__name__)(value))
        return wrapper
    return decorator 

然后像这样使用它

@once_setter(x)
def _x_setter(self, value):
    self._x = value

请注意,您定义的 setter 的名称与属性的名称不同。

装饰器包装setter,并将属性setter设置为调用包装器的lambda函数。

反过来,包装器调用您的 setter,然后用虚拟函数替换自身。

请注意,您可以通过删除

_x_setter
属性来恢复替换

© www.soinside.com 2019 - 2024. All rights reserved.