【问题标题】:python scope issue with anonymous lambda in metaclass元类中匿名 lambda 的 python 范围问题
【发布时间】:2026-02-14 04:10:01
【问题描述】:

我正在使用元类来定义类的只读属性(访问器方法),方法是为类声明的每个字段添加一个只有一个 getter(一个 lambda)的属性。我发现不同的行为取决于我定义 lambda 的位置。如果我在元类的__new__ 方法调用的外部函数中定义getter lambda,而不是直接在元类的__new__ 方法中定义lambda,它会起作用。

def _getter(key):
    meth =  lambda self : self.__dict__[key]
    print "_getter: created lambda %s for key %s" % (meth, key)
    return meth


class ReadOnlyAccessors(type):

    def __new__(cls, clsName, bases, dict):

        for fname in dict.get('_fields',[]):
            key = "_%s" % fname

            # the way that works
            dict[fname] = property(_getter(key)) 

            # the way that doesn't
            # meth = lambda self : self.__dict__[key]
            # print "ReadOnlyAccessors.__new__: created lambda %s for key %s" % (meth, key)
            # dict[fname] = property(meth)

        return type.__new__(cls, clsName, bases, dict)


class ROThingy(object):
    __metaclass__ = ReadOnlyAccessors

    _fields = ("name", "number")

    def __init__(self, **initializers):
        for fname in self._fields:
            self.__dict__[ "_%s" % fname ] = initializers.get(fname, None)
        print self.__dict__


if __name__ == "__main__":
    rot = ROThingy(name="Fred", number=100)
    print "name = %s\nnumber = %d\n" % (rot.name, rot.number)

正如目前所写,执行如下所示:

[slass@zax src]$ python ReadOnlyAccessors.py
_getter: created lambda <function <lambda> at 0x7f652a4d88c0> for key _name
_getter: created lambda <function <lambda> at 0x7f652a4d8a28> for key _number
{'_number': 100, '_name': 'Fred'}
name = Fred
number = 100

注释掉“the way that works”之后的行并取消注释“the way that doesn't”之后的三行会产生这样的结果:

    [slass@zax src]$ python ReadOnlyAccessors.py
    ReadOnlyAccessors.__new__: created lambda <function <lambda> at 0x7f40f5db1938> for key _name
    ReadOnlyAccessors.__new__: created lambda <function <lambda> at 0x7f40f5db1aa0> for key _number
    {'_number': 100, '_name': 'Fred'}
    name = 100
    number = 100

请注意,即使rot.__dict__ 显示_name'Fred'name 属性返回的值也是100

显然,我对创建 lambda 的范围一无所知。

我一直在阅读 Guido 关于访问器元类的文档: https://www.python.org/download/releases/2.2.3/descrintro/#cooperation 以及 Python 数据模型的 Python 文档和这个 http://code.activestate.com/recipes/307969-generating-getset-methods-using-a-metaclass/ 使用元类创建访问器的秘诀, 最后是 * 上我能找到的所有内容,但我就是不明白。

谢谢。

-迈克

【问题讨论】:

    标签: python scope metaclass


    【解决方案1】:

    问题与scope 有关。当你用

    定义meth
    meth = lambda self : self.__dict__[key]
    

    key 变量不是meth 本地范围内的变量。所以当调用meth 函数时,必须在封闭范围内搜索key。 (参见LEGB rule。)它在__new__ 方法的范围内找到它。但是,在调用 meth 时,key 的值不一定是定义 methkey 的值。而key 的值是最后一个由于for-loop 而分配给它的值。它恰好总是'_number'。所以无论你调用什么meth,都会返回self.__dict__['_number']的值。

    您可以通过在__new__ 中以这种方式定义meth 来看到正在发生的事情:

        for fname in dict.get('_fields',[]):
            key = "_%s" % fname
    
            def meth(self):
                print(key) # See what `meth` believes `key` is
                return self.__dict__[key]
    

    产量

    _number    # key is always `_number`
    _number
    name = 100
    number = 100
    

    _getter 起作用的原因是因为key 被传递给_getter。所以当meth被调用时,它会在_getter的作用域中找到key的值,其中key保留了它在调用_getter时得到的值。


    如果您想使用 lambda 代替 _getter,您可以使用 key 的默认值来实现:

    meth = lambda self, key=key: self.__dict__[key]
    

    现在,在meth 内部,key 是一个本地 变量。所以当meth被调用时,key的值将是key在本地范围内的值。默认值在定义时间绑定到函数,因此正确的值绑定到每个meth lambda 函数。

    【讨论】:

      【解决方案2】:

      这是 python 闭包“后期绑定”的另一种表现形式,与元类无关 ;-) -- 尽管可能正在使用的元类使实际问题更难看到......考虑一下:

      funcs = [lambda: x for x in range(30)]
      print funcs[0]()  # 29!
      

      原因是 lambda 函数在调用时从闭包中查找值,而不是在创建时。在这种情况下,即使在创建第一个函数时 i0,但在调用它时,i 的值是 29

      现在,在您的情况下,您也发生了同样的事情,只是使用了变量 key。修复它的一种简单方法是将值作为关键字参数绑定到函数(在创建时进行评估):

      funcs = [lambda _x=x: _x for x in range(30)]
      

      或者,在你的情况下:

      meth = lambda self, _key: self.__dict__[_key]
      

      【讨论】:

        【解决方案3】:

        关键字是动态范围。

        这里很容易陷入陷阱。

        为了让问题更简单,忘掉OO吧,想想下面的代码:

        arr = []
        for i in range(5):
            arr.append(lambda: i)
        
        for lmb in arr:
            print lmb()
        

        还有这段代码:

        def lmb_gen(val):
            return lambda: val
        
        arr = []
        for i in range(5):
            arr.append(lmb_gen(i))
        
        for lmb in arr:
            print lmb()
        

        简单的答案是 lambda 中的 i 绑定到 for 循环中的 i,它在 lambda 被调用之前不断变化。这就是打印 5 4 的原因。

        而在第二个示例中,lambda 中的 val 绑定到参数 val,每次调用 lmb_gen 时该参数都会有所不同。因此,换句话说,环境不同。

        规则是,当一个变量没有在函数中定义时,该变量实际上绑定到第一个“外部”环境。

        这种现象不仅发生在 lambda 的情况下,也发生在命名函数的情况下。

        【讨论】: