函数进阶:闭包、装饰器、列表生成式、生成器、迭代器

时间:2022-06-25 19:52:31

命名空间(又称“名称空间”): 存放名字的地方  (概念性的东西)

例如:变量x = 1, 1存放在内存中,命名空间就是存放名字x与1绑定关系的地方。

名称空间有3种:

  • locals:是函数内的(或者是locals所在的那一层的)名称空间|,包括局部变量和形参
  • globals:全局变量
  • builtins: 内置模块的名字空间

 不同变量的作用域不同就是由这个变量所在的命名空间决定的

 作用域即范围:

1. 全局范围: 全局存活,全局有效

2. 局部范围: 临时存活, 局部有效

 

查看作用域方法: globals(),locals()

 

作用域的查找顺序: LEGB

L: locals

E: enclosing (相邻的)

G:globals

B: builtins 

 

闭包:

def func():
n
= 10
def func2():
print('func2:', n)
return func2 # 没有执行func2, 只是把func2的函数名(func2的内存地址)

f
= func() #执行func函数, 此时func()得到的结果是func2的内存地址,即f就是func2的内存地址

f()
# 执行f, 由于f就是func2的内存地址,此时执行的就是func2函数

# 输出结果:
#
func2: 10

# 在函数(func)外部执行了函数内部的子函数(func2),而且子函数(func2)还能够调用其父级函数(func)作用域里面所有的值。 这种现象就是闭包

 

装饰器:

软件开发“开放-封闭” 原则:已经实现的功能代码不允许被修改,但可以被扩展。

另外,不要改变别人函数的调用方式

 

现在公司有一个网站模块功能如下:

def home():
print('---------主页----------')

def america():
print('--------欧美专区-------')

def japan():
print('--------日韩专区-------')

 

 

现在需要在america 和 japan这两个板块前加上以下登录认证功能:

login_status = False
def login():
info
= ['neo', 'abc123'] # 储存的用户信息
global login_status
if not login_status:
name_input
= input('用户名:')
password_input
= input('密码:')

if name_input == info[0] and password_input == info[1]:
print('登陆成功')
login_status
= True
else:
print('密码错误')
exit()
else:
print('用户已登录,通过检测')

 

 

现有如下几种方法可选: 

1. 直接在America和Japan模块里面加上login认证程序, 如:

def japan():
login()
# 把自己写的认证程序加到别人写好的模块里面
print('--------日韩专区-------')

 

这种方式违反“封闭”原则,pass。

2. 把america和japan这两个函数名当做参数传到login函数里面,如:

login_status = False
def login(func):
info
= ['neo', 'abc123'] # 储存的用户信息
global login_status
if login_status == False:
name_input
= input('用户名:')
password_input
= input('密码:')

if name_input == info[0] and password_input == info[1]:
print('登陆成功')
login_status
= True
else:
print('密码错误')
exit()
if login_status == True:
print('用户已登录,通过检测')
func()
#把需要认证的模块函数名传进来调用执行

def home():
print('---------主页----------')

def america():
print('--------欧美专区-------')

def japan():
print('--------日韩专区-------')

login(japan)
#需要认证时,就把函数名当做参数变量传给login函数

 

这种方式改变了原模块(如japan())的调用方式

 

3. 装饰器:

login_status = False
def login(func):
def inner():
info
= ['neo', 'abc123'] # 储存的用户信息
global login_status
if login_status == False:
name_input
= input('用户名:')
password_input
= input('密码:')

if name_input == info[0] and password_input == info[1]:
print('登陆成功')
login_status
= True
else:
print('密码错误')
exit()
if login_status == True:
print('用户已登录,通过检测')
func()
#把需要认证的模块函数名传进来调用执行
return inner #login()执行的时候,得到的结果只是 把inner的函数名(内存地址)返回给login(func)

def home():
print('---------主页----------')

def america():
print('--------欧美专区-------')

def japan():
print('--------日韩专区-------')

japan
= login(japan) # login(japan)的执行结果是得到了inner函数的内存地址,再赋值给前面的japan 变量后,变量japan就是inner函数的内存地址。
japan() # 此时执行japan函数其实执行的里面的inner函数,由于闭包的父级函数执行完后里面的变量内存并不会释放、子函数inner里面能够使用login函数里面的所有参数变量,所以inner里面的变量func执行的时候会向login函数调用其传入的先前的japan函数。 这就是装饰器的原理。



#装饰器的正规写法:
@login
def japan():
print('---------日韩专区--------')

# 1.两个函数组成一个闭包,装饰函数做父级,被装饰的函数放在子级函数里面,子级函数名return给父级
#
2. 被修饰的函数名作为参数传给父级函数
#
3.子级函数里面包括装饰代码(如:登录认证)和 被装饰的函数(如:japan())

 

利用非固定参数 *args和 **kwargs 传不固定个数的参数

login_status = False
def login(func):
def inner(*args, **kwargs):
info
= ['neo', 'abc123'] # 储存的用户信息
global login_status
if login_status == False:
name_input
= input('用户名:')
password_input
= input('密码:')

if name_input == info[0] and password_input == info[1]:
print('登陆成功')
login_status
= True
else:
print('密码错误')
exit()
if login_status == True:
print('用户已登录,通过检测')
func(
*args, **kwargs) #把需要认证的模块函数名传进来调用执行
return inner #login()执行的时候,得到的结果只是 把inner的函数名(内存地址)返回给login(func)

def home():
print('---------主页----------')

@login
def america(x,y,z):
print('--------欧美专区-------',x,y,z)

@login
def japan(sytle): # 由于inner中的形参是非固定参数*args和**kwargs,所以传给inner函数多少个变量都行
print('--------日韩专区-------',style)

japan(
'3p'’) # 此时japan(‘3p’)执行的是inner函数,由于原先的japan函数里面需要传一个变量,这个japan函数也需要传一个变量。变量传的顺序是: 新生成的这个japan函数 ---> inner函数 ---> inner里面的func函数(此时的func函数是原先的japan函数) ---> 原先的japan函数

 

 

带参数的装饰器:

login_status = False
def login(style): # 由于装饰器带参数,这里需要加一个形参
def outer(func): #这一层需要传入被装饰的函数名并利用return调用里面的inner函数
def inner(*args, **kwargs):
info
= ['neo', 'abc123'] # 储存的用户信息
global login_status
if login_status == False:
name_input
= input('用户名:')
password_input
= input('密码:')

if name_input == info[0] and password_input == info[1]:
print('登陆成功')
login_status
= True
else:
print('密码错误')
exit()
if login_status == True:
print('用户已登录,通过检测')
func(
*args, **kwargs)
return inner #把inner的内存地址返回给outer函数
return outer #把outer的内存地址返回给login函数

def home():
print('---------主页----------')

@login(
'weixin')
def america(x,y,z):
print('--------欧美专区-------',x,y,z)

@login(
'qq') # 这句话可分解成两步理解: 1. 先是运行login('qq'),此时得到的结果是 outer函数的内存地址 2. login('qq')变成outer的内存地址后,就变成了不带参数的装饰器的情况(例如前面分析的那一种)
def japan(style):
print('--------日韩专区-------')


japan(
'3p') #执行装饰器

# 注:
即使是多层闭包,最里层的变量也可以去它的爷爷级去调用参数。 哪怕是多层闭包,在程序彻底运行完前,参数变量的内存地址也不会释放,还可以被里面的各层函数调用。
 
 
 

 

生成器:

现有需求: 给列表 [0,1,2,3,4,5,6,7,8,9] 中的每个值加1

可以利用如下代码实现:

a = [0,1,2,3,4,5,6,7,8,9]

print(list(map(lambda x:x+1,a)))

 

 

还有一种新写法:

a = [0,1,2,3,4,5,6,7,8,9]

a
= [ i+1 for i in range(10)] # 这种写法就叫列表生成式

print(a)

# 输出结果:
#
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

 

列表生成式语法: 

[ 生成规则(三元运算) for i in 原先的列表  ] 

如:

a = [0,1,2,3,4,5,6,7,8,9]

a
= [ i if i<5 else i*i for i in range(10)] # for 后面可以循环任何iterables, 如字典、列表、字符串等, 通过前面的运算把结果放到一个列表(或元祖)里面,但不能放到字典里面

print(a)

# 输出结果:
#
[0, 1, 2, 3, 4, 25, 36, 49, 64, 81]

 

a = {'neo':'abc','alex':123}

b
= [a[i] for i in a ]

print(b)

# 输出结果:
#
['abc', 123]

 

生成器(generator):

列表生成式:   

a = [ i  for i in range(10)]

 

我们把上面的列表生成式改动一下:

a = ( i  for i in range(10))   # 把列表符号[]改成括号()   # 利用这种方式就变成了一个生成器

print(a)

# 输出结果:
# <generator object <genexpr> at 0x0000001885E70EB8> # 打印结果显示a是一个生成器(generator)的内存地址, 此时a里面没有任何东西,它只是一个算法,但它已经有了生成数据的能力,让a产生数据的方式是利用next()去调用a

print(next(a)) # 输出结果为: 0
print(next(a))  # 输出结果为: 1
print(next(a))  # 输出结果为: 2
print(next(a))  # 输出结果为: 3
print(next(a))  # 输出结果为: 4
print(next(a))  # 输出结果为: 5
print(next(a))  # 输出结果为: 6
print(next(a))  # 输出结果为: 7
print(next(a))  # 输出结果为: 8
print(next(a))  # 输出结果为: 9  # 每利用next()调用一次a,a都会向下产生一个数据,并且不能后退。每生产完一个数据,生成器就停在了这个位置,下次再调用生产时,它会接着前一次的数据继续向下一个生产。所以,每个数据只能生产一次。
print(next(a))  # 输出结果为:  报错,并显示“StopIteration”   #  所以 利用next的方式在调用完生成器里面的数据后会报错
 

 

 

生成器的特性:

1. 我想要什么数据它不会立即产生,我取一次它才创建一次

2. 生成器只能往前走(往下一个生产),不能往回退,原先生产过的值不可能再生产一次; 并且, 生产完数据后再利用next调用会报错

 

调用生成器一种是利用next(),还可以利用for循环调用。 例如:

a = ( i  for i in range(10))  

for i in a: # a为生成器内存地址
print(i)

# 输出结果:
#
0
#
1
#
2
#
3
#
4
#
5
#
6
#
7
#
8
#
9

# 利用for循环取生成器里面的地址,取完之后也不会报错,所以通常利用这种方式。

 

 

另外,next()可以手动调用生成器,也可以利用while调用,如:

a = ( i  for i in range(5))

while True:
print(next(a))

# 0
#
1
#
2
#
3
#
4
#
报错,StopIteration

# 注: next()调用生成器有两种方式: 1. 手动一个个调用; 2. 利用while调用。 这两种方式的本质都是在利用next(),所以调用完之后都会报错。

 

range() 底层就是用生成器实现的 (Python3中)

Python2中 range(10)直接就是个列表(立即生成)

a = range(10)
print(a)

# 输出结果:
#
[0,1,2,3,4,5,6,7,8,9] #在Python2中range()直接就是列表, 但在Python3中,range()只是一个生成器,一种算法,根本没有创建列表

# 在Python2中,xrange()的功能就相当于Python3中的range()
# Python3中已经没有xrange()了

 

 

利用函数实现斐波那契数列(前两数为1,后面的数等于前面的两个数之和):

# 需求: 打印斐波那契数列的第10个值:


def fib(max):
a
= 0
b
= 1
count
= 0
while count < max:
print(b)
a,b
= b,a+b # 这种写法要注意: 此时b赋值给a,是把原先b的值赋值给a;a+b赋值给b,是把原先a的值和原先b的值赋给b
count += 1
fib(
10)

# 输出结果:
#
1
#
1
#
2
#
3
#
5
#
8
#
13
#
21
#
34
#
55

 

 

上面的fib()函数稍作修改就能变成生成器:

def fib(max):
a
= 0
b
= 1
count
= 0
while count < max:
yield b # 只要加了yield,函数就会变成生成器。并且yield后面的对象(如:b)会return给调用fiber(max)函数的对象(例如:fiber(10)),只有在利用next()或for循环调用生成器的时候,fib()函数里面的代码才会一次次执行(就是说 不调用生成器的话,程序就永远冻结在了yield这一步,只有调用生成器(如next)它才会继续执行)。
a,b = b,a+b
count
+= 1
f
= fib(10) # 由于fib()函数里面有yield,所以fib(10)执行fib(max)函数时,函数内部的代码并没有任何的执行,只是把这个函数变成了一个生成器。 # 此时fib(10)的打印结果是:<generator object fib at 0x000000E6605D0EB8> # 需要把生成器的内存地址赋值给一个变量

print(next(f)) # 输出结果: 1
print(next(f)) # 输出结果: 1
print(next(f)) # 输出结果: 2
print(next(f)) # 输出结果: 3
print(next(f)) # 输出结果: 5
print(next(f)) # 输出结果: 8
print(next(f)) # 输出结果: 13
print(next(f)) # 输出结果: 21
print(next(f)) # 输出结果: 34
print(next(f)) # 输出结果: 55

 

 

总结1:

1. 原先函数只能在里面执行,你要是想要里面的执行结果只能return,一return整个函数就停止执行了;但生成器可以把函数里面执行的每一个值、每一个结果返回出来

2. 只要函数内部有yield, 函数名加()后,函数内部的代码根本不执行,只是生成了一个生成器对象

 

总结2:

生成器的创建方法:

1. 类似列表生成式的方法(但不是列表)

2. 函数内带有yield

这两种创建方式的区别: 类似列表生成式的方法最复杂只能处理一个三元运算

 

总结3:

Python2中:  

range = list

xrange = 生成器

Python3中:

range = 生成器

xrange 不存在

 

总结4: return vs yield

return: 返回数据,并终止函数

yield: 返回数据,并冻结函数当前的执行过程; 如果想要继续执行生成器,只能利用类似next()的方法再次唤醒程序。

def fib(max):
a
= 0
b
= 1
count
= 0
while count < max:
yield b
a,b
= b,a+b
count
+= 1
f
= fib(10)

for i in f:
print(i)

 

 

next() 的作用: 唤醒被冻结(如yield)的函数执行过程,直到遇到下一个yield。

注: 类似列表生成式的方法底层也是yield函数

 

循环读取文件的原理也是生成器:

f = open('test.txt','r')

for i in f: #在循环f的时候就是在循环一个生成器
print(i) # 循环一边就读取一行

 

 

注: next(生成器)的另一种写法:  生成器.__next__()    (next两个各有两个下划线)

 

只要函数里面有yield,它就会变成一个生成器,遇到return生成器就会终止,生成器就会报错:

def range2(n):
count
=0
while count < n:
yield count
count
+= 1

new_range
= range2(3)

print(next(new_range))
print(next(new_range))
print(next(new_range))
print(next(new_range))

# 输出结果:
#
0
#
1
#
2
#
报错, stopiteration

# 上面生成器中假如了return:

def range2(n):
return 33333
count
=0
while count < n:
yield count
count
+= 1

new_range
= range2(3)
print(next(new_range))

# 输出结果:
#
报错,StopIteration: 33333 # return让生成器终止执行,就会报错

 

 

所以,函数有了yield之后:

1. 函数名加()就得到了生成器

2. return在生成器里,代表生成器的终止,就会报错

 

生成器的send方法:

可以利用  “ 生成器.send( '指令 ' )  ” 的方法给生成器内部发送指令, 发送的指令是发送到了 yield那一句。

def range2(n):
count
=0
while count < n:
sign
= yield count # send的指令发送给yield语句
print('-----检测-----',sign)
count
+= 1

new_range
= range2(5)

print(next(new_range))
print(next(new_range))
print(new_range.send('test'))

# 输出结果:
#
0
#
-----检测----- None
#
1
#
-----检测----- test
#
2

# 注: 1. send发送的括号里面的内容不能为空
# 2. can't send non-None value to a just-started generator (大致意思就是说,你要是想用send方法调用生成器的第一次执行,send括号里面的内容必须为“None”(生成器起始的第1次并没有yield的等待)
# 3. 可以这么理解: next()这种调用方式的底层其实就是调用的send方法,只不过send的内容为“None”
# 4. next()只能唤醒程序执行,发送给生成器内部的指令只能是“None”; 而send不但可以唤醒生成器继续执行,而且可以给生成器内部发送任何指令


 

 迭代器:

 可直接作用于for循环的数据类型有:

1. 集合数据类型,如 list、tuple、dict、set、str等

2. generator,包括生成器和带yield的generator function

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。

可以使用 isinstance()判断一个对象是不是Iterable对象:

from collections import Iterable
print(isinstance(['a','b','c'],Iterable))

# 输出结果:
#
True

from collections import Iterable

print(isinstance((i for i in range(10)),Iterable))

# 输出结果:
#
True

 

 

定义:可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator  (生成器只是迭代器的一种)

可以用 isinstance()判断一个对象是不是Iterator对象:

from collections import Iterator

print(isinstance(['a','b','c'],Iterator))

# 输出结果:
#
False

from collections import Iterator

print(isinstance((i for i in range(10)),Iterator))

# 输出结果:
#
True

 

 

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。

把list、str、dict等Iterable变成Iterator可以使用iter()函数:

from collections import Iterator

print(isinstance(iter(['a','b','c']),Iterator))

# 输出结果:
#
True

a
= ['a','b','c']
b
= iter(a)
print(b)
print(a)

print(next(b))
print(next(b))

# 输出结果:
#
<list_iterator object at 0x0000004C9B7BA080> # 生成器内存地址
#
['a', 'b', 'c']
#
a
#
b # 可利用next()函数调用该生成器

 注: Python3中的for循环本质上就是通过不断调用next()函数实现的