在Python 2中,我如何写入父作用域中的变量?

78

我有一些类似这样的代码:

def example():
    # other logic omitted

    stored_blocks = {}
    def replace_blocks(m):
        block = m.group(0)
        block_hash = sha1(block)
        stored_blocks[block_hash] = block
        return '{{{%s}}}' % block_hash

    num_converted = 0
    def convert_variables(m):
        name = m.group(1)
        num_converted += 1
        return '<%%= %s %%>' % name

    fixed = MATCH_DECLARE_NEW.sub('', template)
    fixed = MATCH_PYTHON_BLOCK.sub(replace_blocks, fixed)
    fixed = MATCH_FORMAT.sub(convert_variables, fixed)

    # more logic...

将元素添加到stored_blocks中运作良好,但我无法增加嵌套函数中的num_converted。我得到一个异常,说UnboundLocalError: local variable 'num_converted' referenced before assignment

我知道在3.x中,我可以尝试使用nonlocal num_converted,但我怎样才能解决2.x中的这个问题呢?我不想为此使用全局变量。


4
与某些普遍的看法相反(根据这类问题的判断),def 不是唯一定义命名空间的关键字:还有 class - Jochen Ritzel
6个回答

88
问题:这是因为Python的作用域规则不合理。存在+=赋值运算符意味着目标变量num_converted属于封闭函数作用域的局部变量,而在Python 2.x中,没有一种可靠的方法可以访问仅仅是一个嵌套层级外的变量。只有global关键字可以将变量引用从当前作用域提升到最外层。 修复方法:num_converted转换为单个元素的数组。
num_converted = [0]
def convert_variables(m):
    name = m.group(1)
    num_converted[0] += 1
    return '<%%= %s %%>' % name

7
为什么需要解释?我本来以为OP的代码能够正常工作。 - Björn Pollex
36
由于Python的作用域规则非常混乱,+=赋值运算符的存在会将目标num_converted标记为封闭函数作用域的局部变量。在Python 2.x中,没有一种可靠的方式可以访问仅限于当前作用域之外的任何一个层级。只有global关键字能够将变量引用提升到当前作用域之外,但它会直接跳到最顶层。 - Marcelo Cantos
6
这并不是聪明的代码,实际上它相当糟糕。有些类可以使用(请参见问题下面的注释)。该版本使用了一个全局变量,应该始终避免这样做。不使用"global"并不意味着您没有全局变量。 - schlamar
15
变量并非在任何意义上都是全局变量。原帖的开头句指出,所呈现的整个代码块都在一个函数内部。 - Marcelo Cantos
@MarceloCantos 哦,你说得对 :) 然而,在这种情况下,一个类仍然是更合适的选择。 或者像在这里中回答的那样,使用一个显式的命名空间对象,这样就清楚地知道发生了什么。 - schlamar
2
@schlamar:一个变量需要这么多的脚手架。如果你需要比我的答案更清晰的解释(真的吗?),我会倾向于使用 scope = {'num_converted':0} … scope['num_converted'] += 1 - Marcelo Cantos

29

(请参见下面编辑后的答案)

你可以使用类似以下代码:

def convert_variables(m):
    name = m.group(1)
    convert_variables.num_converted += 1
    return '<%%= %s %%>' % name

convert_variables.num_converted = 0

这样,num_converted 就像 C 语言中的 "静态" 变量一样,在 convert_variable 方法中起作用。


(编辑过)

def convert_variables(m):
    name = m.group(1)
    convert_variables.num_converted = convert_variables.__dict__.get("num_converted", 0) + 1
    return '<%%= %s %%>' % name

这种方法可以避免在主过程中初始化计数器。


3
好的。请注意,在定义函数之后,您必须创建属性convert_variables.num_converted,尽管这样做看起来很奇怪,但必须这样做。 - Marc van Leeuwen
@PabloG 给出了这个问题最令人满意的答案,除了在3.x中使用nonlocal之外;使用可变类型[]是一种廉价的解决方法。 - user2290820

9

使用 global 关键字是可行的。如果你写:

num_converted = 0
def convert_variables(m):
    global num_converted
    name = m.group(1)
    num_converted += 1
    return '<%%= %s %%>' % name
num_converted不会成为“全局变量”(即不会出现在任何其他意外的地方),它只是表示可以在convert_variables内部进行修改。这似乎正是您想要的。
换句话说,num_converted已经是全局变量了。所有global num_converted语法所做的就是告诉Python:“在这个函数内部,不要创建一个本地的num_converted变量,而是使用现有的全局变量。”

4
在 Python 2.x 中,global 的作用与在 Python 3.x 中的 nonlocal 相似。 - Daniel Roseman
2
换句话说,num_converted已经是一个全局变量了 - 我的代码正在函数内运行,因此它目前不是全局的。 - ThiefMaster
2
啊,我没有注意到“在函数内部”的部分,抱歉 - 在这种情况下,Marcelo的长度为一的列表可能是一个更好(但丑陋)的解决方案。 - Emile

7
使用类实例来保存状态怎么样?您可以实例化一个类并将实例方法传递给子函数,这些函数将具有对self的引用...

7
听起来有点过头了,像是由一个Java程序员提出的解决方案。;p - ThiefMaster
1
@ThiefMaster 为什么这是过度设计?如果你想要访问父级作用域,你应该在Python中使用一个类。 - schlamar
2
@schlamar 因为在其他支持一级函数的理智语言中(如JS、函数式编程语言),闭包只是正常工作的。 - Dzugaru

6

我有几点注释。

首先,在处理原始回调时,可以使用嵌套函数的一个应用程序,例如在xml.parsers.expat等库中使用的回调。 (尽管库作者选择了这种方法可能会引起反对,但是...仍然有理由使用它。)

其次:在类内部,有比数组(num_converted [0])更好的替代方案。 我想这就是Sebastjan所说的。

class MainClass:
    _num_converted = 0
    def outer_method( self ):
        def convert_variables(m):
            name = m.group(1)
            self._num_converted += 1
            return '<%%= %s %%>' % name

这仍然有些奇怪,需要在代码中进行注释... 但是变量至少是类的本地变量。


嘿,欢迎来到Stack Overflow - 但是在这里发布“评论”作为答案并不是你可以做的事情。我们有评论功能(但是,您需要一些声望才能发布它们 - 但是不要仅仅因为您还没有足够的声望来发表评论而发布答案)。 - ThiefMaster
6
你好,你也受欢迎!我不理解你的话或者你使用的几个术语。而且我很忙,只是想帮助一下! - Steve White
没问题 - 不过可以看看http://stackoverflow.com/about。虽然帮助别人总是受欢迎的,但是无论多好的评论都会最终被删除。 - ThiefMaster

0

修改自: https://dev59.com/Kpzha4cB1Zd3GeqPHJOe#40690954

您可以利用inspect模块来访问调用作用域的全局字典,并写入其中。这意味着即使在导入的子模块中定义了嵌套函数,这个技巧也可以被利用来访问调用作用域。

import inspect 

def get_globals(scope_level=0):
    return dict(inspect.getmembers(inspect.stack()[scope_level][0]))["f_globals"]

num_converted = 0
def foobar():
    get_globals(0)['num_converted'] += 1

foobar()
print(num_converted) 
# 1

根据需要使用scope_level参数。当函数定义在子模块中时,设置scope_level=1即可生效;当装饰器内部的函数定义在子模块中时,设置scope_level=2即可生效,以此类推。

注意:仅因为你可以这样做,并不意味着你应该这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接