python 如何使用setattr或exec创建私有类变量?
我遇到了一个情况,就是当使用 setattr
或 exec
时,伪私有类成员的名字并没有被改变。
In [1]: class T:
...: def __init__(self, **kwargs):
...: self.__x = 1
...: for k, v in kwargs.items():
...: setattr(self, "__%s" % k, v)
...:
In [2]: T(y=2).__dict__
Out[2]: {'_T__x': 1, '__y': 2}
我也试过 exec("self.__%s = %s" % (k, v))
,结果也是一样:
In [1]: class T:
...: def __init__(self, **kwargs):
...: self.__x = 1
...: for k, v in kwargs.items():
...: exec("self.__%s = %s" % (k, v))
...:
In [2]: T(z=3).__dict__
Out[2]: {'_T__x': 1, '__z': 3}
如果用 self.__dict__["_%s__%s" % (self.__class__.__name__, k)] = v
是可以的,但 __dict__
是一个只读属性。
有没有其他方法可以动态创建这些伪私有类成员(而不需要手动写名字改变的规则)?
我可以更好地表述我的问题:
当 Python 遇到一个双下划线(self.__x
)的属性被设置时,背后到底发生了什么?有没有什么神奇的函数在做名字改变的工作?
3 个回答
这是我目前的一个小技巧。如果你有更好的建议,欢迎分享。
class T(object):
def __init__(self, **kwds):
for k, v in kwds.items():
d = {}
cls_name = self.__class__.__name__
eval(compile(
'class dummy: pass\n'
'class {0}: __{1} = 0'.format(cls_name, k), '', 'exec'), d)
d1, d2 = d['dummy'].__dict__, d[cls_name].__dict__
k = next(k for k in d2 if k not in d1)
setattr(self, k, v)
>>> t = T(x=1, y=2, z=3)
>>> t._T__x, t._T__y, t._T__z
(1, 2, 3)
关于这个问题:
当Python遇到一个双下划线(
self.__x
)属性被设置时,它“背后”到底发生了什么?有没有什么神奇的函数来处理这个名字的变化呢?
据我所知,这个问题在编译器中是特别处理的。所以一旦代码变成字节码,名字就已经被改变了;解释器根本看不到原来的名字,也不知道需要特别处理。这就是为什么通过 setattr
、exec
或者在 __dict__
中查找字符串时不起作用的原因;编译器把这些都当作字符串看待,并不知道它们和属性访问有什么关系,所以直接原样通过。解释器对名字的变化一无所知,所以它就直接使用这些名字。
每当我需要绕过这个问题时,我通常会手动进行同样的名字变化,虽然这样做有点 hacky。我发现使用这些“私有”名字通常不是个好主意,除非你确实知道需要它们来实现它们的目的:让类的继承层次结构都能使用相同的属性名,但每个类都有自己的副本。随便在属性名中加上双下划线,仅仅因为它们应该是私有的实现细节,似乎带来的麻烦比好处多;我现在更倾向于使用单下划线,作为一个提示,告诉外部代码不要去碰它。
我认为Python在编译时会对私有属性进行处理...具体来说,这个过程发生在它刚刚把源代码解析成抽象语法树,并将其转换为字节码的阶段。这是执行过程中,虚拟机唯一知道函数定义在哪个类的作用域内的时刻。然后,它会对伪私有属性和变量进行处理,而其他的则保持不变。这有几个影响...
特别是字符串常量不会被处理,这就是为什么你的
setattr(self, "__X", x)
会被忽略的原因。由于处理依赖于函数在源代码中的作用域,因此在类外定义的函数然后“插入”时,不会进行任何处理,因为在编译时并不知道它们“属于”哪个类。
据我所知,没有简单的方法可以在运行时确定一个函数是在哪个类中定义的...至少没有不需要大量
inspect
调用的方法,这些调用依赖于源代码反射来比较函数和类源代码之间的行号。即使这样的方法也不是100%可靠,有些边界情况可能导致错误的结果。这个处理过程实际上对处理并不太细致——如果你尝试在一个不是函数定义所在类的实例上访问
__X
属性,它仍然会为那个类进行处理...这让你可以在其他对象的实例中存储私有类属性!(我几乎可以说这一点是个特性,而不是一个bug)
所以变量的处理需要手动进行,以便你计算出处理后的属性名称,以便调用setattr
。
关于处理本身,它是通过_Py_Mangle函数完成的,使用了以下逻辑:
__X
会在前面加一个下划线和类名。例如,如果类名是Test
,处理后的属性名就是_Test__X
。- 唯一的例外是如果类名以下划线开头,这些下划线会被去掉。例如,如果类名是
__Test
,处理后的属性名仍然是_Test__X
。 - 类名末尾的下划线不会被去掉。
为了把这些内容封装成一个函数...
def mangle_attr(source, attr):
# return public attrs unchanged
if not attr.startswith("__") or attr.endswith("__") or '.' in attr:
return attr
# if source is an object, get the class
if not hasattr(source, "__bases__"):
source = source.__class__
# mangle attr
return "_%s%s" % (source.__name__.lstrip("_"), attr)
我知道这有点“硬编码”了名称处理,但至少它被隔离在一个函数中。然后可以用来处理setattr
的字符串:
# you should then be able to use this w/in the code...
setattr(self, mangle_attr(self, "__X"), value)
# note that would set the private attr for type(self),
# if you wanted to set the private attr of a specific class,
# you'd have to choose it explicitly...
setattr(self, mangle_attr(somecls, "__X"), value)
另外,以下的mangle_attr
实现使用了eval,这样它总是使用Python当前的处理逻辑(虽然我认为上面列出的逻辑从未改变过)...
_mangle_template = """
class {cls}:
@staticmethod
def mangle():
{attr} = 1
cls = {cls}
"""
def mangle_attr(source, attr):
# if source is an object, get the class
if not hasattr(source, "__bases__"):
source = source.__class__
# mangle attr
tmp = {}
code = _mangle_template.format(cls=source.__name__, attr=attr)
eval(compile(code, '', 'exec'), {}, tmp);
return tmp['cls'].mangle.__code__.co_varnames[0]
# NOTE: the '__code__' attr above needs to be 'func_code' for python 2.5 and older