Python 编写纯函数,没有副作用的函数符合在数学中函数的纯粹定义:变量不会在全局范围内发生变化,避免使用global
语句基本可以达到要求。为了达到纯粹,则要求避免函数改变可变对象的状态。
下面是一个纯函数的例子:
def m(n: int) -> int:
return 2 ** n - 1
返回结果仅与参数n
的值有关,既没有改变全局变量,也没有更新任何可变数据结构。
任何(通过自由变量)对Python全局命名空间中值的引用都可以用参数实现,通常这类操作都很简单。下面是一个使用自由变量的例子:
def some_function(a: float, b: float, t: float) -> float:
return a + b * t + global_adjustment
可以将上面的global_adjustment
变量变为函数的参数,然后修改所有使用这个函数的地方。在一个复杂应用中,这么做会引发一系列变动。对全局变量的引用在函数体中表现为自由变量。
Python的许多内置对象都带有状态,例如文件类以及与文件相关的对象,都是常用的有状态对象。可以把Python中的大多数有状态对象看作上下文管理器,虽然很多开发者不使用上下文管理器,但实际上很多对象都实现了它要求的接口。少数有状态对象没有完全实现上下文管理器的接口,但往往实现了close()
方法。可以通过contextlib.closing()
函数为这些对象实现对应的上下文管理接口。
除非是小程序,否则我们难以去除所有的Python有状态对象。因此,必须在发挥函数式优势与管理状态中间寻找平衡。为了达到目标,应尽量用with
语句将有状态对象严格限制在一定的作用域内。
尽可能把文件对象放在
with
语句中。
尽量避免使用全局文件对象和全局数据库连接,以避免相关状态问题。全局文件对象是处理文件的常规方式,例如下面的命令片断所示的函数:
def open(iname: str, oname: str):
global ifile, ofile
ifile = open(iname, "r")
ofile = open(oname, "w")
在这个场景中,其他函数可以随意使用ifile
变量和ofile
变量,而这两个变量不仅是全局的,还始终保持打开的状态。
这样的设计不符合函数式编程的要求,应尽量避免。文件应作为函数的参数,打开的文件应嵌在with
语句中,以确保能正确处理其状态。将全局变量改为普通参数非常重要,会使文件操作更易辨识。
该原则也适用于数据库,应把数据库连接对象作为应用程序中函数的普通参数。有些流行的Web框架把数据库连接设计成全局的,使得整个应用程序都可以使用数据库的某些功能。这种透明性隐藏了Web操作对数据库的依赖,使得单元测试变得非常复杂。但单一的全局数据库连接并不能很好地支持多线程Web服务,使用连接池往往更好。这也说明了总体使用函数式设计,辅以一些严格限制的状态对象,是处理这种情况的理想解决方案。