一、背景引入
函数作用域必须理解,不同语言对于函数作用域在有的细节上是不一样的,这个概念要明白,是理解装饰器的基础。
二、函数作用域
作用域的定义是,一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域。Python标识符只有变量,没有常量。只有字面常量。变量作用域很多。
比如 a=100,这种就属于全局作用域,代码一般写在最上面的,都是全局作用域。函数起名字,要写业务的英文名字。
a=100 #global 全局变量
def foo():
x=100 #局部作用域、本地作用域 local 局部变量
print(1,x)
print(x)
函数内部定义的变量称为局部作用域,比如x就是局部作用域,外部是不能访问的。x在当前函数的外部想访问是看不到的。
上面加入print(x),这段代码会报错的,抛出异常(NameError:name'x' is not defined),原因在于函数是一个封装,他会开辟一个作用域,x变量会限制 在这个作用域中,所以在函数外部x变量不可见。
注意:每一个函数都会开辟一个作用域
三、作用域的分类
全局作用域:
*在整个程序运行环境中都可见
*全局作用域的中的变量称为全局变量
局部作用域:
*在函数、类内部可见
*局部作用域中的变量称为局部变量,其使用范围不能超过其所在的局部作用域
*也称为本地作用域local
一般来讲外部作用域变量可以在函数内部可见,可以使用。反过来,函数内部的局部变量,不能在函数外部看到。对外不可见,对内穿透。
四、函数嵌套
在python当中呢,由于没有常量标识符,我们说的都是变量作用域。作用域是一个范围,当引入到函数之后,出现了作用域。每一个函数都开辟作用域标识的可见范围取决于定义。python中赋值即定义。
一个函数内部,又写入了另外一个函数,这个就是函数嵌套
def outer():
print('outer!!!')
def inner():
print('inner')
inner() #在outer函数中执行
print('outer ~~~~~')
定义和执行是两回事, inner,这个就是标识符,指向了一个函数对象.inner这个标识符是outer函数内部的标识符,所以依旧按照函数内部的局部变量理解。外部不可见,函数执行完,会释放。
def outer():
o=65
def inner():
print('{} in inner'.format(o),chr(o))
print('{} in outer'.format(o),chr(o))
inner()
outer()
这个函数运行结果是什么样子?
输出结果:
65 in outer A
65 in inner A
调用outer(),o是全局变量谁都能用,outer在前输出是因为,def inner()
只是定义了函数并未执行,inner的执行是在
print('{} in outer'.format(o),chr(o)),这句之后,所以才有了上面的
输出结果。
在进行分析代码
def outer1():
o=65 #对外不可见,对内可见,向内穿透
def inner():
o=0x61 #对外不可见面对内可见,向内穿透
print(1,o,chr(o)) #优先用自己作用域的,就近原则
print(2,o,chr(o))
inner()
print(3,o,chr(o))
outer1()
这个函数调用后会输出什么呢?
未上机实验,自己认为会这样输出,
2 65 A
1 97 a
3 97 a
但是上机运行之后发现运行结果如下:
2 65 A
1 97 a
3 65 A
自己的疑问在于,print(3,o,chr(o)) 这句代码为什么会输出 3 65 A,自己认为
代码执行到inner()这个函数内部的时候,o=0x61,会改变o的值,但是没有改变,
为什么没有改变呢?
两个作用域,一个是全局的,一个是局部的外部的o=65按道理来说inner函数内部能
见到这个o变量。在内层作用域inner中,如果定义了o=97,想当于在当前函数inner
作用域中重新定义了一个新的变量o,但是这个o不能覆盖掉外部作用域outer1函数中
的o。只不过对于inner函数来说,其只能可见自己作用域中定义的变量o了。
这句代码print(3,o,chr(o)),它是看不到inner函数内部的变量的,所以说,输出
的以然是3 97 A.
这里原则就是内部优先原则,全局变量和局部变量名相同的时候,内部优先。
python中要记住,赋值即定义
一个赋值语句的问题:
x=100
def foo():
print(x) #外部有没有穿透进来的x?
foo()
输出 100
自己内部没有的的时候,向外穿透去找变量x。自己没有,向外能找到就可以使用
del x 这样删除,这个本质就是引用计数-1,引用计数清零垃圾回收才彻底清除
del foo 这样的删除就是让标识符foo作废,但是foo指向的对象还不知道有没有
删除。
x=200
def foo()
y=x+1
print(y)
foo() 调用后会输出什么?
输出201
*******************************
x=300
def foo()
y=x+1
print(y)
x=x+1
print(x)
x是多少?
300
foo() 执行结果是什么?
自己认为会输出301 301 x和y都是301
但是实际上代码会报错,为什么呢?
输出错误代码 UnboundLocalError,没有绑定的本地变量的错误。
原因在于x=x+1,
碰到局部变量和全局变量同名字的时候,在依照局部优先的原则
修改代码来看一下
x=300
def foo()
x=400
y=x+1
print(y)
x=x+1
print(x)
foo()
输出 401 401
加了x=400,就能正常输出了
在举例
x=300
def foo():
y=x+1
print(y)
x=500
print(x)
print(x)
foo()
这两个输出多少?
自己认为输出 300
301 500
print(x)输出300没问题,但是在foo()函数调用的时候,直接报错了
y=x+1报错 UnboundLocalError
为什么y=x+1报错?
在python中赋值即定义,在当前作用域当中出现了代码谁等于谁,就相当于在本地
作用域当中定义了这个变量。这个是x=500,说明x就是当前作用域的变量。在由于
有内部优先原则,执行到y=x+1的时候,就认为应该是局部变量的x了,但是,由于
x还没完成初始化赋值,就进行使用,会直接报错的。
x=300按道理来说是要向内穿透的,但是在foo函数体内出现了x=500,此时整个函数
体内都认为,x是局部变量了,而不是使用外部x这个全局变量了。这样一来在这个函
数作用域内出现的x都是局部变量x了
y=x+1的报错信息是,使用在定义之前
只要在函数中出现,x= 语句,属于变量赋值,且此变量不加任何语句的修饰。那么次变量一定是当前函数的局部变量,在此函数中,所有的x,都是使用该x。
写赋值语言,一定要确定好变量在语句的作用域是什么
x=300
def foo():
x+=1
print(x)
foo()
函数调用会报错的,x=x+1,这里是重新定义了x,x是局部变量,但是由于x没有初始化
就直接使用了,所以报错。由于python中是赋值即定义,x+=1 实际是x=x+1,这个x
是重新定义的x,是局部变量了,而不是外部变量x了。
看x是不是本地变量,就看有没有出现赋值语言,一旦有赋值语言,就是局部变量。
在函数中写赋值语句,一定要清楚变量的作用域是什么
嵌套函数是装饰器的基础
五、globle语句
还是上面的代码
x=300
def foo():
x+=1
print(x)
foo()
现在我的问题是,我想使用外面的全局变量x,让x在foo函数内部完成加1操作该怎么办呢?
python提供了globle语句来实现,这个操作。
x=300
def foo():
globle x
x+=1 #x=x+1
print(x)
foo()
用globle来声明这个x,x就是全局变量了,这样程序运行之后x是301。
全局变量声明之后x+=1,就没有赋值即定义这个说法了
使用global 修饰的变量是全局变量
全局作用域中必须提前定义好这个变量
global使用原则:
*外部作用域变量会在内部作用域可见,但也不要在这个内部的局部作用域中直接使用,因为函数的目的就是为了封装,尽量与外界隔离。
*如果函数需要使用外部全局变量,请尽量使用函数的形参定义,并在调用传实参进行解决。
*一句话,不用global,学习它就是为了深入的了解变量作用域
六、闭包
自由变量定义:
未在本地作用域中定义的变量。例如定义在内层函数外的外层函数的作用域中的变量
闭包定义:
就是一个概念,出现在嵌套函数中,指的是内层函数引用到了外层函数的自由变量,就形成了闭包。很多语言都有这个概念,最熟悉的就是java Script
看下面代码,进一步理解闭包
def counter():
c=[0]
def inc ():
c[0]+=1 #c[0]=c[0]+1
return c[0]
return inc #有意为之,就是要返回函数,函数用foo变量记住,
#因为inc函数指向的空间不消亡,C闭包必须保留
有3个问题需要回答:
1.代码会出错吗?
2.有闭包吗?
3.有没有函数嵌套
foo=counter()
print(foo(),foo())
c=100
print(foo())
代码不出错,return返回的是inc的地址
有闭包 inc 用到了c
有函数嵌套
这三个语句输出什么?
想不到输出什么,不清楚这个return inc 会返回什么
代码会输出 1 2
3
怎么理解 foo=counter()
foo就是inc,但是counter已经调用结束,内部局部变量按道理要消亡
但是里面的return inc ,返回了这个函数的地址,引用计数加1。没有被
清零,所以这段函数inc可以被继续使用。
虽然inc变量名已经消亡,但是inc指向的地址空间,没有被删除,有foo指向了这
段空间。所以说这个inc()函数依然是可以使用的。
外层变量的c,内层用到了这个c,因为内层函数没有消亡,它inc所用到的外层函数
的c是不能消亡的。c这个标识符消亡了,但是c指向的地址空间没有消亡。还是由于
引用计数的问题,引用计数没有清零,所以C指向的这段空间依然保存下来了。
保存这段空间的操作就是所谓的闭包
counter内部产生两个局部变量 c=[0]和 inc, C这个局部变量指向了一个列表
[0]是在堆中产生,这两个标识符c和inc是在栈上产生的。所以函数执行结束后两
个标识符会消亡。
闭包:
foo指向的函数对象,要使用counter的C,C指向的列表不消亡,由这个不消亡的内存函数对象来保存这个列表这就是闭包。
七、nonlocal
nonlocal 不是我本地的
定义:将变量标记为不在本地作用域定义,而是在上级的某一级局部作用域中 定义,但不能是全局作用域中定义。
def counter(): #这个是python3中比较简单实现闭包的方式
c=0
def inc():
nonlocal c #这个C不是我本地的,向外就近原则
c+=1
return c
return inc
print(counter())
nonlocal c 不是我本地的,是我当前函数外层函数中的某一层上的c变量
但是,绝不能是全局变量
c=0 如果没有,语法肯定会报错,这就是用法
想使用外层函数变量c,就需要在内层函数用nonlocal 声明这个变量
f2()执行会报错,这个就是上面说到的,外部对内部可见,内部变量对外不可见就是说这个c+=1 是c=c+1,这个c是局部变量c,没有被初始化。不用列表是没有闭包这种情况的。
要想不报错就是对这个c变量前加nonlocal修饰,这样就能形成变量类型的闭包了。
这个计数器实现原理就这样
nonlocal也是解决闭包的一个方案,使用globle会打破函数的封装,尽量是不使用globle,nonlocal推荐使用。
八、默认值的作用域
看下面代码
def foo(x=1): 形参都是局部变量
x+=1 #x=出现了,x就是foo函数的局部变量
print(x)
print(foo())
上面的x+=1会报错吗?不会报错的。
形参都是局部变量
第二组函数
def foo1(y=[]):
y.append(1) #是局部变量,y参数不提供,就使用缺省值
print(y) 缺省值在哪里?保存在函数对象上
[]是引用类型
print(foo1())
print(foo1())
第一个会输出 [1]
第二个print输出 [1,1]
为什么会这样输出?
为什么第二次调用foo会输出[1,1]
y是一个局部变量,说明缺省值是同一个列表,
结论:
如果没有缺省值和有缺省值是不同的效果
foo1()这个函数名,会记录缺省值[]列表的地址
print(foo1([])) [1]
print(foo1([])) 输出 [1]
print(foo1([])) [1]
每次都传入了一个新列表,没使用缺省值,这个缺省值由函数对象记录
在函数内部,必须知道缺省值都是什么,上下两个函数,最大的不同在于形参的
类型是不一样的,下面是可变的引用类型
要区别foo1([]) 和foo1(),这两个调用有什么区别
查看函数对象上的属性:
fool.__defaults__
缺省值保存在了元组中,元组一个元素,是列表
def foo2(x='abc',y=123,z=[1,2]):
x+='~'
y+=100
z.append(100)
print(x,y,z)
foo2()
第一次输出 x='abc~' y=223 z=[1,2,100]
foo2()
输出 x='abc~' y=223 z=[1,2,100,100]
x和y是简单类型局部变量, z是列表,会保存地址的
元组和顺序有关,顺序对应形参缺省值的
缺省值如果是列表的话,这个列表的地址会不变。也就是每次调用,
这个列表的地址都是一个位置
foo2.__defaults__
输出('abc', 123, [1, 2, 100, 100])
一定要小心函数这里的引用类型
def foo3(x=1,*args,m=10,n,**kwargs):
print(x,m,n,args,kwargs)
print(foo3.__defaults__)
这里保存的默认值只有1,这个1是x=1.
print(foo3.__kwdefaults__)
{'m': 10}
这个关键字缺省值使用了字典,注意字典是可变的
def foo4(x=[]):
x+=[1]
print(x)
print(1,foo4())
print(2,foo4())
print(3,foo4())
输出
[1]
1 None
[1, 1]
2 None
[1, 1,1]
3 None
我们要告诉大家的是:
x+=1 和 x=x+1 不一样的
+= 就地修改; x=x+1生成一个新对象覆盖x
x+=[1] 就地修改 x.append()
x=x+[1] 生成一个全新的列表,覆盖x,x.extend()
不可变类型 +=是什么?
可变类型 +=又是什么?
a=list(range(2))
print(id(a))
a+=[10]
print(id(a),a)
a=a+[20]
print(id(a),a)
对于不可变类型元组来讲, 将+=转换为 谁=谁+谁
a+=(10,) 等价于a=a+(10,).
不可变类型没有就地修改的加法,相加之后生成一个全新的对象,
然后在覆盖原来的变量。
a=a+(20,)
x=1
x+=1 #整数不能就地修改,这个等价于 x=x+1
x='a'
x+='b' 输出'ab' 这是全新字符串
x= x+'b'