【问题标题】:python How to create private class variables using setattr or exec?python 如何使用 setattr 或 exec 创建私有类变量?
【发布时间】:2011-12-08 14:16:36
【问题描述】:

我刚刚遇到了一种情况,在使用 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__ 是只读属性。

是否有另一种方法可以动态创建这些 psuedo-private 类成员(无需在名称修改中进行硬编码)?


更好地表达我的问题:

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

【问题讨论】:

  • 这有点不寻常,因为您允许构造函数分配任意私有变量。如果构造函数可以给这些变量任何值,为什么还要将它们设为私有呢?您可以使用具有默认值的命名关键字参数来分配这些吗?
  • @MichaelAaronSafyan 这就是我最初所做的,但后来我想扩展 T 以获取任何 kwarg,并考虑在 init 但不允许 S 访问这些成员中的任何一个(因为 S 在调用 T.__init__(self, **kwargs) S 之前可以看到 kwargs 可能会破坏东西)。
  • 此时,我仍在编写代码,所以我想看看这是否可行,如果不可行,我可能会改用 def __init__(self, x=1, y=2): 之类的东西。
  • 来自文档:“请注意,传递给 exec、eval() 或 execfile() 的代码不会将调用类的类名视为当前类;这类似于global 语句,其效果同样仅限于字节编译在一起的代码。同样的限制适用于 getattr()、setattr() 和 delattr(),以及直接引用 dict 时。”这说明了为什么 execsetattr 不起作用......虽然我不知道解决方案。
  • 错位名称在函数的代码对象中硬编码。我尝试使用compile,但它保留了未损坏的名称。

标签: python attributes private-members name-mangling


【解决方案1】:

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

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

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

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

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

所以变量重整必须手动完成,以便您计算重整的属性应该是什么才能调用setattr


关于 mangling 本身,它是由 _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

【讨论】:

  • 哇,你找到了真正的 C 函数,它可以对名称进行修改 (_Py_Mangle)!这正是我一直在寻找的。谢谢伊莱!!
  • 我仍然不确定我是否还会尝试将这些属性设为私有。我将不得不填写更多代码,看看什么效果最好,但如果我这样做了,我将尝试这段代码和来自@eryksun 答案的代码,看看什么效果最好。这段代码作为一个函数看起来更便携,但感谢大家的好答案!它帮助了很多。
【解决方案2】:

解决这个问题:

当遇到双精度时,python 在“幕后”做了什么? 设置下划线 (self.__x) 属性?有没有神奇的功能 那是用来做修饰的吗?

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

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

【讨论】:

  • 这真是个好东西本!我惊讶地发现不是解释器在进行名称修饰,而是编译器。感谢您提供的重要信息。
【解决方案3】:

这是我到目前为止的 hack。欢迎提出改进建议。

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)

【讨论】:

  • 这是一种有趣的方法!乍一看,对于我的剧本的未来读者来说,这似乎会使事情变得过于复杂。不过我会修改它,谢谢!
  • @chown:我希望你能找到一种不那么“hacky”的方式来解决它,但我确实设法在没有硬编码的情况下得到了修改,以防它在未来发生变化——但不太可能那是。由于修饰是由编译器本身创建的,我不知道您是否会找到更好的方法。
猜你喜欢
  • 2021-02-01
  • 2017-01-20
  • 2015-12-20
  • 1970-01-01
  • 1970-01-01
  • 2013-07-03
  • 1970-01-01
  • 2011-10-21
  • 1970-01-01
相关资源
最近更新 更多