Bootstrap

python中的各种作用域

声明:本文中大部分内容均在参考网址中,本人进行精简后,略加修改并加上一点自己的理解。

一、python中作用域

1.局部作用域,在函数内部或lambda、类中的全局局部变量中,调用函数时才会创建。每次调用都会创建一个新的本地作用域。调用结束后会销毁。(函数的参数也位于此作用域,这也解释了为什么不同函数,内部变量或参数名相同,并不引发冲突)。不可修改闭包作用域定义的变量,除非使用nonlocal语句。

2.闭包作用域(nonlocal),存在嵌套函数中,为其外层作用域。调用函数之后创建的变量不在此作用域

3.全局作用域,顶层模块中所有函数外创建的全局变量。在函数内不可被修改,除非使用global语句,使用这些语句,也能在局部作用域创建全局变量。不过最好避免这种做法

4.内置作用域,builtins包,由python自动导入,一般为内置函数。如果担心其中命名被全局作用域覆盖,可显式写出。

与global不同,不能在嵌套或封闭函数之外使用nonlocal,也不能用nonlocal来创建变量。

二、搜索顺序问题(LEGB)

这里有两类情况需要区分:
2.1 在变量引用时,python的搜索顺序是按照从上向下的顺序搜索(如果有的话),这也就是为什么如果你命名变量和python内置变量冲突时,会导致覆盖,从而引发错误。
2.2 而在变量赋值时,则只考虑本地局部作用域,这是因为在python中赋值即定义

abs = 20
abs(-15)	# 由于内置函数名被覆盖,此时会出错

# 1.第一种办法
import builtins
builtins.abs(-15)

# 2.第二种办法
del abs		# 从全局作用域删除其定义,因此可以访问内置作用域
abs(-15)	# 15 正常执行

注意:闭包会从最内层搜索到最外层(多层嵌套情况)

前两个作用域保证你的变量不会在很远的地方被修改,方便调试和维护。

def scope_test():
    spam2 = "outer2 spam"
    def do_test():
        def do_local():
            spam = "local spam"                         # 局部变量
            print("outer two:", spam2)                  # 最外层闭包作用域
            print( do_local.__code__.co_varnames )		# 输出局部作用域变量
            # print("after call:", spam3)               # 调用函数之后创建的变量不在闭包作用域中
        def do_nonlocal():
            nonlocal spam
            spam = "nonlocal spam"

        def do_global():
            global spam
            spam = "global spam"

        spam = "test spam"
        do_local()
        spam3 = "after call create"
        print("After local assignment:", spam)
        do_nonlocal()
        print("After nonlocal assignment:", spam)
        do_global()
        print("After global assignment:", spam)
    do_test()

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

上述输出为:

outer two: outer2 spam
('spam',)
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

三、闭包(Closures)和闭包作用域(Enclosing)

Closures会返回一个函数,其作用相当于冻结一些参数,已供后续使用(类似存款)。经常用于一些需要延后执行的场景。partial()函数及装饰器就是使用这个技术实现的。

def scope_test1():
    spam2 = "spam"
    def do_test():				# 其调用时机被延后,因此定义在外层的变量都可使用
        print(spam2)
        print("After:", spam3)
    spam3 = "after spam"		# 在定义之后的变量也可使用
    return do_test

mfun = scope_test1()
mfun()

其实据此可以分析出,Enclosing的变量必然是存储在do_test()函数中的,这些变量存储在func_name._ _ code _ _.co_freevars中

def scope_test1():
    spam2 = "spam2"
    def do_test():
        print(spam2)
        print("After:", spam3)
    spam3 = "after spam3"
    do_test()
    spam4 = "spam4"
    print( do_test.__code__.co_freevars )   # 属于闭包的自由变量
    print( do_test.__closure__ )            # 这些元素是cell对象
    print( do_test.__closure__[0].cell_contents )   # 这些元素具体的值
    return do_test
mfun = scope_test1()

上述输出为:

spam2
After: after spam3
('spam2', 'spam3')
(<cell at 0x000001FADFAB1888: str object at 0x000001FADF997CA8>, <cell at 0x000001FADFAB1798: str object at 0x000001FADFC21030>)
spam2

从这再次验证了调用函数之后创建的变量不属于函数自由变量,因此在闭包中也就找不到该变量。

四、不符合LEGB的特例

4.1.列表生成式

[item for item in range(5)]     # 类似局部作用域,一旦完成,内部变量销毁
# print( item )					# 会出错
# 与之对比
for item in range(5):
    pass
print(item)						# 输出4

4.2.exception variable

如下述代码,err变量只能在except代码块中使用,类似局部变量。而在except块中定义的其他变量是作为全局变量。要想在全局作用域使用err保存的信息,只能在except块中定义一个辅助变量ex,然后在外部引用ex变量。

mylist = [1, 2, 3]
try:
    mylist[4]
except IndexError as err:
    print(err)          # list index out of range
    ex = err			# 这样就可以在外面使用此信息了
    err = "change"
    print(err)          # change
    new_var = 5

print(new_var)          # 正常输出 5 
print(err)              # NameError: name 'err' is not defined
# -------------------------------
# 与之对比,with as语句并不会出现此问题
with open("score.txt", 'r') as f:
    f.read()
print(f)	# <_io.TextIOWrapper name='score.txt' mode='r' encoding='cp936'>

4.3.类和实例属性

class Myclass:
    attr = 50

a = Myclass()
b = Myclass()
a.attr = 100        # 仅改变自身
print( b.attr )     # 50
Myclass.attr = 100  # 会改变所有实例的该属性
print( b.attr )     # 100

# 例2
class Myclass:
    attr = 50				# class local scope
    def __init__(self, var):
        self.var = var		# 其为实例属性  instance local scope
a = Myclass(40)
b = Myclass(60)
print(a.var)            # 40
print(Myclass.var)      # __init__根本没执行,AttributeError: type object 'Myclass' has no attribute 'var'

# 以下为两者区别
print(a.__init__)       # <bound method Myclass.__init__ of <__main__.Myclass object at 0x000002B5D24F2BE0>>
print(Myclass.__init__) # <function Myclass.__init__ at 0x000002835EA687B8>

self是表示当前实例,也可从另一方面解释,即python中类的方法是与实例进行绑定的,因此没有实例就无法执行,除非是静态方法。

搜索顺序

1.搜索instance local scope,因此实例属性可覆盖类属性
2.class local scope
尽管类中也为局部作用域,但不适用闭包作用域,必须使用点。
实例属性覆盖类属性后,仍能通过类名.属性访问类属性。此时实例.属性返回的是实例属性。

class Myclass:
    attr = 50
    def __init__(self, var):
        # print("attr ", attr )       # 类似闭包作用域做法,NameError
        print("attr ", self.attr )    # 正确执行 或 使用Myclass.attr

a = Myclass(40)

参考

https://realpython.com/python-scope-legb-rule/#understanding-scope
https://docs.python.org/zh-cn/3/tutorial/classes.html
《流畅的python》

;