python入门系列第十七篇: 作用域

489 阅读5分钟
原文链接: zhuanlan.zhihu.com
作用域法则


  • 内嵌的模块全是全局作用域
  • 全局作用域的作用范围仅限于单个文件
  • 每次对函数的调用都创建一个新的本地作用域
  • 赋值的变量名除非声明为全局变量或者非本地变量, 否则均为本地变量
  • 所有其他的变量名都可以归纳为本地的, 全局的, 内置的


变量名解析: LEGB原则

变量名引用的分为三个作用域进行查找: 首先是本地, 之后是函数内(有的话), 之后是全局, 最后是内置。

一个变量的作用域所属于它被赋值或者创建的位置。

如class, def. lambda等语句内赋值的变量都为本地变量。

python的变量名解析机制有时候我们称为: LEGB原则。

  • 当函数在使用未认证的变量时候, python会依次去搜索4个作用域, L(本地作用域), E(嵌套作用域), G(全局作用域), B(内置作用域)。 并且在第一次找到变量名之后就停止搜索, 如果一直没有找到会报错。


代码实例
# Global 作用域
x = 99  # 全局作用域

def func(y): # y: 本地作用域
    # Local 作用域
    z = x + y # x: 全局作用域
    return  z

func(1)

这种变量隔离机制的意义在于: 本地变量是作为临时的变量名, 只有在函数运行时候才需要它们, 执行完之后会被内存回收。


内置作用域

内置作用域指的是: 指的是一个名为__builtin__的内置模块。

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', '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', 'all', 'any', 'ascii', 'bin', 'bool', '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']
>>>

上述这个列表中的变量名就组成了python的内置作用域,概述的讲, 前半部分是内置的异常, 后半部分是内置函数。

LEGB法则最后就会去搜索这些内置作用域中的变量,因此不需要我们手动去导入。

另外有时候我们需要注意: 本地作用域会遮蔽同名的全局作用域。

def test():
    open = 'test'
    open('./demo.py') # TypeError: 'str' object is not callable

test()


x = 10 # global scope

def test():
    x = 9 # local scope
    return  x

print(test()) # 9
print(x) # 10


global语句
  • 全局变量是位于模块文件内部的顶层的变量名
  • 全局变量如果是在函数内被赋值的话, 需要先声明
  • 全局变量名在函数的内部不经过声明也可以被引用
x = 10 # global scope

def test_global():
    global  x
    x = 9

print(x) # 10


x = 10 # global scope

def test_global():
    global  x
    x = 9

test_global()
print(x) # 9


嵌套作用域


x = 10 # global scope

def test_nested():
    x = 9 # 嵌套作用域
    def f():
        print(x)
    f()

print(test_nested()) # 9


x = 10 # global scope

def test_nested():

    def f():
        print(x)
    f()

print(test_nested()) # 10


闭包

这里只是简单提一下闭包的概念。

闭包(closure): 一个能够记住嵌套作用域的变量值的函数。

def make(N):
    def action(X):
        return  X ** N
    return action

f = make(2)
print(f(3))               # 9
print(f(4))               # 16

# N就是由于闭包被记住的那个内嵌变量


使用默认参数来保留嵌套作用域的状态
def test():
    x = 88
    def func(x=x):
        print(x)
    func()

test()  # 88


lambda表达式:

def test_lambda():
    x = 4
    action = (lambda n: x ** n)
    return action

func = test_lambda()
print(func(2))                  # 16


循环下的作用域问题
def makeActions():
    acts = []
    for i in range(5):
        acts.append((lambda x: i ** x))
    return acts

acts = makeActions()
# acts里面保存的函数的i值都是一样, 都是4
print(acts[0](2))    # 16 = 4 ** 2
print(acts[0](3))    # 64 = 4 ** 3
print(acts[1](3))    # 64

出现上述的原因是: 嵌套作用域中的变量在嵌套函数被调用时候才开始查找。所以i记住的循环后的最后那个值。

解决上述的办法: 使用默认参数把当前的值传递给嵌套作用域中的变量

def makeActions():
    acts = []
    for i in range(5):
        acts.append((lambda x, i = i: i ** x))
    return acts

acts = makeActions()
# acts里面保存的函数的i值都是一样, 都是4
print(acts[2](2))    # 4 = 2 ** 2
print(acts[3](3))    # 27 = 3 ** 3
print(acts[4](3))    # 64 = 4 ** 3


nonlocal语句

nonlocal应用于一个嵌套的函数的作用域中的一个名称, 而不是def之外的全局作用域。

并且声明nonlocal名称时候, 它必须已经存在于该嵌套函数的作用域中。

或者说nonlocal允许对嵌套的函数作用域中的名称赋值, 并且把该名称的作用域查找限制在嵌套的def。

def func():
  nonlocal name1, namee2, ...


def tester(start):
    state = start
    def nested(label):
        print(label, state)
    return  nested

f = tester(0)
print(f('spam'))            # spam 0
print(f('ham'))              # ham 0

默认情况下, 我们是不可以去修改嵌套作用域中的名称。

def tester(start):
    state = start
    def nested(label):
        print(label, state)
        state += 1      # UnboundLocalError: local variable 'start' referenced before assignment
    return  nested

f = tester(0)
print(f('spam'))
print(f('ham'))

但是当我们在嵌套作用域中把state声明为nonlocal, 就可以修改它了。

def tester(start):
    state = start
    def nested(label):
        nonlocal state
        print(label, state)
        state += 1
    return  nested

f = tester(0)
print(f('spam'))    # spam 0
print(f('ham'))             # ham 1

note: 一定要记住, nonlocal名称必须要在一个嵌套的def作用域中赋值过。

def tester(start):
    def nested(label):
        nonlocal state      # SyntaxError: no binding for nonlocal 'state' found
        print(label, state)
        state += 1
    return  nested

f = tester(0)
print(f('spam'))
print(f('ham'))

其次, nonlocal作用会限制作用域查找仅在嵌套的def内。不会往上继续查找。

spam = 99

def tester():
    def nested():
        nonlocal spam                      # SyntaxError: no binding for nonlocal 'spam' found
        print('current=', spam)
        spam += 1
    return  nested

t = tester()
print(t())