装饰器
-
闭包
介绍装饰器前先了解一下闭包,在Python中,一切皆对象(Object),函数(Function)也不例外,也是一个普通的对象,函数可以作为一个参数传给其它函数,函数的返回值也可以是一个函数,而这种相关参数和变量都保存在返回的函数中的形式,就称为闭包(Closure),如下示例
def my_closure(): # 使用函数嵌套,先定义一个外部函数 num0 = 1 # 在外部函数中定义一个常量 def my_out(num1,num2): # 再定义一个内部函数,并动态传参,此参数外部无法调用 res = num0 + num1 # 再定义内部函数变量,并且使用外部函数中的变量 res1 = num2 - num0 print(res,res1) # 输出结果 return my_out # 返回内部函数,这种方式就是闭包函数 output = my_closure() # 获取闭包对象,赋值给output output(2,5) # 执行闭包,传参为2和5,输出结果为3和4
闭包的作用是使参数和变量一直保存在内存中,不会被垃圾回收机制收回
此处简单介绍一下函数中
global
和nonlocal
的区别,了解二者的作用域nonlocal是设置为局部变量,必须在嵌套函数中使用,必须先在外部函数定义变量,再在内部函数使用nonlocal声明,表示二者是同一个变量,所以只会修改上一层函数中定义的变量,nonlocal在闭包函数中经常出现
global是设置为全局变量,可以用在最上层函数和嵌套函数中,即使之前未定义变量,修饰后也可以直接使用,在嵌套函数中,不能修改上一层函数中的变量,若只有一个函数就会修改外部变量的值
如下示例
# nonlocal n = 0 # 定义一个全局变量n,并设置初始值为0 def func(): # 定义一个外部函数func() n = 1 # 在外部函数中再定一个n=1的局部变量,对于func1()函数而言,n=1就是外部变量,不能直接使用 def func1(): # 再定一个内部函数func1() nonlocal n # 设置n为局部变量,同样只会改变当前作用域func1函数中n的值,但,是将其赋值给n=1,而不是n=0 n = 2 # 定义一个局部变量,因为设置了n为局部变量,所以程序运行时就可以使用n=1,将1改为2,然后开始执行函数 for i in range(1,101): n += i # 计算n = n+i func1() # 执行内部函数func1() print("局部变量输出:",n) # 因为在func1()函数中的变量已通过nonlocal设为局部变量,参与了运算,所有输出为n = 2+5050 func() # 再调用func() print("全局变量输出:",n) # 因为n=0未参与运算,所以直接输出为0
# golbal n = 0 # 定义一个全局变量n,并设置初始值为0 def func(): # 定义一个外部函数func() n = 1 # 在外部函数中再定一个局部变量n,并设置初始值为1 def func1(): # 再定一个内部函数func1() global n # 设置n为全局变量,它只会改变当前作用域func1函数中n的值,并赋值给n=0,不会改变n=1 n = 2 # 定义一个局部变量,因为设置了n为全局变量,所以程序运行时就是将n=0设置为n=2,然后开始执行函数 for i in range(1,101): n += i # 计算n = n+i func1() # 执行内部函数func1() print("func1()输出:",n) # 因为n=1未参与运算,所以直接打印结果为1 func() # 再调用func() print("func()输出:",n) # 因为n=2替换了n=0,然后参与了func1()函数中的运算,最终计算结果n = 2+5050
-
简单装饰器
装饰器(Decorator)实质上也可理解为一个闭包函数,当只有一个函数类型的参数时,就可称为装饰器,否则就称为闭包函数,如下示例
import time # 导入time库 def run_time(func): # 先定义一个外部函数,func是需要传入装饰器的函数对象 def wrapper(): # 再定义一个内部函数 t1 = time.time() # 获取被调用函数运行前的时间 func() # 被调用的函数在此处运行,如下面定义的even_nums()函数 t2 = time.time() # 获取被调用函数运行后的时间 print("运行时间:{:.2}s".format(t2 - t1)) # 两时间相减得到实际运行时长,保留3位小数 return wrapper # 返回内部函数执行结果 @run_time # 使用@符号调用函数时被称为装饰器,目的是不改变even_nums()函数的前提下,给他增加计时功能 def even_nums(): # 定义一个普通函数 for i in range(1, 20000):# 使用for循环输出20000以内的所有偶数 if i % 2 == 0: print(i) even_nums() # 执行even_nums()函数,同时会输出此函数执行时长
-
带参数的装饰器
import time # 导入time库 def run_time(func): # 先定义一个外部函数,func是需要传入装饰器的函数对象 def wrapper(*args,**kwargs): # 再定义一个内部函数,允许传入多个位置参数和关键字参数 t1 = time.time() # 获取被调用函数运行前的时间 res = func(*args,**kwargs) # 被调用的函数在此处运行,允许传入任意参数,将被调函数执行结果赋值给res t2 = time.time() # 获取被调用函数运行后的时间 print("运行时间:{:.2}s".format(t2 - t1)) # 两时间相减得到实际运行时长,保留3位小数 return res # 返回被调函数执行结果 return wrapper # 返回内部函数执行结果 @run_time # 使用装饰器,等价于even_nums=run_time(even_nums) def even_nums(maxnum): # 定义一个普通函数 for i in range(1, maxnum): # 动态传参,使用for循环循环输出偶数 if i % 2 == 0: print(i) @run_time # 使用装饰器 def names(zs,ls,ww): # 输出对应的中文名称 time.sleep(0.2) # 响应太快,为了显示时间,刻意加上固定等待0.2秒 print(f"打印中文名:{zs},{ls},{ww}") # 输出中文名 even_nums(20000) # 执行even_nums()函数,传入位置参数,同时会输出此函数执行时长 names(ls="李思",ww="王武",zs="张珊") # 执行names()函数,传入多个关键字参数,同时会输出此函数执行时长
打印日志也可以使用装饰器,示例如下
import logging # 导入logging库 logger = logging.getLogger() # 定义日志并设置名称然后赋值给logger logger.setLevel(logging.DEBUG) # 设置日志为DEBUG级别 # ↓定义日志格式,输出格式为:当前时间 - 日志等级 - 函数名 - 日志信息 format = logging.Formatter("%(asctime)s - %(levelname)s - %(funcName)s - %(message)s") yd = logging.StreamHandler() # 输出日志记录 yd.setFormatter(format) # 输出日志使用定义的format格式 logger.addHandler(yd) # 日志输出到控制台 def logging(func): # 定义一个外部函数 def wrapper(*args,**kwargs): # 定义一个内部函数,运行传入任何参数 res = func(*args,**kwargs) # 被调用的函数在此处运行,允许传入任意参数,将被调函数执行结果赋值给res # ↓调用定义的日志,输出INFO等级的日志,格式为:“当前时间-INFO-wrapper-执行函数名,参数值,响应结果” logger.info(f"执行函数:{func.__name__},参数:{args}{kwargs},响应结果:{res}") return res # 返回被调函数执行结果 return wrapper # 返回内部函数执行结果 @logging # 每个函数都可以调用装饰器 def add_num(x, y): # 定义函数 return x+y # 返回xy相加结果 @logging # 使用装饰器 def power(i, n): return i**n # 返回i的n次方结果 @logging # 使用装饰器 def concat(a,b,c,d): return a+b+c+d # 拼接abcd add_num(3, 5) # 执行函数 power(n=2,i=3) concat("2","a",d="3",c="b")
-
带参数的类装饰器
class cldecorator(object): # 定义一个类 def __init__(self, output): # 初始化实例对象,增加out变量,初始值为output self.out = output def __call__(self, func): # 使实例对象可以作为函数调用 def wrapper(*args, **kwargs): # 定义一个函数,允许传入任何参数 res = func(*args, **kwargs) # 被调用的函数在此处运行,允许传入任意参数,将被调函数执行结果赋值给res print(f"{self.out}:{res}") # 打印初始变量和被调用函数执行结果 return res # 返回被调用函数执行结果 return wrapper # 返回wrapper函数执行结果 @cldecorator(output="计算结果") # 使用装饰器,并传入参数 def add_subtract(x, y): # 创建普通函数 return x - y # 返回参数相减结果 add_subtract(6, 4) # 执行add_subtract()函数,响应结果为“计算结果:2”
装饰器的作用是为了在不改变现有函数的情况下,给该函数增加额外的功能,同时提示代码的简洁性、易读性及执行效率
迭代器
在for循环背后有两个核心概念,可迭代对象(iterable)和叫迭代器(iterator),先熟悉一下这两个概念
若一个对象只实现了__iter__方法,但未实现__next__方法,那么就是可迭代对象,而非迭代器
若一个对象既实现了__iter__方法,又实现__next__方法,那么既是可迭代对象,也是迭代器
from collections.abc import Iterator, Iterable
my_list = [1, 2]
print(f"判断my_list是否是可迭代对象:{isinstance(my_list, Iterable)}")
print(f"判断my_list是否是迭代器:{isinstance(my_list, Iterator)}")
print(f"查看my_list的方法:{dir(my_list)}")
from collections.abc import Iterator, Iterable
my_list1 = [1, 2].__iter__() # 生成my_list1迭代器对象
print(f"判断my_list1是否是可迭代对象:{isinstance(my_list1, Iterable)}")
print(f"判断my_list1是否是迭代器:{isinstance(my_list1, Iterator)}")
print(f"查看my_list1的方法:{dir(my_list1)}")
print(my_list1.__next__()) # 使用__next__方法逐个取值
print(my_list1.__next__())
print(my_list1.__next__()) # 第三次取值时,因为无值可取,抛出StopIteration异常
使用for在循环时,循环内部是先执行__iter__方法,获取其迭代对象,然后在内部执行这个迭代器对象的__next__方法,逐个取值,最后无值可取,会break跳出循环,所以在使用for循环获取内部数据时才看不到有异常抛出
my_list1 = [1, 2].__iter__() # 生成my_list1迭代器对象
for i in my_list1:
print(i)
此处[1, 2].__iter__()
就是迭代器,在for循环中,此位置可能是可迭代对象(iterable)、也可能是迭代器(Iterator)、也可能是生成器(Generator)
生成器
如下示例,通过查看返回结果,先了解一下return和yield的区别
def return_odd(maxnum): # 创建一个函数
for i in range(maxnum): # 获取奇数
if i%2 == 1:
return i # 使用return返回
odd = return_odd(5) # 参数为5,执行函数并将结果赋值给odd
print(odd) # 打印odd的值
print(dir(odd)) # 打印odd的方法
def yield_odd(maxnum): # 创建一个函数,这个因为使用了yield,实际这个就是生成器函数
for i in range(maxnum):
if i%2 == 1:
yield i # 使用yield返回
odd = yield_odd(5) # 参数为5,执行函数并将结果赋值给odd,odd就是生成器对象
print(odd) # 打印odd的值
print(dir(odd)) # 打印odd的方法
从结果中可以看出,return是直接返回表达式的值,并且执行到return函数就结束啦,而yield返回的是一个生成器对象,返回的方法中包含__iter__方法和__next__方法
获取生成器中的数据,转换成列表展示,或者使用for循环获取每个数据
def yield_odd(maxnum):
for i in range(maxnum):
if i % 2 == 1:
yield i
odd = yield_odd(5)
print(list(odd)) # 转成列表展示
for i in odd: # 此处的odd就是生成器
print(i)
迭代器与生成器的区别,如下示例
# 迭代器
my_list1 = [0, 1].__iter__()
for i in my_list1: # 获取迭代器中的数据和对应内存中地址
print(i, id(i))
print("\n")
# 生成器
def my_generator(maxnum):
for i in range(maxnum):
yield i
print(list(my_generator(2)))# 结果转换成列表展示
for i in my_generator(2): # 获取生成器中的数据和对应内存中地址
print(i, id(gen))
通过示例结果可以看出:
- 迭代器是对可迭代对象中所有的数据进行遍历,并将每个数据保存到内存中
- 生成器返回的内存id都是一样的,这是因为获取数据时遇到yield会临时暂停处理,并记住当前位置执行状态,数据读取并使用后会立即从内存中释放掉,然后再获取下一个数据,当该生成器恢复时,会从离开位置继续执行,获取新的数据后又放到了原来的内存中,所以两次结果的内存id都是相同的,所以生成器是只将当前数据保存到内存中,每个数据只能读取一次
通过以上了解可知,生成器其实就是一个特殊的迭代器,目的是为了节省内存空间,但使用生成器只能迭代一次,所以工作中如果确定对生成的数据只使用一次时可以考虑使用Python生成器
推导式
推导式又叫做解析式,也有称为生成式、迭代式的,指对列表(list),字典(dict),集合(set)和元祖(tuple)在原有数据上根据条件生成新的数据,可以帮助我们把一个序列或者其它可迭代类型中的元素通过过滤或加工的方式,生成所需的新数据,基本语法为表达式 for 迭代变量 in 可迭代对象 [if 条件表达式]
,中括号内的表示可选参数
-
列表推导式
给出一个列表
num = [2,6,7]
,根据条件可以生成新的列表,示例如下# ↓循环num列表生成名为my_list0的新列表,结果为[2, 6, 7] my_list0 = [i for i in num] # ↓计算num列表中数据的平方,生成名为my_list1的新列表,结果为[4, 36, 49] my_list1 = [i**2 for i in num] # ↓计算num列表中满足if条件的数据,生成名为my_list3的新列表,结果为[16, 17] my_list2 = [i+10 for i in num if i > 3] # ↓计算num列表中数据,生成名为my_list2的嵌套列表,结果为[[3, 4], [7, 36], [8, 49]] my_list3 = [[i+1,i*i] for i in num] # ↓循环嵌套,提取my_list4嵌套列表中的偶数,生成名为my_list5的新列表,结果为[4, 36, 8] my_list4 = [n for i in my_list3 for n in i if n%2==0] # mylist4列表推导式等价于下面的普通写法 num = [] for i in my_list3: for n in i: if n%2==0: num.append(n) print(num) # 输出结果同样为[4, 36, 8] # ↓计算num列表中满足if条件的数据,生成名为my_list5的新列表,结果为['dyd-2', 'dyd-6'] my_list5 = [f"dyd-{i}" for i in num if i < 7] # ↓通过-截取my_list5中的数字,生成名为mylist6的新列表,结果为['2', '6'] my_list6 = [i.split("-",1)[1] for i in my_list3] # mylist6列表推导式等价于下面的普通写法 res = [] for i in my_list3: res.append(i.split("-",1)[1]) print(res) # 输出结果同样为['2', '6']
-
字典推导式
与列表推导式类似,中括号换成花括号即可,示例如下
my_dict0 = {i:i for i in range(1,4)} # 结果为{1: 1, 2: 2, 3: 3} my_dict1 = {i+1:i*2 for i in range(1,4)} # 结果为{2: 2, 3: 4, 4: 6} my_dict2 = {f"param{i}":[i+2,i] for i in range(1,4) if i != 3} # 结果为{'param1': [3, 1], 'param2': [4, 2]} my_dict3 = {f"param{i}":(i+5,i) for i in range(1,4) if i >= 2} # 结果为{'param2': (7, 2), 'param3': (8, 3)} # ↓根据personal_info字典中的元素按照“键=值”格式化,并使用&符号进行连接,最终与https://www.dyd.com?拼接成新的url personal_info = {"name":"yd","id":1,"age":22} # ↓输出结果为https://www.dyd.com?name=yd&id=1&age=22 my_dict4 = "https://www.dyd.com?"+"&".join([f"{k}={v}" for k,v in personal_info.items()]) # mydict4字典推导式等价于下面的普通写法 spl=[] for k,v in personal_info.items(): kv = f"{k}={v}" spl.append(kv) res = "https://www.dyd.com?"+"&".join(spl) print(res) # 输出结果同样为https://www.dyd.com?name=yd&id=1&age=22
-
集合推导式
集合和字典一样都是用花括号表示,只是集合元素不可以是列表,若要与字典区分,可以使用
set()
表示my_set0 = {i for i in range(5)} # 结果为{0, 1, 2, 3, 4} my_set1 = {(i+2,i,i-1) for i in range(5)} # 结果为{(6, 4, 3), (5, 3, 2), (3, 1, 0), (2, 0, -1), (4, 2, 1)} my_set2 = {(i,i,i) for i in range(5) if i<=3} # 结果为{(3, 3, 3), (0, 0, 0), (1, 1, 1), (2, 2, 2)} my_set3 = set((i,i,i) for i in range(5) if i<=3)# 结果与my_set2一致
-
元组推导式
元组推导式不同于上面的三种类型,它并不会立即执行内部循环生成数据,而是得到一个生成器,也可以说这是生成器的简单写法,如下示例
tuple_gen = (i+3 for i in range(2)) print(tuple_gen) # 直接打印会输出<generator object <genexpr> at 0x000002A93C28A340> for i in tuple_gen: print(i) # 结果为3和4 # tuple_gen元组推导式等价于下面的生成器写法 def my_generator(maxnum): for i in range(maxnum): yield i+3 gen = my_generator(2) print(gen) for i in gen: print(i) # 结果同样为3和4
总之,使用推导式可以减少代码量,使代码具有更高的可读性,以及更高的执行效率,最常用的是列表推导式
匿名函数
匿名函数顾名思义就是无需定义标识符的函数,在Python中,使用lambda语法定义匿名函数,lambda函数能接收任何数量(可以是0个)的参数,但只能返回一个表达式的值,当需要将一个函数对象作为参数来传递时,就可以直接定义一个lambda函数,基本语法为lambda [parameters]:expression
,中括号内的表示可选参数
import random
def rand(): # 使用普通函数,在1~10之间生成一个随机数
return random.randrange(1,10)
print(rand())
# ↓用匿名函数,在1~10之间生成一个随机数
num = lambda:random.randrange(1,10)
print(num())
def add(a,b): # 使用普通函数,定义两参数相加的函数
return a+b
print(add(2, 3))
# ↓用匿名函数,定义两参数相加的函数
a = lambda a,b:a+b
print(a(2,5))
def even(c): # 使用普通函数通过if判断奇偶数
if c % 2 == 0:
print(f"{c}是偶数")
else:
print(f"{c}是奇数")
even(11)
# ↓用匿名函数,通过if判断奇偶数
d = lambda c:f"{c}是偶数" if c % 2 == 0 else f"{c}是奇数"
print(d(12))
匿名函数更多时候是与高级函数一起使用,如下文的高阶函数介绍
高阶函数
高阶函数是将一个函数作为参数传递给另一个函数或将函数作为返回值的函数,满足其中一条就可以称之为高阶函数,Python内置函数中提供了多个高阶函数,如:zip、sorted、filter、map和reduce,以下介绍基本语法中的中括号内的参数均表示可选参数
-
zip函数
zip()函数是将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表,若各个可迭代对象中的元素数量不一致,则以最短的可迭代对象为准,基本语法为
zip(*iterables, [strict=False])
,若strict=True
则两个可迭代对象元素数量必须一致,否则报ValueError: zip() argument 2 is longer than argument 1
list1 = ["a",2,3,"b"] # 创建两个列表,两个列表元素数量不一致,会以list1长度为准,也就是说list2中20是不会输出的 list2 = [10,16,12,19,20] list3 = zip(list1,list2) # 使用zip函数组成新的列表 print(list3) # 输出结果为zip对象,<zip object at 0x000001BF7D703D00> for i in list3: print(i) # 输出结果为('a', 10) (2, 16) (3, 12) ('b', 19) print(list(zip(list1,list2,strict=True))) # 运行报错ValueError,因为list1和list2元素数量不一致
可以使用*号运算符,将元组拆解后重组为列表,比如将list3结果转为列表
list4,list5 = zip(*list3) # 拆分并重组元组,结果赋值给list4,list5 print(list(list4)) # 输出结果为['a', 2, 3, 'b'] print(list(list5)) # 输出结果为[10, 16, 12, 19] print(list1 == list(list4)) # 判断拆分得到的list4是否与list1相同,结果为True print(list2 == list(list5)) # 判断拆分得到的list5是否与list2相同,结果为False,因为少了元素20
-
sorted函数
sorted()函数用于对数据排序,它不会修改原始列表,而是返回一个新的列表,基本语法为
sorted(iterable,[key=None,reverse=False])
list1 = [2,0,2,6,19,-1] print(sorted(list1,reverse=True)) # 对list1列表降序排列,默认升序排,reverse=True表示降序 list2 = [ {"name": 'zs', "id": 410928, "age": 22}, {"name": 'ls', "id": 410102, "age": 26}, {"name": 'ww', "id": 110101, "age": 24} ] print(sorted(list2,key=lambda x:x["id"])) # 对list2字典列表根据age进行升序排列 list3 = [('a', 10),(2, 16),(3, 12),('b', 19)] print(sorted(list3, key=lambda x: x[1])) # 对list3元组列表根据元组中的第二个元素进行升序排列
-
filter函数
filter() 函数用于过滤可迭代对象中不符合条件的元素,它会把传入的函数依次作用于每个元素,返回值是True则保留该元素,并组成新的迭代器,基本语法为
filter(function or None, iterable)
# 函数参数项为None时,循环读取数据,会过滤掉0 a = [0,2,0,1,6,0,8] re = filter(None,a) print(list(re)) # 输出结果为[[2, 1, 6, 8] lis = [] for i in range(1,5): # 使用for循环过滤 if i %2 ==1: lis.append(i) print(lis) # 结果为[1, 3] def odd(maxnum): # 定义一个函数 return maxnum %2 ==1 # 返回奇数 res = filter(odd,range(5)) # 过滤出0~5之间的奇数 print(list(res)) # 转为列表输出,结果同样为[1, 3] # ↓也可结合匿名函数进行过滤 resu = filter(lambda x: True if x%2==1 else False,range(5)) # 如果为奇数则返回True,否者返回False print(list(resu)) # 转为列表输出,输出0~5之间判断奇数结果为True的数,结果同样也是[1, 3] # 使用上文介绍的列表推导式也能完成 print([i for i in range(1, 5) if i%2==1]) # 结果同样为[1, 3]
-
map函数
map()函数是将函数映射到可迭代对象中的每一项并输出其结果组成的迭代器,如果传入多个可迭代对象,函数也必须接受相同个数的实参,基本语法为
map(function,iterables,[key,default])
list1 = [2,6,3,9] # 创建一个数字列表 def my_map(x): # 定义一个普通函数 return x**2 # 返回x的平方 a = map(my_map,list1) # 使用map函数,x就等于list1中的每个元素 print(list(a)) # 转为列表输出,结果为[4, 36, 9, 81] list2 =[] # 使用for循环也可以实现上面的执行结果,首先创建一个空列表 for i in list1: # 读取list1中的元素 list2.append(i**2) # 每个元素进行平方运算后添加到list2空列表中 print(list2) # 打印添加元素后的list2列表,结果同样为[4, 36, 9, 81] # ↓在用map()函数中使用匿名函数表示上面操作 list3 = map(lambda x: x ** 2,list1) # 将list1中的每个元素赋值给x,然后进行平方运算,输出结果为map对象 print(list(list3)) # 转为列表输出,结果同样是[4, 36, 9, 81] list4 = [{"name": 'zs', "age": 22},{"name": 'ls', "age": 26},{"name": 'ww', "age": 24}] # ↓用map()函数,每个年龄加两岁 list5 = map(lambda x: x["age"]+2,list4) # 将list4中的每个元素赋值给x,然后将元素中的age加2 print(list(list5)) # 转为列表输出,结果为[24, 28, 26] list6 = [('a', 10),(2, 16),(3, 12),('b', 19)] # 创建一个元组列表 list7 = map(lambda x,y:x+y[1],list1,list6) # 将list1中的每个元素赋值给x,将list6中元组赋值给y,x与元组中的第2个值相加 print(list(list7)) # 转为列表输出,结果为[12, 22, 15, 28]
-
reduce函数
reduce()函数接收的参数和map()函数类似,只是reduce()函数的function必须带2个参数,计算过程是从sequence中取出第1个和第2个元素,计算得到的结果重新作为下一次迭代的第1个参数,与sequence的第3个元素继续进行计算,如此反复直到最后一个元素,基本语法为
reduce(function,sequence,[default])
res = 0 for i in range(1,101): # 使用for循环完成1~100高斯求和 res = i+res # 计算过程为(((((1 + 2)+(3)+ 4)+ 5)…… 100) print("计算结果为:"res) # 结果为5050 from functools import reduce# 使用reduce函数需要先导包 print(reduce(lambda x, y: x + y, range(1, 101))) # 使用reduce函数计算,结果同样为5050 print(reduce(lambda x, y: x + y, range(1, 101),10)) # 使用默认值10,在计算前先加上默认值,结果为5060
文章开头说的闭包其实也是一种高阶函数,但是高阶函数不一定是闭包,请注意他们之间的区别,以上内容在学习和使用时多思考它们之间的区别,并和普通函数多做比较,能够加深印象,也更容易理解,了解更多请查看官方文档