Python实战之函数的一些奇技淫巧
不妨大胆一点,有很多事没有答案。——–大鱼海棠
写在前面
博文为
《Python Cookbook》
读书后笔记整理书很不错,感兴趣小伙伴可以去拜读下
博文涉及内容包括:
语法方面
- 定义接受任意数量参数的函数
- 定义只允许接受字典参数的函数
- 定义函数参数类型注释,函数体注释信息打印
- 定义返回多个值的函数
- 定义有默认参数的函数
- 定义匿名或内联函数
- 匿名函数如何捕获变量值
函数调优方面:
- 减少可调用对象的参数个数
- 将单方法的类转换为函数
- 带额外状态信息的回调函数
- 优雅的访问闭包中定义的变量
食用方式
- 本文适合初学
python
的小伙伴,需要了解Python
基础知识 - 可能小伙伴们觉得
pytohn
函数有什么可讲的,只要会基本语法,用的时候灵活运用就可以了 - 实际上真的是这样么?希望通过本文认识不一样的
Python
函数
- 本文适合初学
理解不足小伙伴帮忙指正
不妨大胆一点,有很多事没有答案。——–大鱼海棠
名词解释
- 位置参数: 直接传递变量值
- 关键字参数:给指定变量名传递一个变量值
- 默认参数: 一般为定值的关键字参数,赋值在函数定义时完成,必须为不可变类型
函数
如何定义接受任意数量参数的函数
你想构造一个可接受任意数量参数的函数。
让一个函数接受任意数量的位置参数,python 可以使用一个 * 参数
1 | def avg(first, *rest): |
在函数内部的处理机制中,rest会转化为所有其他位置参数组成的元组。所以我们可以直接当成了一个序列来使用
在其他的语言中,这种语法也叫做可变参数
,JavaScript的可变参数函数定义
1 | function fun(a,...b){ |
为了接受任意数量的k-v
关键字参数参数,使用一个以**
开头的参数。比如:
1 | import html |
如果希望某个函数能同时接受任意数量的位置参数和关键字参数,可以同时使用*和**
。比如:
1 | def anyargs(*args, **kwargs): |
所有位置参数
会被放到args元组
中,所有关键字参数
会被放到字典kwargs
中。
一个*
参数只能出现在函数定义中最后一个位置参数后面
,而**
参数只能出现在最后一个参数
。有一点要注意的是,在*
参数后面仍然可以定义其他参数。这里有点不太理解
1 | def a(x, *args, y): |
如何定义只允许接受关键字参数的函数
你希望函数的某些参数强制使用关键字参数传递
将强制关键字参数放到某个 * 参数
后面就能达到这种效果。
1 | def recv(maxsize, *, block): |
利用这种技术,我们还能在接受任意多个位置参数的函数中指定关键字参数。
1 | def mininum(*values, clip=None): |
那为什么要使用关键字参数,而不用位置参数?
很多情况下,使用关键字参数会比使用位置参数表意更加清晰,另外,使用强制关键字参数也会比使用**kwargs
参数更好,因为在使用函数help
的时候输出也会更容易理解:
通过help
方法也可以直接输出注释信息
1 | def mininum(*values, clip=None): |
嗯,执行输出
1 | Help on function mininum in module __main__: |
如何定义函数参数类型注释,函数体注释信息打印
关于函数体注释信息打印可以看上面的Demo
写好了一个函数,然后想为这个函数的参数增加一些额外的信息,这样的话其他使用者就能清楚的知道这个函数应该怎么使用。一般的编译型语言都会强制的声明,解释型语言则没有那么多要求,那如果我希望在python里面去声明类型应该如何处理
使用函数参数注解是一个很好的办法,它能提示程序员应该怎样正确使用这个函数。例如,下面有一个被注解了的函数:
1 | Python 3.9.0 (tags/v3.9.0:9cf6752, Oct 5 2020, 15:23:07) [MSC v.1927 32 bit (Intel)] on win32 |
add.__annotations__
可以打印函数的注解信息
1 | add.__annotations__ |
python解释器不会对这些注解添加任何的语义
。它们不会被类型检查,运行时跟没有加注解之前的效果也没有任何差距。然而,对于那些阅读源码的人来讲就很有帮助啦。第三方工具和框架可能会对这些注解添加语义。同时它们也会出现在文档中。
尽管你可以使用任意类型的对象给函数添加注解 (例如数字,字符串,对象实例等等),不过通常来讲使用类
或着字符串会比较好点
。
如何定义返回多个值的函数
希望构造一个可以返回多个值的函数
为了能返回多个值,函数直接 return 一个元组就行了,默认情况下回返回一个元组。
1 | def myfun(): |
1 | a,b,c = myfun() |
相比来讲,这里 GO
就要方便的很多,不但可以传递多个参数,同时可以返回异常信息,自动拆包,不同的是GO
需要定义返回值
1 | func (ip IP) MarshalText() ([]byte, error) { |
如何定义有默认参数的函数
你想定义一个函数或者方法,它的一个或多个参数是可选的并且有一个默认值
这个没啥可说的,小伙伴应该都很熟悉,需要注意这里的默认参数和关键字参数有相似的地方,当关键字参数的值为不可变得,即为默认参数,但是行为是不同的,默认参数一般会给一个默认值,是不可变得,而关键字参数是传递的变量给一个定义好的变量名
普通的默认参数函数
1 | def spam(a, b=42): |
需要注意的是 如果默认参数
是一个可修改的容器比如一个列表、集合或者字典
,可以使用None
作为默认值,就像下面这样:
1 | def spam(a, b=None): |
但是这样写的话会有一个问题,我们如何确认当前关键字变量使用的是默认参数,还是传递的关键字参数
我们可以像下面这样写:
1 | object() _no_value = |
通过执行我们可以看到,传递一个 None 值
和不传值
两种情况是有差别的。
默认参数的值仅仅在函数定义的时候赋值一次
1 | >>> x = 42 |
注意到当我们改变x
的值的时候对默认参数值并没有影响,这是因为在函数定义的时候就已经确定了它的默认值了
,这里类似于python
的vars()
,可以默认获取当前上下文的变量值。
其次,默认参数的值应该是不可变的对象,比如None、True、False、数字或字符串
。特别的,千万不要像下面这样写代码:
1 | def spam(a, b=[]): # NO! |
如果你这么做了,当默认值在其他地方被修改后你将会遇到各种麻烦。这些修改会影响到下次调用这个函数时的默认值。换句话讲,这其实是一个共享变量,随着使用在不断变化,比如:
1 | >>> def spam(a, b=[]): |
最好是将默认值设为None
,然后在函数里面检查它,前面的例子就是这样做的。
在测试 None 值时使用 is 操作符是很重要的,不要使用下面的方式
1 | def spam(a, b=None): |
这么写的问题在于尽管None值确实是被当成False,但是还有其他的对象(比如长度为0的字符串、列表、元组、字典等)都会被当做False。因此,上面的代码会误将一些其他输入也当成是没有输入。比如:
1 | 1) # OK spam( |
所以在默认参数中,判断参数是否为空的清理。唯一能做的就是测试同一性。这个刚好符合要求。
如何定义匿名或内联函数
你想为sort()操作创建一个很短的回调函数,但又不想用def 去写一个单行函数,而是希望通过某个快捷方式以内联方式来创建这个函数。
当一些函数很简单,仅仅只是计算一个表达式的值的时候,就可以使用lambda表达式
来代替了。比如:
lambda表达式某种意义上讲,是函数式编程的体现,行为参数化的思想,本质上是匿名函数的语法糖。
1 | lambda x, y: x + y add = |
这里和JS里的语法很类似,但是JavaScript中的lambad要强大的多,和Java里的相类似,不单单可以写一行语句,甚至可以嵌套。但是Java的lambad表达式接收的外接的共享变量必须为不可变得。
1 | var arr = [2,15,8,11,7,4]; |
python看一个具体的Demo
1 | >>> names = ['David Beazley', 'Brian Jones','Raymond Hettinger', 'Ned Batchelder'] |
有人编写大量计算表达式值的短小函数或者需要用户提供回调函数的程序的时候,会常常使用lambda表达式。
匿名函数如何捕获变量值
你用 lambda 定义了一个匿名函数,并想在定义时捕获到某些变量的值。
1 | 10 x = |
这其中的奥妙在于lambda表达式中的x是一个自由变量,在运行时绑定值
,而不是定义时就绑定,这跟函数的默认值参数定义是不同的
。因此,在调用这个lambda表达式的时候,x的值是执行时的值。
这里值得一提的是Java中lambda表达式也有需要注意的,当表达式内部使用外部的共享(引用)变量要单独赋值或者定义为final,java通过这样一种方式,在语法层面强制lambad表达式引用的局部变量不可被改变,引起局部变量的语义冲突,类似上面python那样。我最初以为java中代码编译后会涉及指令重排,执行lambad的时候,变量为执行到这里的值,所以为了保证表达式中的变量是自己想要,需要强制设定,其实和指令重排没关系,只是为了避免上面的那种语法混淆,提醒 coder 在使用lambad的时候,外部的局部变量值在内部使用时是不应该改变的。
1 | 10 x = |
如果你想让某个匿名函数在定义时就捕获到值
,可以将那个参数值定义成默认参数
即可,就像下面这样:
1 | 10 x = |
减少可调用对象的参数个数
你有一个被其他 python代码使用的callable 回调对象,可能是一个回调函数或者是一个处理器,但是它的参数太多了,导致调用时出错。
如果需要减少某个函数的参数个数,你可以使用functools.partial()
。partial()
函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。为了演示清楚,假设你有下面这样的函数:
1 | def spam(a, b, c, d): |
现在我们使用partial()
函数来固定某些参数值:
1 | from functools import partial |
可以看出partial()
固定某些参数并返回一个新的callable
对象。这个新的callable
接受未赋值的参数,然后跟之前已经赋值过的参数合并起来,最后将所有参数传递给原始函数。
1 | def distance(p1, p2): |
现在假设你想以某个点为基点,根据点和基点之间的距离来排序所有的这些点。列表的 sort()方法接受一个关键字参数来自定义排序逻辑,但是它只能接受一个单个参数的函数(distance()很明显是不符合条件的)。现在我们可以通过使用 partial()来解决这个问题:
1 | >>> pt = (4, 3) |
partial()
通常被用来微调其他库函数所使用的回调函数的参数
使用 multiprocessing
来异步计算一个结果值,然后这个值被传递给一个接受一个 result
值和一个可选 logging
参数的回调函数
1 | def output_result(result, log=None): |
当给apply_async()
提供回调函数时,通过使用partial()传递额外的logging参数。而multiprocessing对这些一无所知——它仅仅只是使用单个值来调用回调函数。
作为一个类似的例子,考虑下编写网络服务器的问题,socketserver ,使用 partial() 就能很轻松的解决——给它传递 ack 参数的值来初始化即可
1 | from socketserver import StreamRequestHandler, TCPServer |
很多时候partial()能实现的效果,lambda表达式也能实现。比如,之前的几个例子可以使用下面这样的表达式:
1 | points.sort(key=lambda p: distance(pt, p)) |
将单方法的类转换为函数
你有一个除 init () 方法外只定义了一个方法的类。为了简化代码,你想将它转换成一个函数。
哈,这个和Java的函数式接口特别像
1 | from urllib.request import urlopen |
这个类可以被一个更简单的函数来代替:
1 | from urllib.request import urlopen |
使用一个内部函数
或者闭包
的方案通常会更优雅一些。简单来讲,一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境
。因此,在我们的解决方案中,opener()
函数记住了template
参数的值,并在接下来的调用中使用它。
任何时候只要你碰到需要给某个函数增加额外的状态信息的问题
,都可以考虑使用闭包。相比将你的函数转换成一个类而言,闭包通常是一种更加简洁和优雅的方案。Python 装饰器
1 |
带额外状态信息的回调函数
你的代码中需要依赖到回调函数的使用(比如事件处理器、等待后台任务完成后的回调等),并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。
会不会很熟悉,好多的js里面也会这样写,
1 | def apply_async(func, args, *, callback): |
这里的callback
必须为一个关键字参数
1 | def apply_async(func, args, *, callback): |
print result() 函数仅仅只接受一个参数 result 。不能再传入其他信息,而当你想让回调函数访问其他变量或者特定环境的变量值的时候就会遇到麻烦,为了让回调函数访问外部信息
,一种方法是使用一个绑定方法来代替一个简单函数。
1 | class ResultHandler: |
使用这个类的时候,你先创建一个类的实例,然后用它的 handler() 绑定方法来
1 | class ResultHandler: |
使用闭包方式
1 |
|
使用协程来完成同样的事情:
1 | def make_handler(): |
对于协程,你需要使用它的 send() 方法作为回调函数
1 | handler = make_handler() |
这里 nonlocal
声明语句用来指示接下来的变量会在回调函数中被修改。如果没有这个声明,代码会报错。
访问闭包中定义的变量
你想要扩展函数中的某个闭包,允许它能访问和修改函数的内部变量。
通常来讲,闭包的内部变量对于外界来讲是完全隐藏的。但是,你可以通过编写访问函数并将其作为函数属性绑定到闭包上来实现这个目的
1 | def sample(): |
nonlocal
声明可以让我们编写函数来修改内部变量的值,函数属性允许我们用一种很简单的方式将访问方法绑定到闭包函数上,这个跟实例方法很像(尽管并没有定义任何类)。
还可以进一步的扩展,让闭包模拟类的实例。你要做的仅仅是复制上面的内部函数到一个字典实例中并返回它即可。例如:
1 | import sys |
结果显示,闭包的方案运行起来要快大概 8%,大部分原因是因为对实例变量的简化访问,闭包更快是因为不会涉及到额外的 self 变量。
总体上讲,在配置的时候给闭包添加方法会有更多的实用功能,比如你需要重置内部状态、刷新缓冲区、清除缓存或其他的反馈机制的时候。
Python实战之函数的一些奇技淫巧
https://liruilongs.github.io/2022/07/12/Python/Python实战之函数的一些奇技淫巧/