装饰器为我的所有类_attributes Python3+创建@property getters

问题描述 投票:0回答:1
我正在使用 SQLAlchemy 编写一个代表对象的类。我希望它的许多成员属性(实际上是全部)以

_

(下划线)开头,具有只读 @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?对于 

__

 两个下划线也很有用。

python-3.x python-decorators
1个回答
1
投票
这里的根本问题是该类没有任何这些属性。它有一个创建实例属性的

__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"
    
© www.soinside.com 2019 - 2024. All rights reserved.