在元类中使用匿名lambda的python范围问题

时间:2014-08-18 20:57:42

标签: python scope metaclass

我正在使用元类来为类定义只读属性(访问器方法),方法是为类声明的每个字段添加一个只有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

评论“工作方式”之后的行,并在“不通过的方式”之后取消注释三行:

    [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

显然,我不了解我正在创建lambdas的范围。

我一直在阅读Guido关于访问者元类的文档: https://www.python.org/download/releases/2.2.3/descrintro/#cooperation 以及Python数据模型和Python的Python文档 http://code.activestate.com/recipes/307969-generating-getset-methods-using-a-metaclass/ 使用元类创建访问器的方法, 最后我可以找到StackOverflow上的所有内容,但我只是没有得到它。

谢谢。

-Mike

3 个答案:

答案 0 :(得分:2)

问题与scope有关。使用

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

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

您可以通过meth内部__new__来定义 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。因此,当调用key时,它会在_getter的范围内找到key的值,其中_getter会保留_getter时获得的值被叫了。


如果您想使用lambda而不是key,可以使用meth = lambda self, key=key: self.__dict__[key] 的默认值:

meth

现在,在key内,meth本地变量。因此,当调用key时,key的值将是本地范围中meth的值。默认值绑定到 definition-time 处的函数,因此正确的值绑定到每个{{1}} lambda函数。

答案 1 :(得分:1)

这是python闭包的“后期绑定”的另一种表现形式,与元类无关;-) - 虽然可能是游戏中的元类使实际问题更难看...考虑:

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

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

现在在你的情况下,你发生了同样的事情,只有变量key。解决这个问题的一种简单方法是将值作为关键字参数绑定到函数中(在创建时进行评估):

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

或者,在您的情况下:

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

答案 2 :(得分:0)

关键词是动态范围。

这里很容易进入陷阱。

让问题更容易,忘掉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的情况下,而且发生在命名函数的情况下。