python 如何使用setattr或exec创建私有类变量?

13 投票
3 回答
5216 浏览
提问于 2025-04-17 04:24

我遇到了一个情况,就是当使用 setattrexec 时,伪私有类成员的名字并没有被改变。

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 个回答

2

这是我目前的一个小技巧。如果你有更好的建议,欢迎分享。

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)
4

关于这个问题:

当Python遇到一个双下划线(self.__x)属性被设置时,它“背后”到底发生了什么?有没有什么神奇的函数来处理这个名字的变化呢?

据我所知,这个问题在编译器中是特别处理的。所以一旦代码变成字节码,名字就已经被改变了;解释器根本看不到原来的名字,也不知道需要特别处理。这就是为什么通过 setattrexec 或者在 __dict__ 中查找字符串时不起作用的原因;编译器把这些都当作字符串看待,并不知道它们和属性访问有什么关系,所以直接原样通过。解释器对名字的变化一无所知,所以它就直接使用这些名字。

每当我需要绕过这个问题时,我通常会手动进行同样的名字变化,虽然这样做有点 hacky。我发现使用这些“私有”名字通常不是个好主意,除非你确实知道需要它们来实现它们的目的:让类的继承层次结构都能使用相同的属性名,但每个类都有自己的副本。随便在属性名中加上双下划线,仅仅因为它们应该是私有的实现细节,似乎带来的麻烦比好处多;我现在更倾向于使用单下划线,作为一个提示,告诉外部代码不要去碰它。

11

我认为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

撰写回答