我试图实现一个类属性,其中设置器只能被调用一次,我想知道如何最好地实现这一点?以及如何使其最“Pythonic”?
我考虑过的选项:
property
。还有其他想法吗?
以及如何最好地实施的建议?
如果您经常使用它以及其他
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
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.
这很简单,也更通用。函数的装饰器仅被调用一次,并忽略后续调用。
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()
我经常更喜欢这种方式; “显式优于隐式”:
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
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
文档末尾的示例)。
我会调用辅助方法来设置值并使该方法替换自身。
@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
属性来恢复替换