python 中的变量作用域(variable scope)

一般来说,python 中包含如下四种变量作用域

  • local:函数本地作用域
  • enclosed:闭包作用域
  • global:全局作用域
  • built-in:内建类型作用域
  • 当 python 解析器遇到某一个变量时,它就会以如下优先级确定这个变量定义在哪里。我们可以看出,这遵循了一种 “自发现位置开始由内到外” 的规则。

    local > enclosed > global > built-in
    

    1. local 作用域

    def foo():
        x = 1
        print(locals())
        y = 2
        print(locals())
    
    foo()
    

    图中的 xy 都是 local 作用域的变量。

    他们只存在于 foo 函数中,这意味着当 foo 函数执行结束时,他们就无法被再次使用,并且可能会被垃圾回收掉(除非他们成为闭包变量 – enclosed)

    并且要注意的是,每当 foo 函数被执行一次时,xy 都会被创建一次,包括每次递归。

    注意到代码中的 print(locals()) 部分,他可以实时的查看到当前作用域中有多少本地变量。上面的函数输出为

    {'x': 1}
    {'x': 1, 'y': 2}
    

    2. enclosed 作用域

    def outer():
        x = 1
        y = 'b'
    
        def inner():
            return x, y
    
        return inner
    
    
    i = outer()
    print(i())
    print(i.__closure__)
    

    这是一个典型的闭包定义,在 inner 中引用了外层函数的局部变量 xy
    这样的结果是,xy 将一直维持在内存中,供 inner 函数使用。

    注意到 print(i.__closure__) 这一行代码,这是一种获取一个函数在使用的闭包变量的途径。

    上面的函数输出如下,我们可以看到,这个函数对一个 int 类型的变量和一个 str 类型的变量维持了引用(即 xy

    (1, 'b')
    (<cell at 0x1065633a0: int object at 0x1064000f0>, <cell at 0x1065625f0: str object at 0x10651f6b0>)
    

    2.1 回顾作用域的强度比较

    回到最开始的作用域强度讨论,由于 local 强于 enclosed,下面的代码将使用 inner 函数的本地变量,而非闭包变量

    def outer():
        x = 1
        y = 'b'
    
        def inner():
            x = 'b'
            y = 2
            return x, y
    
        return inner
    
    
    i = outer()
    print(i())
    print(i.__closure__)
    
    # 输出如下
    # ('b', 2)
    # None
    

    2.2 闭包值的存储:再深入一些闭包

    我们注意到,每个闭包都是以 cell 对象的形式存储的,而不是直接存储了变量的引用。让我们通过以下代码来理解 cell 提供的作用

    def outer():
        x = 1
    
        def inner():
            return x
    
        print('before', inner.__closure__)
        x = 2
        print('after', inner.__closure__)
        return inner
    
    
    i = outer()
    print(i())
    
    # 输出如下
    # before (<cell at 0x1050cb1f0: int object at 0x104f640f0>,)
    # after (<cell at 0x1050cb1f0: int object at 0x104f64110>,)
    # 2
    

    我们首先观察到 inner 函数中的 x,在 outer 中进行修改时,也会跟随者发生变化。

    仔细观察 before 位置和 after 位置对 __closure__ 值的打印,我们发下 cell 的地址并没有发生变化,而其中存储的 int 地址却发生了改变。

    这意味着 cell 的作用是提供了对外层函数中闭包变量的一个容器。当真实值发生改变时,python 解释器会将容器中的值进行修改。

    3. global 作用域

    a_global_var = 1
    
    
    def foo():
        print(a_global_var)
    
    
    foo()
    print(foo.__globals__)
    

    上面代码的输出如下。global 也即全局变量。

    值以字典的形式,存储在一个函数的 __globals__ 属性中。

    1
    # 我格式化了 json 输出
    {
    	'__name__': '__main__', 
    	'__doc__': None, 
    	'__package__': None, 
    	'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x107d25000>, 
    	'__spec__': None, 
    	'__annotations__': {}, 
    	'__builtins__': <module 'builtins' (built-in)>, 
    	'__file__': '这里我修改过', 
    	'__cached__': None, 
    	'a_global_var': 1, 
    	'foo': <function foo at 0x107ce0040>
    }
    

    实际上,当 python 解释器第一次遇到一个陌生的函数的时候,他会将当前可见的全局变量(可以通过内置函数 globals() 获取)复制到函数的 __globals__ 属性中。让我们看一下如下代码

    a_global_var = 111
    
    
    def foo():
        print(a_global_var)
    
    
    print(globals())  # 输出 ...'a_global_var': 111...
    print(foo.__globals__)  # 输出 ...'a_global_var': 111...
    foo()  # 输出 111
    
    a_global_var = 222
    
    print(globals())  # 输出 ...'a_global_var': 222...
    print(foo.__globals__)  # 输出 ...'a_global_var': 222...
    foo()   # 输出 222
    
    print(id(globals()))  # 输出 4473497280
    print(id(foo.__globals__))  # 输出 4473497280,说明两者是同一个对象
    

    a_global_var 被修改为 222 时,我们发现 globals()foo.__globals__ 中存储的 a_global_var 都被修改了。

    进一步通过打印 id 值我们会发现,实际上 globals()foo.__globals__ 指向了同一个变量。

    3.1 关于全局变量的更加复杂的例子

    当引入其他模块时,通常是可以引入其他模块中定义的全局变量的。但是如果引入其他模块的全局变量和当前脚本定义的全局变量名称冲突了会怎么样呢?

    bbbbb.py

    a_global_var = 'a_global_var in bbbbb.py'
    no_name_conflict = 'no_name_conflict in bbbbb.py'
    

    aaaaa.py

    from bbbbb import *
    
    a_global_var = 'a_global_var in aaaaa.py'
    
    
    def foo():
        print('In aaaaa', a_global_var)
        print('In aaaaa', no_name_conflict)
    
    
    foo()
    

    不出意料的,会输出

    In aaaaa a_global_var in aaaaa.py
    In aaaaa no_name_conflict in bbbbb.py
    

    我们发现 a_global_var 来自于 aaaaa.py 脚本。那么是否说明引入模块的全局变量优先级就是会较低呢?让我们将 aaaaa.py 修改为如下代码

    a_global_var = 'a_global_var in aaaaa.py'
    from bbbbb import *
    
    
    def foo():
        print('In aaaaa', a_global_var)
        print('In aaaaa', no_name_conflict)
    
    
    foo()
    

    其输出变为了

    In aaaaa a_global_var in bbbbb.py
    In aaaaa no_name_conflict in bbbbb.py
    

    这实际很好理解

  • 当运行 a_global_var = ... 时刻,会将 a_global_var 添加进入 globals() 对应的字典中
  • 当运行 from bbbbb import * 时,会将 bbbbb 中可见的变量都复制到当前的 globals() 对应的字典中
  • 所以他们的顺序决定了谁最后被更新,就用谁的数据!

    这也提醒我们

  • 尽量避免使用全局变量,当不得不使用其他文件的某个全局变量时,通过 模块.全局变量 方式的使用是更加安全的选择。
  • 注意即便是 import 语句位置的修改,也可能造成不可预期的问题。
  • 4. built-in 作用域

    built-in 作用域相对简单,是一些 python 内置的属性和方法。如下:

    # print(dir(globals()['__builtins__']))
    ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 
    'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 
    'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 
    'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 
    'EncodingWarning', 'EnvironmentError', 'Exception', 'False', 
    'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 
    'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 
    'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 
    'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 
    'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 
    'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 
    'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 
    'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 
    'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 
    'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 
    'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 
    'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 
    'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 
    'ZeroDivisionError', 
    '__build_class__', '__debug__', '__doc__', '__import__', 
    '__loader__', '__name__', '__package__', '__spec__', 
    'abs', 'aiter', 'all', 'anext', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 
    'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 
    'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 
    'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 
    'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 
    'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 
    'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 
    'sum', 'super', 'tuple', 'type', 'vars', 'zip']
    

    在实际开发中需要注意的一点是,应该避免和 built-in 作用域中的变量重名,否则容易造成一些预料不到的异常,例如一个简化的错误例子如下:

    def foo(id):
        x = 1
        # do something
        print(id(x))
        # do something else
    

    在这个例子中,因为输入变量覆盖了系统的 id 函数,导致 id(x) 处可能报错,或者无法按照预期输出。

    关于 nonlocalglobal 关键字

    nonlocal 关键字用来提示 python 解释器某个变量应该从非本函数变量中找到引用。

    需要注意的是,nonlocal 只能用在 “嵌套” 的函数中,也就是上面所说的闭包;而不能访问 global 作用域的变量

    让我们从下面问题代码出发,来学习 nonlocal 关键字的使用

    def outer(y):
    
        def inner():
            y = y + 5  # UnboundLocalError: local variable 'y' referenced before assignment
            print('y =', y)
    
        return inner
    
    
    i = outer(1)
    i()
    i()
    

    这段代码会报错 UnboundLocalError: local variable 'y' referenced before assignment,因为虽然在在 enclosed 的作用域下,我们可以读取 y 的值,但解释器会先看到 y = y + 5 等号左边的值,这意味着要给一个 local 作用域的变量赋值。

    更清晰的来说,对于 y python 解释器必须要界定他的作用域范围,而由于他先检测到赋值操作,所以将 y 界定成了 local 作用域;当他开始处理 y + 5 时,会尝试读取 local 作用域的 y 值,结果发现在赋值前被引用了(referenced before assignment)。

    实际只需要将 y = y + 5 修改为 x = y + 5,就不会报错了。而更加正确的修改办法,是使用 nonlocal 关键字,用来启发 python 解释器:y 是一个非 local 作用域的变量。

    def outer(y):
    
        def inner():
            nonlocal y
            y = y + 5
            print('y =', y)
    
        return inner
    
    
    i = outer(1)
    i()  # 输出 y = 6
    i()  # 输出 y = 11
    

    global 的用法和机制和 local 类似,在一个函数中,被 global 修饰的变量,会提示 python 解释器应该在全局变量中查找,而不是 local 作用域。

    仿照上面讲解 nonlocal 时的例子,我们也很容易构造如下会报错的写法。

    x = 5
    
    
    def foo():
        x = x + 5  # UnboundLocalError: local variable 'x' referenced before assignment
        print(x)
    
    
    foo()
    

    总结

    本文介绍了关于 python 变量作用域和他们的一些存储机制,方便大家在遇到相关 bug 时第一时刻能想到原因所在。

  • local:函数本地作用域
  • enclosed:闭包作用域
  • 闭包的存储机制:cell
  • global:全局作用域
  • 需要格外注意模块引入时的全局变量,很容易产生意想不到的 bug
  • built-in:内建类型作用域
  • 注意避免覆盖内建函数和类型
  • nonlocalglobal 关键字
  • 作者:小郎碎碎念

    物联沃分享整理
    物联沃-IOTWORD物联网 » python 中的变量作用域(variable scope)

    发表回复