一、Python函数式编程之概念简介
1.1 什么是函数式编程
编程语言支持通过以下几种方式来解构具体问题:
- 过程式编程:大多数的编程语言都是 过程式 的,所谓程序就是一连串告诉计算机怎样处理程序输入的指令。C、Pascal 甚至 Unix shells 都是过程式语言。
- 声明式编程:在 声明式 语言中,你编写一个用来描述待解决问题的说明,并且这个语言的具体实现会指明怎样高效的进行计算。 SQL 可能是你最熟悉的声明式语言了。 一个 SQL 查询语句描述了你想要检索的数据集,并且 SQL 引擎会决定是扫描整张表还是使用索引,应该先执行哪些子句等等。
- 面向对象(OOP)编程:程序会操作一组对象,对象拥有内部状态,并能够以某种方式支持请求和修改这个内部状态的方法。Smalltalk 和 Java 都是面向对象的语言。 C++ 和 Python 支持面向对象编程,但并不强制使用面向对象特性。
- 函数式(Functional)编程 将一个问题分解成一系列函数。 理想情况下,函数只接受输入并输出结果,对一个给定的输入也不会有影响输出的内部状态。 著名的函数式语言有 ML 家族(Standard ML,Ocaml 以及其它变种)和 Haskell。
一些语言的设计者选择强调一种特定的编程方式。 这通常会让以不同的方式来编写程序变得困难。其它多范式语言则支持几种不同的编程方式。Lisp,C++ 和 Python 都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。在大型程序中,不同的部分可能会采用不同的方式编写;比如 GUI 可能是面向对象的而处理逻辑则是过程式或者函数式。
在 函数式程序 里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。
有些语言对纯洁性要求非常严格,甚至没有诸如 a=3 或 c = a + b 之类的赋值语句,但很难避免所有的副作用,如打印到屏幕上或写到磁盘文件之类的副作用。另一个例子是调用 print() 或 time.sleep() 函数,它们都没有返回一个有用的值。这两个函数被调用只是为了它们的副作用,即向屏幕发送一些文本或暂停执行一秒钟。
函数式风格的 Python 程序并不会极端到消除所有 I/O 或者赋值的程度;相反,它们会提供像函数式一样的接口,但会在内部使用非函数式的特性。比如,函数的实现仍然会使用局部变量,但不会修改全局变量或者有其它副作用。
函数式编程可以被认为是面向对象编程的对立面。对象就像是颗小胶囊,包裹着内部状态和随之而来的能让你修改这个内部状态的一组调用方法,以及由正确的状态变化所构成的程序。函数式编程希望尽可能地消除状态变化,只和流经函数的数据打交道。在 Python 里你可以把两种编程方式结合起来,在你的应用中编写接受和返回对象实例的函数。
1.2 Python的函数式编程的特点
- 不是纯函数式编程:允许有变量
- 支持高阶函数:函数可以作为变量
- 支持闭包:可以返回函数
- 支持匿名函数
1.3 Python 内置函数
Python 内置的函数 及 功能用法 参见官网文档:https://docs.python.org/zh-cn/3.11/library/functions.html
内置函数功能及示例:https://blog.csdn.net/aobulaien001/article/details/132773510
二、Python函数式编程之函数简介
2.1 Python函数的定义与调用
在 Python 中,函数是一种可重用的代码块,用于执行特定的任务或操作。函数可以接受输入参数,并返回输出结果,从而实现模块化和封装性编程的目的。Python 中定义函数的语法如下:
|
|
其中,def
是定义 函数(function) 的关键字,function_name
是函数的名称,parameters
是函数的参数列表,用圆括号包裹,多个参数之间用逗号 ,
分隔,如果没有参数,则留空即可。函数名称和参数列表组成了函数的签名(signature),用于唯一识别和调用该函数。
函数的主体部分由冒号和缩进的代码块组成,通常包含一些语句和表达式来完成具体的计算或操作。函数的文档字符串(documentation string)是一个可选的字符串,用于描述函数的作用、参数、返回值等信息,可以通过内置函数 help()
来查看。
函数执行完毕后,可以使用 return
语句来返回一个值(expression),也可以不返回任何值,此时默认返回 None。
下面是一个简单的示例,演示如何定义和调用一个函数:
|
|
2.2 Python函数的参数传递及内部机制
Python 中的函数可以无参数(函数签名中的参数列表为空)、接受一个或多个参数,其中包括位置参数、默认参数、可变参数和关键字参数等不同类型的参数。下面一一进行介绍:
- 位置参数
位置参数 是指按照参数的位置顺序传递的参数,也称为必选参数。当函数被调用时,需要按照函数定义中的参数列表依次传递相应数量的位置参数。例如:
|
|
在上面的示例中,定义了一个函数 greet_user,它接两个位置参数 greet 和 name,用于向用户发送个性化的问候消息。然后,分别调用 greet_user 函数,并传递不同的参数值,从而输出不同的问候消息。
- 默认参数
默认参数 是指在函数定义时 为部分或全部 参数给定的固定参数值(默认值),这些给定默认值的参数,在函数被调用时,如果没有提供对应的参数,则会使用默认值代替。例如:
|
|
在上面的示例中,修改了函数 greet_user 的定义,增加了一个默认参数 message,并设置其默认值为 “How are you doing?"。
然后,分别调用 greet_user 函数,并传递不同的参数值。第一个调用只提供了必选参数 name,因此使用了默认值 “How are you doing?” 来代替缺失的参数值。第二个调用则显式地提供了两个参数值,从而覆盖了默认值。在函数定义中,默认参数必须放在位置参数之后。
Tips: 默认参数的位置
- 在Python中,函数的默认参数应该在函数定义时指定,并且应该放在非默认参数之后。如果默认参数放置在了非默认参数之前,将会导致解释器无法正确解析函数定义,从而引发语法错误:
SyntaxError: non-default argument follows default argument
。
- 关键字参数
关键字参数 是指调用函数时按照形参名称显式地传递的参数,也称为命名参数。在函数调用时,可以使用形参名称来标识和传递对应的参数值。例如:
|
|
在上面的示例中,分别使用形参名称来指定要传递的参数值,从而更加清晰地表示了参数的含义和顺序。这在接口复杂或者参数数量较多的情况下尤为有用。
Tips: 关键字参数 使得函数调用时的参数传递顺序不受限制,因此可以实现函数的调用顺序和定义时的顺序不一致。
- 可变参数
可变参数 是指能够接受任意数量的参数的函数,也称为不定长参数。在 Python 中,有两种类型的可变参数:*args
和 **kwargs
。
*args
表示接受任意数量的位置参数,参数会被打包成一个元组(tuple),并传递给函数。例如:
|
|
在上面的示例中,定义了一个函数 add_numbers,它使用可变参数 *nums
接受任意数量的位置参数,然后对它们进行求和并返回结果。注意,在函数内部使用循环对所有参数进行处理时,需要先将它们打包成一个元组。
**kwargs
表示接受任意数量的关键字参数,参数会被打包成一个字典(dict),并传递给函数。例如:
|
|
在上面的示例中,定义了一个函数 print_info,它使用可变参数 **kwargs
接受任意数量的关键字参数,然后依次输出每个参数的名称和值。
- 参数解包:
*
和**
简化多参数的传递
当有一个列表或字典,想要把其元素作为参数传递给函数时,*
和 **
看用来简化多参数的传递:
|
|
- 限制函数参数传递方式的秘密武器
/
和*
Python 中定义函数时,通过在参数列表中放置斜杠(/
),可以强制 /
之后的参数可以通过位置参数或关键字参数的形式进行传递,但是 /
之前的参数只能通过位置参数的形式进行传递:
|
|
Python 中定义函数时,通过在参数列表中放置星号(*
),可以强制 *
之前的参数可以通过位置参数或关键字参数的形式进行传递,但是 *
之后的参数只能通过关键字参数形式进行传递:
|
|
- 函数参数传递的内部机制
在许多编程语言中,函数参数的传递方式通常分为两大类:传值(Call by Value) 和 传引用(Call by Reference):
传值(Call by Value) :如果一个语言函数参数采用传值方式,那么当调用函数时,会为每个参数创建一个副本,函数内对传入参数所做的修改不会影响到原始数据(外层调用函数的参数变量)。
传引用(Call by Reference):而传引用则是直接传递变量的内存地址给函数,这意味着函数内对传入参数的修改会直接影响到原始数据(外层调用函数的参数变量)。
然而,Python中的参数传递更像是“传引用”的变体,但实际上更准确地描述是传递了对象的引用(或者说指针),而非直接传递对象本身。Python 语言中的这种更为特殊的机制,常被称为 传对象引用(Pass by Object Reference)。
在Python中,无论参数是基本类型(如int, float, str)还是复杂类型(如list, dict, object),传递给函数的都是这些值的引用(内存地址),而不是值本身。
但是,对于不可变类型(如int, float, str, tuple),虽然传递的是引用,但无法通过这个引用来改变原始数据,因为这些类型的值一旦创建就不能被改变。
例1:基本类型的参数传递
|
|
例2:列表作为参数传递
|
|
- 可变默认参数的陷阱
在定义函数时给参数设置了默认值,且该默认值是可变类型时,这个默认值会在函数定义时被创建,并在每次函数调用时复用。这可能导致意料之外的结果。例如:
|
|
为了避免这种问题,最佳实践是使用None作为默认值,并在函数内部初始化可变对象。
|
|
2.3 Python函数的返回值
在Python中,函数的 返回值 用于表示函数的结果,函数可以返回任何类型的值,包括列表、字典、类的实例等,使用 return
语句来指定函数的返回值。如果函数执行完毕没有 return 语句,默认返回None。
Python 中有 以下几种控制返回值的方式:
- 不返回任何值(返回None):
|
|
- 使用return不带任何值(相当于返回None):
|
|
- 返回单个值(对象):
|
|
- 返回多个值(对象):
|
|
函数返回多个值时,Python解析器默认会将它们放在一个元组中,程序员也可以显式地返回一个元组。
- 提前退出函数并返回一个值:
|
|
2.4 Python函数作用域
在 Python 中,函数具有自己的作用域(scope),也就是变量和对象的访问范围。在函数内部定义的变量属于局部变量,只能在函数内部使用;而在函数外部定义的变量属于全局变量,可以在函数内部和外部使用。例如:
|
|
在上面的示例中,定义了两个全局变量 x 和 y ,然后在函数 print_vals 中 给 x 赋值 100 并输出了 x、y 的值。由于 y 是全局变量,在函数内部可以直接访问和使用。
但是,在函数内部对 x 进行了赋值,因此在函数内部创建了一个局部变量 x,而在函数外部的 x 并没有被修改,因此函数内部的 x 覆盖了函数外部的 x,因此输出了 100。在函数返回后,函数外部的 x 依然保持不变,因此输出了 10。
为了在函数内部修改全局变量的值,需要使用 global
关键字来声明变量:
|
|
在上面的示例中,定义了一个全局变量 x,然后在函数 modify_x 中使用 global 关键字来声明 x 是全局变量,从而可以修改它的值。经过函数调用后,x 的值已经被修改为 100。
Tips: 在 Python 中:
- 局部变量和全局变量的名称不能重复,否则在局部变量作用域内同名的全局变量会被隐藏。
- 如果要在函数内部(局部作用域)使用全局变量的值而不修改它,可以直接使用全局变量名(只需注意同名局部变量隐藏全局变量),或者将其作为参数传递给函数。
- 如果要在函数内部(局部作用域)使用全局变量的值并且要修改它,则可以使用 global 关键字来引用它。
2.5 Python匿名函数
Python 中的匿名函数(lambda 函数)是一种特殊类型的函数,它没有名称,通常只包含一个表达式,并且可以接受任意数量的参数。匿名函数的语法如下:
|
|
其中,lambda 是关键字,parameters 是参数列表,多个参数之间用逗号分隔,如果没有参数,则留空即可。expression 是函数体,用于执行具体的计算或操作,并返回结果。
匿名函数通常用于简单的、一次性的操作或者作为其它函数的参数传递。由于匿名函数没有名称,因此可以节省代码量并提高可读性。
详情可参考:Python 05_Python中lambda匿名函数用法详解
三、Python函数式编程之高阶函数
3.1 Python 高阶函数简介
Python中的高阶函数是指 接受一个或多个函数作为输入(参数)或返回一个函数作为输出的函数。这些函数在Python中非常常见,并且常常出现在函数式编程中。如 filter()、map()、reduce()、sorted() 等。
- 示例一:接收一个或多个函数作为输入参数的高阶函数
|
|
如果传入abs作为参数f的值:
|
|
根据函数的定义,函数执行的代码实际上是:
|
|
由于参数 x, y 和 f 都可以任意传入,如果 f 传入其它函数,就可以得到不同的返回值。
- 示例二:返回一个函数作为输出的高阶函数
|
|
上面的函数定义,在函数 f 内部又定义了一个函数 g。由于函数 g 也是一个对象,函数名 g 就是指向函数 g 的变量,所以,最外层函数 f 可以返回变量 g,也就是函数 g 本身。 调用函数 f,会得到 f 返回的一个函数(g):
|
|
需要特别注意的是,返回函数和返回函数值的语句是非常类似的,但是它们的结果是截然不同的,返回函数时,不能带小括号,而返回函数值时,则需要带上小括号以调用函数:
|
|
返回函数有很多应用,比如可以将一些计算延迟执行,举个例子,定义一个普通的求和函数:
|
|
但是,如果返回一个函数,就可以“延迟计算”:
|
|
由于可以返回函数,在后续代码里就可以决定到底要不要调用该函数。
3.2 Python filter() 函数
filter()
函数是 Python 内置的一个有用的高阶函数,filter()
函数接收一个函数 f 和 一个 list,这个函数 f 的作用是对每个元素进行判断,返回 True 或 False,filter() 根据判断结果自动过滤掉不符合条件(False)的元素,并返回一个迭代器,可以迭代出所有符合条件的元素。
例如,要从一个list [1, 4, 6, 7, 9, 12, 17]中删除偶数,保留奇数:
|
|
利用filter()函数,可以完成很多很有用的功能,例如,删除 None 或者空字符串:
|
|
Tips: 注意: s.strip()会默认删除空白字符(包括’\n’, ‘\r’, ‘\t’, ’ ‘)
3.3 Python map() 函数
map()
是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在list的每个元素上,map()函数会返回一个迭代器,可以依次迭代得到原来 list 的元素被函数 f 处理后的结果。
|
|
由于 list 包含的元素可以是任何类型,因此,map() 不仅仅可以处理只包含数值的 list,事实上它可以处理包含任意类型的 list,只要传入的函数f可以处理这种数据类型。
3.4 Python reduce() 函数
和 map()
函数一样,reduce()
函数也是Python内置的一个高阶函数。reduce()
函数接收的参数和 map()
类似,一个函数 f,一个list,但其行为和
map()
不同,reduce()
传入的函数 f 必须接收两个参数 并返回一个值,reduce()
首先将使用 list 的前两个元素调用 f 函数获取返回值,然后该返回值 和 list 的下一个元素 继续调用 f 函数获取新的返回值,以此反复调用,直至 调用至 list 最后一个元素,并返回最终结(最后一次调用 f 函数 的)果值。
在python2中,reduce()函数和map()函数一样,可以直接使用,但是在python3中,reduce()函数被收录到functools包内,需要引入functools才可以使用。 例如:
|
|
得到的结果是25,实际过程是这样的,reduce()
函数会做如下计算:
- 先计算头两个元素:f(1, 3),结果为4;
- 再把结果和第3个元素计算:f(4, 5),结果为9;
- 再把结果和第4个元素计算:f(9, 7),结果为16;
- 再把结果和第5个元素计算:f(16, 9),结果为25;
- 由于没有更多的元素了,计算结束,返回结果25。
上述计算实际上是对 list 的所有元素求和。虽然Python内置了求和函数sum(),但是,利用reduce()求和也很简单。 reduce()还可以接收第3个可选参数,作为计算的初始值。如果把初始值设为100,计算:
|
|
结果将变为125,因为第一轮计算是:
计算初始值和第一个元素:f(100, 1),结果为101。
3.5 Python sorted() 函数
Python内置的 sorted()
函数可对list进行排序:
|
|
sorted()
函数,默认是由小到大排序列表的元素,可以指定 reverse=True 参数,则是由大到小排序。
当 list 的每一个元素又是一个容器时,则会以第一个元素来排序,比如在下面示例中,列表 score 的每个元素都是包含名字和成绩的一个tuple,此时,sorted()
函数将按名字首字母进行了排序并返回:
|
|
对于上述排序成绩的情况,默认是按照第一个名字进行排序的,在实际生活中更常见的是按照成绩排序,有没有办法让 sorted()
函数按照成绩来进行排序呢?
如果需要按照成绩高低进行排序,需要指定排序的字段是成绩,sorted()
接受 key 参数,用来指定排序的字段,key的值是一个函数,接受待排序列表的元素作为参数,并返回对应需要排序的字段。因此,sorted()
函数也是高阶函数。
|
|
3.6 Python zip() 函数
zip()
函数接受一系列可迭代对象作为参数,将它们按照索引打包成一个元组构成的新的可迭代对象。每个元组中的元素来自于不同的可迭代对象,对应于相同的索引位置。
zip()
函数可以将可迭代对象打包
|
|
在上述代码中,定义了两个可迭代对象numbers和letters,然后使用zip()函数将它们打包成一个新的可迭代对象zipped。最后,将zipped转换为列表并打印结果。
zip()
还可以将打包后的元组解包合并成多个列表。
|
|
在上述代码中,首先使用zip()函数将numbers和letters打包,并将结果保存在zipped中。然后,使用*操作符解包zipped,合并为多个列表merged_numbers和merged_letters。
当可迭代对象的长度不等时,zip()函数会停止在最短的可迭代对象结束迭代:
|
|
在上述代码中,定义了两个不等长的可迭代对象numbers和letters,其中letters只有两个元素。使用zip()函数将它们打包,最终只能迭代两次。
zip()函数与*操作符的结合使用转置二维列表
|
|
通过zip()函数和操作符,可以将二维列表的行和列互换。在上述代码中,定义了一个二维列表matrix,使用zip()函数和操作符将其转置为transposed_matrix。
3.7 Python 闭包
Python 语言支持在函数内部嵌套定义函数,在函数内部定义的函数和外部定义的函数是一样的,只是函数内部定义的函数无法被外部访问:
|
|
上面示例中的 g 函数是一个全局函数,可以被其它代码直接调用执行,如果将 g 函数的定义 移入(嵌套到) 函数 f 内部,就可以防止其它代码随意调用 g函数了:
|
|
再来看看前面 3.1 小节定义的 calc_sum 函数:
|
|
可以发现,这种情况下没法把 lazy_sum 函数 移到 calc_sum 的外部进行定义,因为它引用了 calc_sum 的参数 list_(局部变量)。
像这种内层函数引用了外层函数的变量(参数也算变量),然后返回内层函数的情况,称为 闭包(Closure)。
闭包的特点是 返回的函数还引用了外层函数的局部变量,所以,要正确使用闭包,就要确保引用的局部变量在函数返回后不能变。举例如下:
|
|
可能期望调用f1(),f2()和f3()结果为1,4,9,但实际结果全部都是 9。原因就是当 count() 函数返回了3个函数时,这3个函数所引用的变量 i 的值已经变成了3。由于f1、f2、f3并没有被调用,所以,此时它们并未计算 ii,当 f1 被调用时才计算 ii,此时 i 的值已经变成了3,这一点通过 f() 函数中的 print(i) 可以确定,因此调用 f1() 得到的结果都是 9,调用 f2() 和 f3() 也是相同原理,结果也是 9。
因此,返回函数不要引用任何循环变量,或者后续会发生变化的变量。
3.8 偏函数(partial function)
当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化调用者的负担。
比如,内置的 int() 函数可以把字符串转换为整数,当仅传入字符串时,int() 函数默认按十进制转换:
|
|
但 int() 函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:
|
|
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,此时,可以定义一个 int2() 的函数,该函数只接收一个参数,在该函数中调用 int() 函数 默认把 base=2 传进去, 如下所示:
|
|
这样使用 int2() 函数来转换二进制就非常方便了:
|
|
偏函数(partial function) 指的就是“创建一个调用另外一个部分参数或变量已经预置的函数”的函数的用法,如上所示,int() 函数的base参数,没有指定的时候,默认是以十进制工作的,当指定base=2的时候,int2实际上就变成了部分参数(base)已经预置了的偏函数。 functools.partial 就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
|
|
四、Python 装饰器(decorator)
4.1 什么是装饰器(decorator)
Python 中的 @
符号是一个非常强大而又灵活的功能,它通常用于函数定义之前,紧接着一个函数名,这一组合代表一个叫做 装饰器(decorator) 的 语法糖。
|
|
上面示例中,@print
其实是将 say_hello 函数"装饰"或"包装"了一层 print 函数。换句话说,say_hello = print(say_hello) 被隐式地执行了。
装饰器的工作原理可以概括为以下几个步骤:
- 定义装饰器函数
- 将装饰器应用于目标函数
- 在运行时替换目标函数
示例:
|
|
- 定义装饰器函数
uppercase
。这个函数接受一个函数 func 作为参数,并返回一个新的函数 wrapper。 - 使用
@uppercase
语法将uppercase
装饰器应用于say_hello
函数。这实际上是将say_hello
函数传递给uppercase
函数,并将返回值重新赋值给say_hello
。 - 当调用
say_hello("Alice")
时,实际上调用的是wrapper
函数,而不是原始的say_hello
函数。wrapper
函数中调用了原始的say_hello
函数(参数 func),并对其返回值进行大写转换。
通过这个过程,成功地在不改变 say_hello 函数本身的情况下,扩展了它的功能,这就是**装饰器(decorator)**的核心机制。
Python 中的 装饰器(decorator) 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。 Python中提供了 @
语法来使用 装饰器(decorator),这样可以避免手动编写 f = decorate(f) 这样的代码。
Python 装饰器(decorator)按使用时是否有参数可分为 无参数 和 有参数两种类型。
4.2 无参数的装饰器(decorator)
使用时不带参数的装饰器称为 无参数装饰器,如下所示定义一个 @log 装饰器函数并使用它装饰 factorial 函数:
|
|
上述示例中,@log 就是一个装饰器(decorator),它接收一个函数 f 作为参数,返回一个新函数 fn,新函数 fn 内部会调用 f 函数并打印出调用 f 函数的语句。
@log 装饰器的本质是如下代码所示
|
|
对于参数不是一个的函数,使用 @log 装饰器时 调用将报错:
|
|
因为 add() 函数需要传入两个参数,但是 log函数里 return f(x)
语句写死了只含一个参数的返回函数。要让 @log 自适应任何参数定义的函数,可以利用Python的 *args
和 **kwargs
,保证任意个数的参数总是能正常调用:
|
|
4.3 有参数的装饰器(decorator)
使用时带参数的装饰器称为 有参数装饰器,
在上一节使用的 @log 装饰器,对于被装饰的函数,log打印的语句是不能变的(除了函数名)。
假如项目开发中有这样一个需求:有的函数非常重要,希望打印出’[INFO] call xxx()…‘。 有的函数不太重要,希望打印出’[DEBUG] call xxx()…’。 这时,log函数本身就需要传入’INFO’或’DEBUG’这样的参数,类似这样:
|
|
把上面的 my_func() 调用翻译成高阶函数的调用,就是:
|
|
把每一步展开,就是:
|
|
上面的 rmfc = log_decorator(my_func) 语句又相当于:
|
|
所以,带参数的log函数首先返回一个decorator函数,再让这个decorator函数接收my_func并返回新函数,相当于是在原有的二层嵌套里面,增加了一层嵌套:
|
|
@log('DEBUG')
装饰的test函数 的调用执行相当于如下高阶函数调用执行过程:
|
|
4.4 装饰器的应用
在处理重复计算或者进行昂贵的函数调用时,使用 lru_cache
装饰器缓存中间结果可以显著提高效率:
|
|