9

我刚刚遇到了一种情况,即私有类成员名称在使用setattror时不会被破坏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__它是一个只读属性。

有没有另一种方法可以动态创建这些私有类成员(在名称修改中没有硬编码)?


表达我的问题的更好方法:

当遇到设置的双下划线 ( self.__x) 属性时,python 在“幕后”做了什么?是否有用于进行修饰的魔术功能?

4

3 回答 3

7

我相信 Python 在编译期间会进行私有属性修改......特别是,它发生在它刚刚将源解析为抽象语法树并将其呈现为字节码的阶段。这是在执行过程中,VM 唯一知道函数在其(词法)范围内定义的类的名称。然后它会破坏伪私有属性和变量,并保持其他所有内容不变。这有几个含义......

  • 特别是字符串常量不会被破坏,这就是为什么你setattr(self, "__X", x)被单独留下的原因。

  • 由于修改依赖于源中函数的词法范围,因此在类外部定义然后“插入”的函数不会进行任何修改,因为关于它们“所属”类的信息在编译时是未知的.

  • 据我所知,没有一种简单的方法可以确定(在运行时)函数在哪个类中定义......至少在没有大量inspect调用依赖源反射来比较函数之间的行号和类来源。即使这种方法也不是 100% 可靠,也有可能导致错误结果的边界情况。

  • 这个过程实际上是相当不雅的修饰 - 如果您尝试访问不是__X该函数在其中定义函数的类的实例的对象上的属性,它仍然会为该类修饰它......让你将私有类属性存储在其他对象的实例中!(我几乎认为最后一点是一个特性,而不是一个错误)

因此,变量重整必须手动完成,以便您计算重整的 attr 应该是什么才能调用setattr.


关于 mangling 本身,它是由_Py_Mangle函数完成的,它使用以下逻辑:

  • __X得到一个下划线和前置的类名。例如,如果它是Test,则损坏的 attr 是_Test__X
  • 唯一的例外是如果类名以任何下划线开头,则这些下划线将被删除。例如,如果类是__Test,则损坏的 attr 仍然是_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
于 2011-10-17T04:47:45.750 回答
4

解决这个问题:

当遇到设置的双下划线 ( self.__x) 属性时,python 在“幕后”做了什么?是否有用于进行修饰的魔术功能?

AFAIK,它在编译器中基本上是特殊情况。因此,一旦它在字节码中,名称就已经被破坏了;解释器根本看不到未修改的名称,也不知道需要进行任何特殊处理。这就是为什么通过setattr,exec或在中查找字符串的引用__dict__不起作用的原因;编译器将所有这些都视为字符串,并且不知道它们与属性访问有任何关系,因此它不会改变它们。解释器对名称修饰一无所知,所以它只是直接使用它们。

我需要解决这个问题的时间,我只是手动完成了同名的修改,就像那是一样的。我发现使用这些“私有”名称通常不是一个好主意,除非在这种情况下您知道您需要它们来实现其预期目的:允许类的继承层次结构都使用相同的属性名称但有一个副本每班。仅仅因为它们应该是私有的实现细节而使用双下划线的属性名称似乎弊大于利;我已经习惯只使用一个下划线来暗示外部代码不应该接触它。

于 2011-10-17T05:14:25.867 回答
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)
于 2011-10-16T05:45:59.940 回答