命名空间与作用域

浏览 1590

课文

命名空间

命名空间为 namespace 的直译。从名字上就很容易理解,命名空间为存储命名的空间。听起来有点绕,没关系。

我们先来理解什么是命名。

num = 5
name = 'xiaoming'

以上,我们简单地定义了两个变量,便等同于创建了两个名字与对象的对应关系,这种建立名字与对象映射关系的行为便是 命名

字典就是一个名字与值对应的典型例子,这使得 python 中的命名空间通常用字典实现。

print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000286D12E9610>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '.\\learn.py', '__cached__': None, 'num': 5, 'name': 'xiaoming'}

调用 globals 方法便可返回 全局命名空间 ,打印结果可以看到全局命名空间是一个字典对象,同时包含了我们刚刚定义的两个变量。

包含 全局命名空间 在内有以下三种命名空间:

  • 局部命名空间 Local namespace:函数中定义的命名,包括函数的参数。
  • 全局命名空间 Global namespace:当前模块中的命名,与其他模块中 import 进来的命名。
  • 内置命名空间 Built-in namespace:Python 语言内置的命名,比如函数名 print、type 等等关键字。

image

a = 1 # a 处在全局命名空间

def func():
    b = 2 # b 处在局部命名空间

print(__name__) # __name__ 处在内建命名空间

内建命名空间

所谓的内建命名空间便是包括了 python 自带的一些变量与函数,比如 dict,list,type,print 等等。这些都是由 python 默认从 builtins 模块导入。

我们可以配合 dir 函数打印一下 __builtins__ 模块的内容,便可以看到内建命名空间中的内容。

print(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', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', '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']

全局命名空间

可以简单地理解最外一层定义的变量或其他模块导入的变量都存在于全局命名空间中。

上面已经提到用 globals 函数可以看到当前全局命名空间中的内容。

a = 1

print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002641A0B9610>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '.\\learn.py', '__cached__': None, 'a': 1}

可以看到除了最后我们定义的变量 a , 还有一些默认导入的变量。

局部命名空间

程序当前执行的代码块内的命名都属于局部命名空间。

locals 函数可以看到当前局部命名空间中的内容。

a = 1

def func():
    b = 2
    print(locals())

func()
{'b': 2}

如果在最外层代码中调用 locals , 此时它与 glocals 输出的内容是等价的,因为当程序执行到最外层代码时,局部命名空间等同于全局命名空间。

a = 1

def func():
    b = 2
    print(locals())

func()

print(locals())
print(globals())
{'b': 2}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000277EBA69610>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '.\\learn.py', '__cached__': None, 'a': 1, 'func': <function func at 0x00000277EB976280>}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000277EBA69610>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '.\\learn.py', '__cached__': None, 'a': 1, 'func': <function func at 0x00000277EB976280>}

命名空间的作用

命名空间包含了从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。

命名空间可以将我们各部分的命名独立,避免了它们之间的冲突。你在命名一个变量时不再需要翻遍整个项目来防止命名冲突导致影响其他部分的代码。

a = 1

def func():
    a = 2
    print('局部命名空间的a:', a)

func()
print('全局命名空间的a:', a)
局部命名空间的a: 2
全局命名空间的a: 1

以上的两个 a 处于不同的命名空间,它们之间互不影响,在局部命名空间的命名不会影响到全局命名空间的命名。

作用域

我们了解到命名空间是可以独立存在的,并且它们以一定的层次来构建,这使得我们能以一定范围引用我们的变量。

限定变量名字的可用性的区域就是这个名字的作用域,并且作用域会直接决定我们引用一个变量时查找区域的顺序。

我们看一个简单的例子:

a = 1  # 全局作用域

def func():
    b = 2  # 闭合作用域

    def func_2():
        c = 3  # 局部作用域

print(__name__)  # __name__ 处于内建作用域

在这段代码中 a, b, c, __name__ 分别处于 全局作用域闭合作用域局部作用域内建作用域

一个函数内如果还定义有函数,则当前函数为闭合作用域,否则为局部作用域

作用域有四种:

  • 局部作用域 Local:最内层的函数 (deflambda)。
  • 闭合作用域 Enclosing:如果 A 函数内定义了一个 B 函数,那么相对 B 函数内的变量 A 函数内的变量就处于闭合作用域。
  • 全局作用域 Global:最外部定义的变量
  • 内建作用域 Built-in:内建模块内定义的函数与关键字

python 寻找一个变量的顺序是:LEGB,也就是 局部→闭合→全局→内建。

image

# 全局作用域
a = 1

def func():
    # 闭合作用域
    b = 2
    def func_2():
        c = 3
        # 局部作用域
        print('全局作用域的 a:', a)
    func_2()

func()
全局作用域的 a: 1

在 func_2 函数中打印 a 变量时,先查找局部作用域并未查找到,接着查找闭合作用域同样未找到,最后在全局作用域中找到 a 变量并打印。

需要注意的是,只有模块、类以及函数才会引入新的作用域,其他的代码块(if、for、while、try 等等)是不会引入新的作用域的。

a = 1  # 全局作用域

for i in range(10):
    a = i

print('全局作用域的 a:', a)
全局作用域的 a: 9

此时的 a 依然在 for 循环中被修改。

变量隐藏

依据作用域寻找命名的顺序,我们要注意我们内部定义的变量会隐藏掉外部变量。

a = 1  # 全局作用域

def func():
    a = 2  # 局部作用域
    print('局部作用域的 a:', a)

func()
# 函数内的修改并未影响到全局作用域的 a
print('全局作用域的 a:',a)
局部作用域的 a: 2
全局作用域的 a: 1

由于我们在函数内又定义了 a 变量,使得全局作用域的 a 变量被隐藏,我们就无法再访问全局作用域的 a 变量了。因此开发时如果还需要访问外层的变量,还需有意使用不同的变量名。

同理,我们在函数内的赋值语句会创建一个新的命名 a 在局部作用域中,不会影响到全局作用域下的 a 变量。如果我们有意修改外部变量就必须视情况使用 globalnonlocal 关键字。

global 和 nonlocal关键字

如果我们想在函数中修改全局作用域中的值,就只会在局部作用域中生成一个新的命名。

这会就需要global 关键字,它使得我们可以修改全局作用域内的变量。

a = 1 # 全局作用域的 a

def func():
    global a # 引入了全局作用域的 a 变量
    print('修改前的 a:', a)
    a = 2
    print('修改后的 a:', a)

func()
print('最终的 a:', a)
修改前的 a: 1
修改后的 a: 2
最终的 a: 2

nonlocal 关键字同理,它允许我们修改闭合作用域内的变量。

a = 1  # 全局作用域的 a

def func():
    a = 2  # 闭合作用域的 a 变量

    def func_2():
        nonlocal a  # 引入了闭合作用域的 a 变量
        print('修改前的 a:', a)
        a = 3

    func_2()
    print('闭合作用域的 a:', a)

func()
print('全局作用域的 a:', a)
修改前的 a: 2
闭合作用域的 a: 3       
全局作用域的 a: 1

func_2 在执行时引用了闭合作用域内的 a 变量,因此修改的是闭合作用域内的 a 变量。

有一种常见的坑我们需要避免,看以下代码。

# 全局作用域
a = 1

def func_1():
    # 在局部作用域定义 a 变量
    a = 2

def func_2():
    # 引入全局作用域的 a 变量并重新赋值
    global a
    a = 2

def func_3():
    # 尝试对局部作用域的 a 变量进行自增操作
    # 由于此时 a 变量未定义则报错
    a += 1

func_1()
func_2()
func_3()
Traceback (most recent call last):
  File ".\learn.py", line 20, in <module>
    func_3()
  File ".\learn.py", line 16, in func_3
    a += 1
UnboundLocalError: local variable 'a' referenced before assignment

python 会将在 func_3 的 a += 1 解释为尝试对局部作用域的 a 变量进行自增操作,由于此时 a 变量未定义会导致报错。

命名空间与作用域的区别与联系

命名空间是一种实际的代码实现,而作用域是程序设计上的一种规定。

一个变量处在哪个命名空间则决定了它的作用域,而它的作用域则表示了它的作用范围与被引用时所查找的顺序优先级。

我们可以把命名空间与作用域理解成部门与责任范围。

一个人身处全球事业部(命名空间),则他的责任范围便是全球市场(作用域)。

一个人身处北京事业部(命名空间),则他的责任范围便是北京市场(作用域)。

我们很容易把命名空间与作用域的概念搞混,因为他们之间本身就联系紧密。实际的学习与开发中我们也无需严格地区分它们。

评论

登录参与讨论

江逐天月

该内容已删除

2021-04-12

回复

共 1 条
  • 1
前往
  • 1

课程目录

第一课:命名空间与作用域 第二课:闭包 第三课:装饰器 第四课:迭代器 第五课:生成器

学习遇到困难?微信扫码进入社群与小伙伴一起交流讨论。