python学习(四):函数应用进阶 本篇博客主要介绍了
Lambda函数
装饰器
函数实例
内容整理和修改自骆昊老师的github_python100天教学。
一、Lambda函数 在使用高阶函数的时候,如果作为参数或者返回值的函数本身非常简单,一行代码就能够完成,那么我们可以使用Lambda函数 来表示。Python中的Lambda函数是没有的名字函数,所以很多人也把它叫做匿名函数 ,匿名函数只能有一行代码,代码中的表达式产生的运算结果就是这个匿名函数的返回值。之前代码中的is_even
和square
函数都只有一行代码,我们可以用Lambda函数来替换掉它们,代码如下所示。
numbers1 = [35 , 12 , 8 , 99 , 60 , 52 ] numbers2 = list (map (lambda x: x ** 2 , filter (lambda x: x % 2 == 0 , numbers1)))print (numbers2)
通过上面的代码可以看出,定义Lambda函数的关键字是lambda
,后面跟函数的参数,如果有多个参数用逗号进行分隔;冒号后面的部分就是函数的执行体,通常是一个表达式,表达式的运算结果就是Lambda函数的返回值,不需要写return
关键字。
如果需要使用加减乘除这种简单的二元函数,也可以用Lambda函数来书写,例如调用上面的calc
函数时,可以通过传入Lambda函数来作为op
参数的参数值。当然,op
参数也可以有默认值,例如我们可以用一个代表加法运算的Lambda函数来作为op
参数的默认值。
def calc (*args, init_value=0 , op=lambda x, y: x + y, **kwargs ): result = init_value for arg in args: if type (arg) in (int , float ): result = op(result, arg) for value in kwargs.values(): if type (value) in (int , float ): result = op(result, value) return resultprint (calc(1 , 2 , 3 , x=4 , y=5 )) print (calc(1 , 2 , 3 , x=4 , y=5 , init_value=1 , op=lambda x, y: x * y))
提示 :注意上面的代码中的calc
函数,它同时使用了可变参数、关键字参数、命名关键字参数,其中命名关键字参数要放在可变参数和关键字参数之间,传参时先传入可变参数,关键字参数和命名关键字参数的先后顺序并不重要。
有很多函数在Python中用一行代码就能实现,我们可以用Lambda函数来定义这些函数,调用Lambda函数就跟调用普通函数一样,代码如下所示。
import operator, functools fac = lambda num: functools.reduce(operator.mul, range (1 , num + 1 ), 1 ) is_prime = lambda x: x > 1 and all (map (lambda f: x % f, range (2 , int (x ** 0.5 ) + 1 )))print (fac(10 )) print (is_prime(9 ))
提示1 :上面使用的reduce
函数是Python标准库functools
模块中的函数,它可以实现对数据的归约操作,通常情况下,过滤 (filter)、映射 (map)和归约 (reduce)是处理数据中非常关键的三个步骤,而Python的标准库也提供了对这三个操作的支持。
提示2 :上面使用的all
函数是Python内置函数,如果传入的序列中所有布尔值都是True
,all
函数就返回True
,否则all
函数就返回False
。
二、装饰器 装饰器是Python中用一个函数装饰另外一个函数或类并为其提供额外功能 的语法现象。装饰器本身是一个函数,它的参数是被装饰的函数或类,它的返回值是一个带有装饰功能的函数。很显然,装饰器是一个高阶函数,它的参数和返回值都是函数。下面我们先通过一个简单的例子来说明装饰器的写法和作用,假设已经有名为downlaod
和upload
的两个函数,分别用于文件的上传和下载,下面的代码用休眠一段随机时间的方式模拟了下载和上传需要花费的时间,并没有联网做上传下载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import randomimport timedef download (filename ): print (f'开始下载{filename} .' ) time.sleep(random.randint(2 , 6 )) print (f'{filename} 下载完成.' ) def upload (filename ): print (f'开始上传{filename} .' ) time.sleep(random.randint(4 , 8 )) print (f'{filename} 上传完成.' ) download('MySQL从删库到跑路.avi' ) upload('Python从入门到住院.pdf' )
现在我们希望知道调用download
和upload
函数做文件上传下载到底用了多少时间,这个应该如何实现呢?相信很多小伙伴已经想到了,我们可以在函数开始执行的时候记录一个时间,在函数调用结束后记录一个时间,两个时间相减就可以计算出下载或上传的时间,代码如下所示。
start = time.time() download('MySQL从删库到跑路.avi' ) end = time.time()print (f'花费时间: {end - start:.3 f} 秒' ) start = time.time() upload('Python从入门到住院.pdf' ) end = time.time()print (f'花费时间: {end - start:.3 f} 秒' )
通过上面的代码,我们可以得到下载和上传花费的时间,但不知道大家是否注意到,上面记录时间、计算和显示执行时间的代码都是重复代码。有编程经验的人都知道,重复的代码是万恶之源 ,那么有没有办法在不写重复代码的前提下,用一种简单优雅的方式记录下函数的执行时间呢?在Python中,装饰器就是解决这类问题的最佳选择。我们可以把记录函数执行时间的功能封装到一个装饰器中,在有需要的地方直接使用这个装饰器就可以了,代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import timedef record_time (func ): def wrapper (*args, **kwargs ): start = time.time() result = func(*args, **kwargs) end = time.time() print (f'{func.__name__} 执行时间: {end - start:.3 f} 秒' ) return result return wrapper
使用上面的装饰器函数有两种方式,第一种方式就是直接调用装饰器函数,传入被装饰的函数并获得返回值,我们可以用这个返回值直接覆盖原来的函数,那么在调用时就已经获得了装饰器提供的额外的功能(记录执行时间),大家可以试试下面的代码就明白了。
download = record_time(download) upload = record_time(upload) download('MySQL从删库到跑路.avi' ) upload('Python从入门到住院.pdf' )
上面的代码中已经没有重复代码了,虽然写装饰器会花费一些心思,但是这是一个一劳永逸的骚操作,如果还有其他的函数也需要记录执行时间,按照上面的代码如法炮制即可。
在Python中,使用装饰器很有更为便捷的语法糖 (编程语言中添加的某种语法,这种语法对语言的功能没有影响,但是使用更加方法,代码的可读性也更强,我们将其称之为“语法糖”或“糖衣语法”),可以用@装饰器函数
将装饰器函数直接放在被装饰的函数上,效果跟上面的代码相同,下面是完整的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import randomimport timedef record_time (func ): def wrapper (*args, **kwargs ): start = time.time() result = func(*args, **kwargs) end = time.time() print (f'{func.__name__} 执行时间: {end - start:.3 f} 秒' ) return result return wrapper@record_time def download (filename ): print (f'开始下载{filename} .' ) time.sleep(random.randint(2 , 6 )) print (f'{filename} 下载完成.' )@record_time def upload (filename ): print (f'开始上传{filename} .' ) time.sleep(random.randint(4 , 8 )) print (f'{filename} 上传完成.' ) download('MySQL从删库到跑路.avi' ) upload('Python从入门到住院.pdf' )
上面的代码,我们通过装饰器语法糖为download
和upload
函数添加了装饰器,这样调用download
和upload
函数时,会记录下函数的执行时间。事实上,被装饰后的download
和upload
函数是我们在装饰器record_time
中返回的wrapper
函数,调用它们其实就是在调用wrapper
函数,所以拥有了记录函数执行时间的功能。
如果希望取消装饰器的作用,那么在定义装饰器函数的时候,需要做一些额外的工作。Python标准库functools
模块的wraps
函数也是一个装饰器,我们将它放在wrapper
函数上,这个装饰器可以帮我们保留被装饰之前的函数,这样在需要取消装饰器时,可以通过被装饰函数的__wrapped__
属性获得被装饰之前的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import randomimport timefrom functools import wrapsdef record_time (func ): @wraps(func ) def wrapper (*args, **kwargs ): start = time.time() result = func(*args, **kwargs) end = time.time() print (f'{func.__name__} 执行时间: {end - start:.3 f} 秒' ) return result return wrapper@record_time def download (filename ): print (f'开始下载{filename} .' ) time.sleep(random.randint(2 , 6 )) print (f'{filename} 下载完成.' )@record_time def upload (filename ): print (f'开始上传{filename} .' ) time.sleep(random.randint(4 , 8 )) print (f'{filename} 上传完成.' ) download('MySQL从删库到跑路.avi' ) upload('Python从入门到住院.pdf' ) download.__wrapped__('MySQL必知必会.pdf' ) upload = upload.__wrapped__ upload('Python从新手到大师.pdf' )
三、函数实例 案例1:设计一个生成验证码的函数。
说明 :验证码由数字和英文大小写字母构成,长度可以用参数指定。
import randomimport string ALL_CHARS = string.digits + string.ascii_lettersdef generate_code (code_len=4 ): """生成指定长度的验证码 :param code_len: 验证码的长度(默认4个字符) :return: 由大小写英文字母和数字构成的随机验证码字符串 """ return '' .join(random.choices(ALL_CHARS, k=code_len))
可以用下面的代码生成10组随机验证码来测试上面的函数。
for _ in range (10 ): print (generate_code())
说明 :random
模块的sample
和choices
函数都可以实现随机抽样,sample
实现无放回抽样,这意味着抽样取出的字符是不重复的;choices
实现有放回抽样,这意味着可能会重复选中某些字符。这两个函数的第一个参数代表抽样的总体,而参数k
代表抽样的数量。
案例2:设计一个函数返回给定文件的后缀名。
说明 :文件名通常是一个字符串,而文件的后缀名指的是文件名中最后一个.
后面的部分,也称为文件的扩展名,它是某些操作系统用来标记文件类型的一种机制,例如在Windows系统上,后缀名exe
表示这是一个可执行程序,而后缀名txt
表示这是一个纯文本文件。需要注意的是,在Linux和macOS系统上,文件名可以以.
开头,表示这是一个隐藏文件,像.gitignore
这样的文件名,.
后面并不是后缀名,这个文件没有后缀名或者说后缀名为''
。
def get_suffix (filename, ignore_dot=True ): """获取文件名的后缀名 :param filename: 文件名 :param ignore_dot: 是否忽略后缀名前面的点 :return: 文件的后缀名 """ pos = filename.rfind('.' ) if pos <= 0 : return '' return filename[pos + 1 :] if ignore_dot else filename[pos:]
可以用下面的代码对上面的函数做一个简单的测验。
print (get_suffix('readme.txt' )) print (get_suffix('readme.txt.md' )) print (get_suffix('.readme' )) print (get_suffix('readme.' )) print (get_suffix('readme' ))
上面的get_suffix
函数还有一个更为便捷的实现方式,就是直接使用os.path
模块的splitext
函数,这个函数会将文件名拆分成带路径的文件名和扩展名两个部分,然后返回一个二元组,二元组中的第二个元素就是文件的后缀名(包含.
),如果要去掉后缀名中的.
,可以做一个字符串的切片操作,代码如下所示。
from os.path import splitextdef get_suffix (filename, ignore_dot=True ): return splitext(filename)[1 ][1 :]