python-命名空间和作用域

前言

前段时间写脚本的时候,在调用函数的时候,一直在想在python中函数的参数是传值还是传引用?先看一下下面两个例子

a = 1

def setvalue(arg):
    arg = 100
    print(arg)
    
setvalue(a)
print(a)

这个例子中,会发现最后打印出的 a 仍然是 1 ,看起来像是传值调用。再看另一个例子:

a = [1]

def setvalue(arg):
    arg[0] = 100
    print(arg[0])

setvalue(a)
print(a[0])

这个例子的结果 a 中的数值已经修改成了 100,这样看又是传引用。熟悉C语言的可能在这里会说,这里传的是地址,所以修改有效。当然可以这么理解,但是在 python 中,对待变量与赋值需要换一个角度去理解。这也是我学python遇到的第一个难点,涉及到了python的namespacescope,想要学好python是必须要弄懂的。

一切皆对象

python 最大的特点也是最核心的思想,以前就提到过就是: 一切皆对象 。在说 namespacescope 之前,先说一下python中给变量赋值的原理。

a = 1   #1
a = 2   #2
b = a   #3
del a   #4

在 python 中,所有的int,string,list,dict,函数和类等等等等,我们都把它看成是一个对象

比如上面的 1 的意思,正确的理解是 a 引用了 1 这个对象或者将 a 绑定给了 1 ,,而不是将 1 这个值赋给 a 。深入点说,就是 1 这个对象的所有属性包括值都只存在自身,a 只是在 1 上打了个标签,并没有将实际的数据拷贝到自身!你只是可以通过命名 a 能够访问到 1 这个对象的信息。而如果执行了上面语句 4 ,含义就是在对象上删掉 a 这个标签,在通过 a 访问时就会报a命名未定义的错误。

语句 2 会删除掉a1上的标签,触发python的垃圾回收机制,然后语句 3 会将 b 也绑定到2这个对象上。

再回到最先的例子中,用刚谈到的对象概念去重新理解。例子1中,在调用函数 setvalue 时,只是将参数 arg 也绑定到 a 所引用的对象 1,但是在 setvalue 函数中,删除掉了 arg 在对象 1 上的标签,重新绑定到了对象 100 上,对 a 是没有影响的。
在例子2中,arg 也绑定到 [0] 这个列表对象上。函数中的操作仍然是对 a[0] 这个对象的操作,所以结果肯定是同时影响到已经绑定到 a[0] 上的所有变量的。

Consider

消化完上面的内容之后,再看一个例子

a = 1

def setvalue():
    a = 100
    
setvalue()
print(a)

简单的加一句声明之后

a = 1

def setvalue():
    global a
    a = 100
    
setvalue()
print(a)

简单的解释一下,因为在第一个例子中,函数中的 a 是局部变量,作用域只在函数内,所以不影响函数外的命名空间。第二个例子中,使用了 global 关键字,作用是将函数内对 a 的操作影响扩展到全局,所以函数外的结果收到了影响。

Namespace

Definition

命名空间的定义:变量到对象的映射集合。一般都是通过字典来实现的。主要可以分为三类:

  • 内置命名空间
  • 函数的本地命名空间
  • 模块的全局命名空间

对于模块的全局命名空间,因为有着模块名的前缀,所以互相是没有影响的。比如模块A和B都有c变量,那么通过A.c和B.c来使用是不冲突的。这三种命名空间也有着自己的生存周期,除了第二个函数的本地命名空间生存周期只在函数的调用开始到结束,其他两个的生存周期都是可以看做持续到解释器退出的。

作用

命名空间的作用:程序在直接访问变量时,会在当前的命名空间内查找。

比如:你在程序内执行 x = A 时,就会在当前的命名空间增加x到A的映射。如果使用del x 会删除掉命名空间中x的命名。

这里稍微拓展一下,import导入模块的本质,就是将其他模块的类、函数、变量等对象加入到程序的命名空间。再谈的深一点,python 中类的继承也是通过命名空间来实现的!这个下次笔记再说。。跑偏了

Scope

作用域 就是一个 Python 程序可以直接访问命名空间的正文区域。也可以理解是多种层级命名空间的叠加作用。在一个python程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。可以具体分为以下四个作用域:

  • Local(innermost)
    包含局部变量,比如一个函数/方法内部。
  • Enclosing
    包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,内层函数可能搜索外层函数的namespace,但该namespace对内层函数而言既非局部也非全局。
  • Global(next-to-last)
    当前脚本的最外层,比如当前模块的全局变量。
  • Built-in(outtermost)
    Python builtin 模块。包含了内建的变量/关键字等。

一个命名的作用域在它初次定义的时候确定,如果在当前作用域找不到命名时,会到外层作用域中寻找相应的命名,最后回到全局作用域和内置作用域中寻找。也就是按照LEGB的顺序来寻找一个命名对应的对象。

概念很抽象,具体来看代码

  1. 作用域如何生效的例子:
def test():
    b = 200
    def test2():
        b = 100
        print(b)
    test2()
    print(b)

b = 300
print(b)

函数 test2() 中访问b时,在本地作用域率先找到 b=100 这条语句,所以由100这个对象起作用。在 test() 中,它的本地作用域中b的引用为200,不会去 test2() 中去搜索。如果删除掉 test2()b=100的语句,那么test2函数调用时,在本地作用域找不到b的引用,会向上级enclosing作用域寻找,成功找到b的引用200,所以200在test2函数中生效!

  1. 作用域在定义时生效:
def test():
    print a
    a = 100

a = 200
test()

这个例子会报错,因为在 test() 函数中,因为本地作用域有命名a的引用操作,所以 print a 会优先使用本地作用域的命名。但是定义在使用之后,所以会报错。

事实上,所有引入新命名的操作都作用于局部作用域。

官方文档这句话的意思,这里的局部作用域要以相对角度去理解。

global、nonlocal

通过 globalnonlocal 两个关键字,可以将内层的变量引入到全局作用域。终于讲到了之前那个例子,弄懂了命名空间和作用域的基础上,就能很简单的理解那个例子了。

看具体的例子:

def scope_test():
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"
    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

输出是

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

需要注意的是,python2中并不支持 nonlocal 关键字来重新关联作用域。

Globals() Locals()

这两个方法是关于返回作用域的函数,前者返回全局作用域。后者返回当前的本地作用域。用这两个函数可以帮助理解namespacescope,直接贴上我写的一段代码:

#!usr/bin/env python
# -*- encoding: utf-8 -*-

import os
from sys import argv

class test:
    pass

def test2(arg1,arg2):
    print(locals())
    return arg1 + arg2

def test3():
    b = 2
    def test4():
        b = 200
        return b
    test4()
    print(locals())

a = 1
b = 100
test2(a,b)
test3()
print(globals())

输出结果:

{'arg2': 100, 'arg1': 1}
{'test4': <function test3.<locals>.test4 at 0x02AB36A8>, 'b': 2}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00F1A310>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'python.py', '__cached__': None, 'os': <module 'os' from 'E:\\python\\lib\\os.py'>, 'argv': ['python.py'], 'test': <class '__main__.test'>, 'test2': <function test2 at 0x00EDD540>, 'test3': <function test3 at 0x02AB36F0>, 'a': 1, 'b': 100}
  • 第一个字典记录的是test2()函数的本地作用域,可以看到只有两个参数的命名。
  • 第二个字典是test3()函数的本地作用域,有一个函数对象和一个命名。
  • 第三个是这个py文件的全局作用域信息,除了py文件中定义的函数、类、变量等,还有一些特殊变量__name__ __doc__ 内置模块 __builtins__ 我们引入的 os 模块也是作为一个模块对象,但是不同的是通过 form sys import argv 来引入的 argv 已经和原模块脱离联系,我们已经在本py文件中重新拷贝了一份。

推荐阅读更多精彩内容