_
(下划线)开头,具有只读 @property。我不想多次编写该代码。因此,我考虑了一个应用于我的类的装饰器,以读取其所有属性,并为那些以
@property
开头且名称不带
_
下划线的属性创建
_
。就像
self._age
得到
@property def age(self): return self._age
。我有点挣扎,因为我已经忘记了一些关于编写以下代码的装饰器:
def make_readonly_properties(cls):
for attr_name in dir(cls):
if attr_name.startswith('_'):
setattr(cls, attr_name[1:], property(getattr(cls, attr_name)))
return cls
这是一个测试这个想法的示例类,到目前为止还没有工作。
@make_readonly_properties
class X:
def __init__(self):
self._age = 34
self._address = 'some address'
self.dog_name = 'Saitama'
我刚刚开始怀疑是否没有 PEP?对于 __
两个下划线也很有用。
__init__
方法,但了解该信息的唯一方法是运行该方法,或者从方法中的代码中提取信息(这一开始会非常混乱)。标准库处理类似事情的方式,例如
dataclasses.datclass
,是依靠类型提示作为指定属性的方式,但这也会为您创建一个
__init__
。坦率地说,我认为这是最好的方法。但是这是Python,我们可以做很多自省,所以我们为什么不实际尝试一下,看看我们能用更混乱的替代方案走多远。我们可以使用
inspect
从方法中提取源代码。如果它在类定义中,它将缩进,因此我们可以使用
textwrap.dedent
使其可解析,因为我们将使用
ast
模块来遍历抽象语法树,并检查任何赋值到属性。
def make_readonly_properties(cls):
import ast, inspect, textwrap
__init__ = vars(cls)['__init__'] # ignoring inheritance here and assume that an `__init__` was written
tree = ast.parse(textwrap.dedent(inspect.getsource(__init__)))
attrs = set()
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Attribute):
attrs.add(target.attr)
def getter_maker(attr_name):
def _fget(self):
return getattr(self, attr_name)
return _fget
for attr_name in attrs:
if attr_name.startswith('_'):
setattr(cls, attr_name[1:], property(fget=getter_maker(attr_name)))
return cls
@make_readonly_properties
class X:
def __init__(self):
self._age = 34
self._address = 'some address'
self.dog_name = 'Saitama'
注意,上面不处理继承,并假设类定义中写入了 __init__
。我们可以说这是为了简单和理智而故意的。这是 ipython REPL 中的一个小测试:
In [1]: def make_readonly_properties(cls):
...: import ast, inspect, textwrap
...: __init__ = vars(cls)['__init__'] # ignoring inheritance here and assume that an `__init__` was written
...: tree = ast.parse(textwrap.dedent(inspect.getsource(__init__)))
...: attrs = set()
...: for node in ast.walk(tree):
...: if isinstance(node, ast.Assign):
...: for target in node.targets:
...: if isinstance(target, ast.Attribute):
...: attrs.add(target.attr)
...:
...: def getter_maker(attr_name):
...: def _fget(self):
...: return getattr(self, attr_name)
...: return _fget
...:
...: for attr_name in attrs:
...: if attr_name.startswith('_'):
...: setattr(cls, attr_name[1:], property(fget=getter_maker(attr_name)))
...:
...: return cls
...:
...: @make_readonly_properties
...: class X:
...: def __init__(self):
...: self._age = 34
...: self._address = 'some address'
...: self.dog_name = 'Saitama'
...:
In [2]: x = X()
In [3]: x.age
Out[3]: 34
In [4]: x.address
Out[4]: 'some address'
In [5]: x.dog_name
Out[5]: 'Saitama'
In [6]: x.age = 1
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[6], line 1
----> 1 x.age = 1
AttributeError: property of 'X' object has no setter
In [7]: x.address = "foo"
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[7], line 1
----> 1 x.address = "foo"
AttributeError: property of 'X' object has no setter
In [8]: x.dog_name = "Fido"