基础概念
命名空间作用域闭包globalnonlocalglobalslocalsdir([object]) 深入理解
为什么命名空间三个作用域却四个?闭包被返回时, 外层函数命名空间已经销毁, 为什么之后还可以使用?谁可以产生作用域?如何理解修改built-in域? 后记
基础概念 命名空间名称到对象的映射,python中大多数用字典实现
built-in,内建global,全局local,局部
命名空间是有生命周期的, 简单说就是函数返回了就结束了
作用域python程序可以直接访问命名空间的正文区域
built-in,内建global,全局enclosing,闭包外且非全局的local,局部
读取变量时, 查找顺序肯定是由内向外的,局部没有,就找enclosing,再没有就找global,都没有找内建, LEGB
再没有就NameError, 如果其中一步有了, 那就停止寻找了
x = 'global'
def foo():
x = 'foo'
def boo():
print(x)
boo()
foo()
# foo
关于修改变量(这里说的是变量指向, 不是说可变对象的那个修改), 只能修改自身作用域范围内的变量, 内层和外层都不能修改
x='global'
def foo():
x = 'foo'
foo()
print(x)
# global
闭包
函数中定义函数,内层函数用到了外层函数的变量,这时内层函数就是一个闭包
实际上闭包很像对象,但是如果需要函数粒度的抽象,那就用闭包
global内层作用域直接修改global的变量,无论多内层他都能直接修改global
x='global'
def a():
x='a'
def b():
x='b'
def c():
global x
x = 'c'
print('c,x',x)
c()
print('b,x',x)
b()
print('a,x',x)
a()
print('global,x',x)
# 输出
"""c,x c
b,x b
a,x a
global,x c"""
nonlocal
修改enclosing作用域变量的值
def foo():
x='foo'
def doo():
nonlocal x
x = 'doo'
doo()
print(x)
foo()
# doo
globals
返回全局命名空间的映射,是个字典,貌似是可以改的, 经测试改了也有效果
globals()['xxx'] = 10 print(xxx) # 输出10locals
返回局部命名空间的映射,是个字典,别修改,改了也没用, 官方是这样说的
def foo():
locals()['xxx']=10
print(xxx)
foo()
# 报错 NameError: name 'xxx' is not defined
在模块级别用globals和locals返回同一个字典
dir([object])首先返回的是个列表
如果有参数, 尝试返回传入对象的有效属性列表如果没参数, 返回当前作用域中的名称列表
也就是可以这样
print(sorted(dir()) == sorted(globals())) # True
dir试图返回对象最相关的而不是最全面的信息, 主要是用在交互时用, 而不是用来保证结果的一致性
深入理解 为什么命名空间三个作用域却四个?命名空间和作用域是可以一一对应的, 为什么多出来一个enclosing区域
是因为nonlocal函数的原因, nonlocal可以修改local域外, global域内的, 这个区域其实是外层函数的local域, 为了区分, 将其称为enclosing
变量查找顺序是由内而外的, 如果有多个enclosing域, 也是由内向外, 而且nonlocal只能修改第一个包含该变量的enclosing作用域,例如
x='global'
def a():
x='a'
def b():
x='b'
def c():
nonlocal x
x = 'c'
print('c,x',x)
c()
print('b,x',x)
b()
print('a,x',x)
a()
print('global,x',x)
# 输出
"""c,x c
b,x c
a,x a
global,x global"""
c中的nonlocal x只能修改b中的x,a中的x是鞭长莫及的。加入b中没定义x那么c就可以修改a的x,例如
x='global'
def a():
x='a'
def b():
def c():
nonlocal x
x = 'c'
print('c,x',x)
c()
print('b,x',x)
b()
print('a,x',x)
a()
print('global,x',x)
# 输出
"""c,x c
b,x c
a,x c
global,x global"""
闭包被返回时, 外层函数命名空间已经销毁, 为什么之后还可以使用?
def foo():
x = 10
def boo():
print(x)
return boo
func = foo()
func()
上面这个典型的闭包例子, 当我们执行func = foo()时, foo的命名空间就消失了, x也没了, 但为什么之后执行func()时仍然可以print(x)?
闭包函数都有闭包属性, 即__closure__属性,发现是个tuple[cell],闭包用到的外层函数的变量,都有会有一个对应一个cell对象,而且cell对象的cell_contents属性,可以直接获取变量的值, 直接看下面代码
def foo():
x = 10
def boo():
print(x)
return boo
func = foo()
print(func.__closure__)
print(func.__closure__[0].cell_contents)
# (| ,)
# 10
|
所以, 虽然外层命名空间消失了, 但是被闭包用到的变量都通过cell对象维持与闭包的联系
每个被闭包用到的变量都有一个对应的cell对象,所以当有两个闭包,用同一个外层变量时,它们__closure__中的cell对象是同一个, 看下面的例子
def foo():
x = 10
def boo():
print(x)
def coo():
print(x)
print(id(boo.__closure__[0]) == id(coo.__closure__[0]))
foo()
# True
谁可以产生作用域?
经常说只有函数|类|模块会产生新的作用域, 代码块if|for|while等不会产生新的作用域, 实际上各种生成器表达式也会产生作用域, 可以试试下面这句
[print(locals()) for a in range(10)]如何理解修改built-in域?
运行这段代码, 我们会有种错觉, global作用域代码竟然修改了built-in作用域变量!
class Foo:
def __init__(self, *args, **kwargs): print('Foo')
set = Foo
def foo():
print(set([1, 2, 2, 3]))
foo()
print(set([2, 2, 2, 2]))
# Foo
# <__main__.Foo object at xxx>
# Foo
# <__main__.Foo object at xxx>
事实并非如此此, 我们只是在global命名空间添加了一个名为set的变量而已, 只是由于变量搜索顺序按照LEGB, 找到一个就停手, 所以当其他作用域使用set时, 搜索到global就得到了, 如果不去添加这个set变量, 平常都要搜索到built-in的, 看下面代码, 两次输出只多了一个set而已
print(locals()) set = 1 print(locals())
如果定义了跟built-in域一样的变量, 可以import builtins模块去使用真正built-in域的变量
import builtins
class Foo:
def __init__(self, *args, **kwargs): print('Foo')
set = Foo
print(builtins.set([1,2,2,2,3]))
# {1, 2, 3}
如何真正修改built-in域的变量呢? 我想大家应该已经知道了
import builtins
class Foo:
def __init__(self, *args, **kwargs): print('Foo')
builtins.set = Foo
print(set([1,1,1,1,1,2]))
print(builtins.set([1,2,2,2,3]))
# Foo
# <__main__.Foo object at 0x0000022E111C8FD0>
# Foo
# <__main__.Foo object at 0x0000022E111C8FD0>
后记
本文是在作者搜索闭包时顺带出来的一连串问题, 并根据官方文档和网上一些文章编写的, 不保证都对, 但都是跑了代码测试和经过思考的, 实际上这些函数和问题在日常编码中基本不会遇到, 但要是能梳理清楚了, 对理解Python肯定是有帮助的; 作用域方面的问题暂时只能想这么多, 如果有其他知识, 欢迎补充! 结束.
如有问题, 欢迎交流!



