一、背景引入

函数作用域必须理解,不同语言对于函数作用域在有的细节上是不一样的,这个概念要明白,是理解装饰器的基础。

二、函数作用域

作用域的定义是,一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域。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'