Python函数式编程(第2版)(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-11 09:35:41

点击下载

作者:(美)史蒂文·洛特(Steven F. Lott)

出版社:人民邮电出版社有限公司

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Python函数式编程(第2版)

Python函数式编程(第2版)试读:

前言

函数式编程为创建代码简洁明了的软件提供了许多技术。虽然Python不是纯粹的函数式语言,但仍然可以使用Python进行函数式编程。

Python具备函数式编程的许多核心特征,使得我们可以借鉴其他函数式语言的设计模式和编程技术,编写出简洁优雅的代码。尤其值得一提的是Python的生成器表达式,使用它可以避免在内存中创建大型数据结构,通过降低资源消耗来提高执行速度。

Python缺少创建纯粹函数式程序所需的一些语言特征,例如无限递归、针对所有表达式的惰性求值(lazy evaluation)以及优化编译器等。

函数式编程的许多核心要素都在Python中有所体现,例如函数是头等对象。Python还提供了许多典型的高阶函数,例如广泛使用的内置map()、filter()和functools.reduce()等,以及不那么明显的sorted()、min()和max()等。

本书通过Python语言诠释函数式编程的核心思想,旨在利用函数式编程的优点,编写出代码简洁明了的Python程序。目标读者

如果你希望借鉴函数式编程语言的技术和设计模式编写出简洁明了的Python程序,本书便是为你准备的。某些算法用函数式方法编写更为简洁,我们可以也应该运用函数式方法编写出更易读且更易维护的Python程序。

可以通过函数式编程在适宜的场景中开发出高性能的算法,但Python往往会生成大型中间数据结构,耗尽机器的内存和CPU。因此常用生成器表达式代替大型列表,前者在保证可读性的前提下,内存消耗更少,运算速度更快。本书内容

第1章:函数式编程概述,介绍Python中函数式编程对应的技术和语言特征,以及函数式设计为Python程序带来的好处。

第2章:函数式编程的特点,分析函数式编程范式的6个核心特征,以及每个特征在Python中的实现方法,还会讲到一些在Python中不易实现的函数式语言特征,例如为了支持编译优化,许多语言的类型匹配规则非常复杂。

第3章:函数、迭代器和生成器,介绍如何在Python中使用不可变对象和生成器表达式,如何将函数式编程的核心思想应用于Python和Python内置的集合类型,以及如何将函数式编程理念运用于这些数据结构。

第4章:使用集合,介绍如何使用Python的内置函数操作数据集。其中重点介绍几个比较简单的函数,例如any()和all(),它们的共同点是能将集合转换为单个值。

第5章:高阶函数,介绍一些常用的高阶函数,例如map()和filter(),以及如何创建新的高阶函数。

第6章:递归与归约,介绍如何使用递归设计算法,以及使用for循环提升性能,还会介绍其他一些应用广泛的归约函数,例如collections.Counter()。

第7章:元组处理技术,介绍使用不可变的元组和命名元组代替状态可变对象的方法。相比而言,不可变对象没有误用属性导致对象行为异常(不连续或无效)的问题,用起来更简单可靠。

第8章:itertools模块,介绍Python标准库中处理集合和生成器的几个函数,可用于简化处理集合数据的程序。

第9章:高级itertools技术,介绍itertools模块中不太常用的组合器函数,并演示错误使用这些函数导致的组合器膨胀问题。

第10章:functools模块,介绍如何将functools模块中的函数用于函数式编程。此模块中适用于构建装饰器的少数函数留待第11章讨论。本章还会介绍一些支持其他函数式编程的函数。

第11章:装饰器设计技术,介绍如何用装饰器构建复合函数。虽然使用装饰器能给程序开发带来很大的灵活性,但也有概念限制:过于复杂的装饰器非但无用,还会严重降低程序的可读性。

第12章:multiprocessing和threading模块,介绍函数式编程的一大优势:便于分流任务负载。使用不可变对象能避免设计欠佳的同步写入操作导致运行结果不可预料。

第13章:条件表达式和operator模块,介绍避开Python严格求值顺序的一些变通方法及其局限性,以及使用operator模块给某些简单的处理带来的轻微提升。

第14章:PyMonad库,介绍PyMonad库的主要特点以及更丰富的函数式编程手段,还有单子(monad)。在一些函数式语言中,代码优化会打乱某些操作的顺序,而开发者可以使用单子强制程序按照期望的顺序执行。由于Python按照严格的顺序对表达式和声明求值,因此在Python中,单子的理论研究价值高于实用价值。

第15章:Web服务的函数式设计方法。如果把Web服务看作从请求到响应的转换,那么可以把开发Web服务看作开发能实现该转换的一组函数。本章将介绍如何借助函数式编程方法构建响应式动态Web内容。

第16章:优化与改进,介绍提升程序性能的一些方法和技巧。在适合的场景中,这些方法(例如内存化)不但易于实现,并且能显著提升程序性能。如何使用本书

阅读本书需要读者对Python 3和应用开发有基本了解。本书不会涉及Python中细微、复杂的语言特性,也不需要读者了解实现语言功能的内部机制。

读者需要对函数式编程有基本了解。由于Python不属于函数式语言,因此本书不会深入探讨函数式编程的概念,而会着重介绍其中适用于Python并有实用价值的部分。

书中的部分示例使用探索性数据分析(EDA)引出问题,演示函数式编程的特点。对统计学和概率论有基本了解有助于理解问题。只有很少一部分示例涉及数据科学。

你的计算机上需要安装并运行Python 3.6。关于Python的更多信息,请访问http://www.python.org。本书的示例代码经常使用类型提示,所以请安装最新版本的mypy。关于最新版本的mypy,请访问https://pypi.python.org/pypi/mypy。

第9章的示例代码使用了PIL和BeautifulSoup 4。为了保持版本兼容,使用了PIL库的新分支版本Pillow代替原始PIL库,详情请访问https://pypi.python.org/pypi/Pillow/2.7.0和https://pypi.python.org/pypi/beautifulsoup4/4.6.0。

第14章的示例代码使用了PyMonad库,详情请访问https://pypi.python.org/pypi/PyMonad/1.3。

可以通过如下命令安装以上所有库。$ pip install pillow beautifulsoup4 PyMonad下载源代码

如果你是从http://www.packtpub.com网站购买的图书,登录自己的账号后就可以下载所有已购图书的示例代码。如果你是从其他地方购买的图书,请访问http://www.packtpub.com/support网站并注册,我们会将代码文件直接发送到你的电子邮箱。

你也可以通过以下步骤下载代码文件。

(1) 在我们的网址上登录或注册。

(2) 选择SUPPORT标签。

(3) 点击Code Downloads & Errata。

(4) 在Search框中输入书名并按屏幕上的提示操作。

文件下载后,使用以下工具的最新版本来解压缩或提取文件夹。● WinRAR / 7-Zip(Windows)● Zipeg / iZip/UnRarX(Mac)● 7-Zip / PeaZip(Linux)

本书代码也托管在GitHub上,访问https://github.com/PacktPublishing/Functional-Python-Programming-Second-Edition/即可获取1。Packt拥有丰富的图书和视频资源,相关代码见GitHub仓库:https://github.com/PacktPublishing/。欢迎查阅!

1你可以直接访问本书中文版页面,下载本书项目的源代码:http://www.ituring.com.cn/book/2658。——编者注排版约定

本书如下约定文本样式。

正文中的代码采用以下样式:“Python有其他声明,如global或nonlocal,用于在特定命名空间中修改变量的规则。”

代码块的样式如下所示。s = 0for n in range(1, 10): if n % 3 == 0 or n % 5 == 0: s +=nprint(s)

如果代码块的特定部分需要注意,相应的行或项会加粗。s = 0for n in range(1, 10): if n % 3 == 0 or n % 5 == 0: s += nprint(s)

命令行或输出如下所示。$ pip install pillow beautifulsoup4 PyMonad

新的术语和重要的词语将显示为黑体。在屏幕上(如菜单或对话框中)出现的文字按如下样式显示:“在现有的许多范式中,我们重点区分函数式编程和命令式编程。” 此图标表示警告或需要特别注意的内容。 此图标表示提示或技巧。问题与反馈

一般反馈:发送邮件至feedback@packtpub.com并在主题处注明书名。如果对于本书有任何方面的疑问,请发送邮件至questions@packtpub.com。

勘误:尽管我们尽力确保内容准确,但出错仍在所难免。如果你在书中发现错误,不管是文本还是代码,希望能告知我们,我们不胜感激。2如果你发现任何错误,请访问http://www.packtpub.com/submit-errata,选择书名,点击Errata Submission Form链接,并输入详细说明。

2针对本书中文版的勘误,请到http://www.ituring.com.cn/book/2658查看和提交。——编者注

反盗版:如果你发现我们的作品在互联网上被以任何形式非法复制,请立即为我们提供地址或网站名称,非常感谢。请把可疑盗版材料的链接发到copyright@packtpub.com。

成为作者:如果你掌握某个领域的专业知识,并且有兴趣写作图书,请访问authors.packtpub.com。评论

请给出评论。阅读、使用本书后,请在购买网站上留下评论。这样潜在读者可以参考你的意见来决定是否购买,Packt可以了解到你对该产品的看法,作者也能看到你对本书的反馈。谢谢!

想了解关于Packt的更多信息,请访问packtpub.com。电子书

扫描如下二维码,即可购买本书电子版。第 1 章 函数式编程概述

函数式编程通过在函数中定义表达式和对表达式求值完成计算。它尽量避免由于状态变化和使用可变对象引入复杂性,让程序变得简洁明了。本章将介绍函数式编程的一些基本技术,以及如何在Python中运用这些技术。最后会介绍通过这些设计模式构建Python应用时,函数式编程带来的好处。

Python包含大量函数式编程特征,但它不是纯粹的函数式编程语言。它不仅具备函数式编程的诸多优势,还保留了命令式编程的强大优化能力。

本书中的许多示例来自EDA领域。使用函数式编程方式解决该领域的问题可以很好地展示其特点,而且与其他解决方法相比有明显优势。

本章的主要目标是介绍函数式编程的基本原则,第2章开始编写Python代码。 本书主要使用Python 3.6作为实现语言,部分示例

也可以在Python 2中运行。1.1 编程范式

编程范式并没有统一的划分标准。本书重点分析其中两个范式:函数式编程和命令式编程。二者最重要的特征区别是状态。

在命令式语言(比如Python)中,计算的状态是通过不同命名空间中变量的值反映的。变量的值决定计算的当前状态,一条语句通过增加或改变(甚至是删除)变量来改变当前状态。“命令式”语言的每一条语句都是一个通过某种方式改变状态的命令。

本书主要关注赋值语句以及它如何改变状态。除此之外,Python中的其他语句包括用于在命令空间中改变变量规则的global、nonlocal等,用于改变变量所处语境的def、class和import等,用作判断条件确定一组语句如何改变计算状态的try、except、if、elif和else等。而循环语句,如for和while,则是将一组语句作为整体以重复改变计算状态。可见所有这些语句都是通过某种方式改变变量状态的。

理想状态下,每一条语句通过改变状态,推动计算从初始状态向期望的最终结果不断靠近。然而,这种“推动计算一步步向前”的模式难以验证。需要首先定义出最终状态,找到能达到该状态的语句,从而推导出达到该状态需要的前提条件,然后重复上述步骤,直到找到一个可接受的初始状态。

在函数式语言中,使用“对函数求值”这一更简单的概念代替改变变量值的“状态”,每次对函数求值都会在现有对象的基础上创建一个或多个新对象。函数式程序即函数的组合,相应的开发过程是:首先设计一组易于理解的底层函数,然后在此基础上设计符合业务需求的高级函数。相比于由复杂的流程控制组成的指令集合,高级函数更容易可视化。

形式上,函数求值更接近算法的数学表达。以简单的代数形式设计算法,便于处理特殊情况和边界条件,而且函数更有可能按照预期工作,也便于编写单元测试用例。

请注意,通常函数式程序比功能相同的命令式(面向对象或者过程式的)程序更加简洁明了和高效,但这些优点并不是自然而然的,需要仔细地设计,但付出的努力通常少于设计功能类似的过程式程序。1.2 细分过程范式

命令式编程可以再细分为多个子类别,本节简单介绍过程式编程和面向对象编程的区别,并重点讲解面向对象属于命令式编程的原因。过程式编程和面向对象编程虽然有区别,但它们与函数式编程的差别更大。

下面通过代码示例解释这些概念。有些人觉得这么做是在重新造轮子,然而这其实是抽象概念的具体表现。

对于某些计算过程,完全可以忽略Python的面向对象特点,只使用简单的数值计算。例如用下面的方法可以得到一组属性相同的数。s = 0for n in range(1, 10): if n % 3 == 0 or n % 5 == 0: s += nprint(s)

和m仅包括3或5的倍数。以上程序是严格过程式的,避免使用Python的任何面向对象特征。程序的状态由变量s和n定义,变量n的取值范围是1~10,每次循环中n的值依次增加,可以确定n == 10时循环结束。使用类似的原始数据结构,完全可以用C或者Java编写出功能相同的程序。

下面利用Python的面向对象编程(object-oriented programming,OOP)特征编写一段类似的代码。m = list()for n in range(1, 10): if n % 3 == 0 or n % 5 == 0: m.append(n)print(sum(m))

程序运行结果与前面的结果相同,但它内部维护了一个状态可变的集合对象m,计算状态由m和n定义。

m.append(n)和sum(m)令人费解的语法让一些开发者误以为Python不是纯粹的面向对象语言:它混合了function()和object.method()两种语法。然而事实上Python是纯粹的面向对象语言,一些语言(例如C++)允许使用非对象的原始数据类型,例如int、float和long。Python中没有原始数据类型,前缀的语法形式不会改变语言的本质。

严格地说,完全可以采用纯粹的面向对象风格,基于list类生成一个包含sum方法的子类。class Summable_List(list): def sum(self): s = 0 for v in self: s += v return s

接下来使用Summable_List()类代替list()方法初始化变量m,就可以用m.sum()方法代替sum(m)方法来对m求和了。该示例可以证明Python是纯粹的面向对象语言,前缀的使用仅是语法糖而已。

前面3个例子都基于变量值显式确定程序的状态,使用赋值语句改变变量值,推动计算前进。我们可以在程序中插入assert语句,确保程序状态完全按照要求变化。

关键之处不是命令式编程存在某种缺陷,而是函数式编程是一种思维方式的转变,这种改变适用于许多场景。下面介绍如何用函数式方法编写同一个算法,你会发现函数式编程并没有使算法显著变短或变快。1.2.1 使用函数式范式

在函数式编程中,求3或5的倍数可分为两部分。● 对一系列数值求和。● 生成一个满足某个条件的序列,例如3或5的倍数组成的序列。

一个列表的和的递归形式定义如下。def sumr(seq): if len(seq) == 0: return 0 return seq[0] + sumr(seq[1:])

可以把序列的和分为两种情况。基础形式:一个长度为0的序列,和为0。递归形式:序列的和等于序列中的第一个元素加上序列中后续元素的和。

由于递归形式的序列长度小于原序列,所以任何长度有限的序列最终都会退化为基础形式。

该函数运行示例如下。>>> sumr([7, 11])18>>> 7+sumr([11])18>>> 18+sumr([])0

第一个例子计算了包含多个值的列表之和。第二个例子演示了递归规则将第一个值seq[0]和后续所有值的和seq[1:]相加。最后一个计算包含了对空列表求和,其值定义为0。

这个例子中,代码最后一行的+运算符和初始值0表明其为求和。如果将运算符从+改为*,将初始值从0改为1,则表明其为序列乘积。后面会详细介绍这种抽象化方法。

对于一列值,可以用类似的方法递归,定义如下。def until(n, filter_func, v): if v == n: return [] if filter_func(v): return [v] + until(n, filter_func, v+1) else: return until(n, filter_func, v+1)

该函数的基础形式为:给定一个值v和一个上限n,如果v达到上限,则返回一个空列表。

根据filter_func()函数的不同返回值,递归形式有两种情况。如果v通过了filter_func()函数的测试,返回一个序列,则该序列的第一个元素是v,后续元素由until()作用于后续序列的返回值组成。如果v没有通过filter_func()函数的测试,将忽略该值,返回值由函数作用于剩余元素得到的值组成。

可以看到v在每次递归中递增,直到达到上限n,也就是基础形式。

下面介绍如何使用until()函数生成3或5的倍数。首先定义一个用于筛选数值的lambda对象。mult_3_5 = lambda x: x % 3 == 0 or x % 5 == 0(这里使用lambda定义简单函数是为了保持简洁。如果函数比较复杂,多于一行代码,请使用def语句。)

从命令提示符界面观察lambda的行为,如下所示。>>> mult_3_5(3)True>>> mult_3_5(4)False>>> mult_3_5(5)True

结合until()函数,它可以生成一系列3或5的倍数。

使用until()函数生成一系列值,如下所示。>>> until(10, lambda x: x % 3 == 0 or x % 5 == 0, 0)[0, 3, 5, 6, 9]

然后可以使用之前定义的递归版sum()函数计算一系列数值的和了。这里将用到的所有函数,包括sum()、until()和mult_3_5(),都定义为递归的,计算时不需要使用临时变量保存计算状态。

之后还会多次用到这种纯粹递归风格的函数来定义思想。请注意,许多函数式语言的编译器可以优化此类简单的递归函数,但Python不会进行此类优化。1.2.2 使用混合范式

下面介绍如何用函数式编码实现前面计算3或5的倍数的例子。混合型函数的实现代码如下所示。print(sum(n for n in range(1, 10) if n % 3 == 0 or n % 5 == 0))

这里使用了嵌入式生成器表达式迭代数值集合,并计算它们的和。range(1, 10)方法是可迭代的,所以它是一种生成器表达式,返回一个数值序列。n for n in range(1, 10) if n % 3 == 0 or n % 5 == 0稍复杂一些,但它也是可迭代表达式,返回一个数值集合。变量n与集合中的每个值绑定,表示集合的内容,而非当前的计算状态。sum()方法的输入值是一个可迭代表达式,输出最终计算结果:对象23。 绑定变量仅存在于生成器表达式内部,上述程序

中,变量n在生成器表达式之外是不可见的。

可以将表达式中的if从句看作独立的函数,便于用其他函数替换它。可以使用一个名为filter()的高阶函数代替上面生成器表达式中的if从句,第5章会详细介绍高阶函数。

这个例子中的变量n不同于前面两个命令式实现中的变量n,生成器表达式之外的for语句在本地命名空间中创建变量,而生成器表达式并不创建for语句式的变量。>>> sum(n for n in range(1, 10) if n % 3 == 0 or n % 5 == 0)23>>> nTraceback (most recent call last): File "", line 1, in NameError: name 'n' is not defined

生成器表达式绑定的范围外不存在变量n,即它并不定义计算状态。1.2.3 对象的创建过程

在某些情况下,观察中间对象有助于理解计算过程,但请注意,计算的路径并不是唯一的。当函数满足交换律和结合律的时候,改变求值顺序会创建出不同的中间对象。通过这种方式,可以在保证计算正确性的同时提升计算性能。

以下面这个表达式为例:>>> 1+2+3+410

下面讲解不同的计算过程是如何得到相同的计算结果的。由于+运算符满足交换律和结合律,有许多条计算路径都能得到相同结果。

根据选择待计算值顺序的不同,有以下两种主要的计算路径。>>> ((1+2)+3)+410>>> 1+(2+(3+4))10

第一种情形是从左向右结合并求值,此时对象3和6作为求值过程的中间对象被创建出来。

第二种情形则是从右向左结合并求值,中间对象是7和9。在这个简单的整数算术运算中,两种方式的表现相同,优化无助于提升性能。

涉及数组的追加操作时,改变结合方式可能会提升性能。

示例如下。>>> import timeit>>> timeit.timeit("((([]+[1])+[2])+[3])+[4]")0.8846941249794327>>> timeit.timeit("[]+([1]+([2]+([3]+[4])))")1.0207440659869462

可以看到,从左向右计算性能更佳。

对于函数式编程的设计,以任意顺序使用+运算符(或者add()函数),结果不变,即+运算符不影响使用方式。1.2.4 乌龟塔

严格意义上,Python的函数式编程并非函数式的,Python不是Haskell、OCaml或Erlang。请注意,真正完成计算过程的处理器硬件本身就不是函数式的,甚至严格意义上不是面向对象的,CPU实际上是过程式的。所有的编程语言都基于抽象、库、框架和虚拟机,这里

的抽象又基于更底层的抽象、库、框架和虚拟机。有个很形

象的比喻:整个世界被一只大乌龟驮在背上,这只大乌龟又

被另外一只更大的乌龟驮在背上,这只更大的乌龟又被一只

比它还大的乌龟驮在背上······一言以蔽之:全是乌龟。——佚名

抽象形成的层是无尽的。

更重要的是,这种抽象和虚拟机并不会影响通过Python的函数式特性设计软件。

即使在函数式语言内部,也存在更纯粹的语言和不太纯粹的语言。有些语言经常使用monads处理像文件系统输入、输出这样有状态的事务,另外一些语言则使用类似于Python的混合型环境,通过仔细地隔离含有状态的过程式动作来设计函数式的软件。

本书的函数式Python编程基于以下3层抽象。● 应用由函数组成,直到层层分解碰到对象。● 支撑函数式编程的Python运行时环境是由对象组成的,直到层层

分解碰到库。● 支撑Python运行的库就是驮着Python的乌龟。

更底层的操作系统和硬件有它们各自的乌龟塔,而且与我们要处理的问题无关。1.3 函数式编程经典示例

本节基于John Hughes的论文“Why Functional Programming Matters”,来分析一个函数式编程的经典实例,这篇文章出自论文集Research Topics in Functional Programming。

此论文深入分析了函数式编程,并提供了几个例子,我们只分析其中的一个:用Newton-Raphson算法求解函数(平方根函数)。

该算法的许多实现都是通过loops显式管理状态的,比如Hughes的论文中就给出了一段Fortran代码,通过有状态的命令式流程求解。

算法的主体是如何根据当前的近似值计算出下一个近似值。函数next_()以sqrt(n)的当前近似值x为参数,计算出下一个近似值,并确保最终解就在之前近似值的范围内,代码如下所示。def next_(n, x): return (x + n / x) / 2

该函数计算出一系列值,相近两个值的距离每次迭代减半,所以会迅速收敛到,即。这里没有将迭代函数命名为next(),以避免与Python的内置函数发生冲突,使用next_()保证在不冲突的前提下尽量清晰地表达出函数的功能。

在命令提示符界面使用该函数,如下所示。>>> n = 2>>> f = lambda x: next_(n, x)>>> a0 = 1.0>>> [round(x,4) for x in (a0, f(a0), f(f(a0)), f(f(f(a0))),)][1.0, 1.5, 1.4167, 1.4142]

首先定义收敛到的lambda表达式并赋值给变量f,将变量作为初始值,然后对一系列递归值求值:、,等等。将这些函数放在一个生成器表达式中,便于对返回值做指定精度的四舍五入,从而使计算结果更易读,并便于doctest使用。该序列会快速地向收敛。

我们可以编写一个函数,生成一个含的无限长序列,向平方根收敛。def repeat(f, a): yield a for v in repeat(f, f(a)): yield v

该函数利用近似函数f()和初始值a生成近似值。如果把近似函数替换成前面定义的next_()函数,就可以得到关于参数n平方根的一系列近似值。 其中repeat()函数要求f()函数只有一个参数,而定

义的next_()函数有两个参数。可以用一个匿名函数对象

lambda x: next_(n, x)绑定其中一个变量,创建next_()函数的

部分绑定版本。Python的生成器函数不能自动实现递归,必须显式迭代

递归结果,并一个一个单独生成计算结果。使用return

repeat(f, f(a))并不能多次循环生成一系列值,而会结束迭代

并返回一个生成器表达式。

有两种方法可以返回一系列值,而不是生成器表达式。● 编写显式for循环:for x in some_iter: yield x● 使用yield from语句:yield from some_iter

从递归生成器表达式中返回结果,这两种方法的效果相同,这里倾向于使用yield from语句。不过在有些情况下,yield结合复杂表达式,往往比相应的映射和生成器表达式更清晰。

当然,我们并不想计算无限长序列,只要两次迭代的近似值足够接近,就可以任取其中一个作为最终解。通常用希腊字母表示两个值足够接近,这里的含义是计算平方根的误差上限。

在Python中,我们需要设法从无限序列中一次取一个值,通常把复杂的递归包裹在简单的接口函数中,见如下代码片段。def within(ε, iterable): def head_tail(ε, a, iterable): b = next(iterable) if abs(a-b) <= ε: return b return head_tail(ε, b, iterable) return head_tail(ε, next(iterable), iterable)

首先定义了内部函数head_tail(),以误差允许范围、可迭代序列中的一个值a和可迭代序列的剩余部分iterable为参数,iterable的下一个值与变量b绑定。如果,两个值距离足够近,表明已找到平方根的解;否则以b为参数,递归调用函数head_tail(),以获取下一次迭代的近似值。

函数within()只需要用参数iterable的第一个值初始化内部的head_tail()函数,后面由递归自动完成。

有些函数式语言允许将一个值放回可迭代序列,在Python中,这类似于用unget()或者previous()方法将一个值追加到迭代器中,然而Python的可迭代数据结构并没有提供这种高级功能。

结合上面3个函数next_()、repeat()和within(),即可创建求平方根函数。def sqrt(a0, ε, n): return within(ε, repeat(lambda x: next_(n,x), a0))

repeat()函数基于next_(n, x)函数生成一个(可能的)无限长序列,当两次迭代值之差小于时,within()即停止序列继续生成值。

使用这个sqrt()函数需要提供一个初始值a0和误差值,表达式sqrt(1.0, .0001, 3)表示从初始估计值1.0开始计算,误差值为0.0001。对于大多数应用,初始值可以选择1.0,不过初始值与实际平方根越接近,函数收敛越快。

以上近似算法的最初版本是用Miranda语言编写的,可以看到Miranda和Python的实现之间有一些显著区别,主要是Miranda可以构建cons,可以通过类似于unget的方式将值放回可迭代对象中。Miranda和Python的这种对比说明了Python适用于实现多种函数式编程技术。1.4 EDA

本书后续章节的函数式编程示例大多来自EDA领域,该领域包含很多处理复杂数据集的算法和技术,函数式编程往往能很好地连接起问题领域和解决方案。

虽然每个人有自己的行事风格,但处理EDA领域的问题通常可以划分成下面几个阶段。● 准备数据:主要是抽取和变换源应用中的数据。例如解析原始数

据格式,对数据执行某种程度的清洗(比如移除不可用数据和异

常数据等),这是函数式编程擅长的领域。● 数据探测:对数据进行初始画像,通常使用一些基本的统计函数

来完成,这也是函数式编程擅长的领域。用专业术语讲,该阶段

我们关注数据的单变量和双变量统计特征,实际上就是数据的描

述性统计特征值,包括平均值、中位数、众数等。数据探测还可

能涉及数据可视化,但本书不探讨这个主题,因为它不怎么采用

函数式编程。如果你感兴趣,可以尝试一些工具包,例如

SciPy。访问如下网址,可获取有关SciPy工作原理和使用方法的

更多信息。● https://www.packtpub.com/big-data-and-business-intelligence/learning-scipy-numerical-and-scientific-computing● https://www.packtpub.com/big-data-and-business-intelligence/learning-python-data-visualization● 数据建模与机器学习:主要解决如何从已有模型中提取新数据,

但本书不涉及,因为从数学角度看有些模型十分复杂,讨论这些

问题无助于理解函数式编程。● 评估与比较:当存在多个可用模型时,就需要针对当前数据评估

哪个模型更适合。此过程主要涉及计算模型常用的一些描述型统

计特征值,函数式设计技术能有所帮助。

EDA的目标是创建模型为应用决策提供依据。很多情况下,一个模型可能就是一个简单的函数。使用函数式编程方式,便于将已有模型应用于新数据,生成业务人员可以理解的结果。1.5 小结

本章主要介绍了编程范式,并比较了函数式编程和另外两种常用的命令式编程范式的区别。本书旨在向读者介绍Python的函数式编程特征。我们讲到了Python并非纯粹的函数式编程语言,Python函数式编程的指导思想是将简洁明了的函数式编程与性能优化相结合,形成有Python特色的混合式编程方法。

下一章将详细介绍函数式编程的5种基本技术,这些技术构成了Python混合函数式编程的核心要素。第 2 章 函数式编程的特点

Python内置了函数式编程的大部分特性。编写函数式的Python代码要求我们尽量避免使用命令式(包括过程式和面向对象式)编程技术。

本章将介绍以下函数式编程技术。● 头等函数和高阶函数,也称“纯函数”。● 不可变数据结构。● 严格求值与非严格求值,也称“积极求值”与“惰性求值”。● 用递归代替显式循环语句。● 函数类型系统。

回顾一下上一章提到的概念,首先,纯粹的函数式编程避免了由于使用变量赋值导致程序显式维护计算状态而带来的复杂性;其次,Python不是纯粹的函数式语言。

本书不会给出函数式编程的确切概念。Python不是纯粹的函数式语言,并且严格的定义并无帮助。我们将关注公认的重要函数式特性,不涉足有争议的模糊地带。

本章的示例代码将涉及Python 3的类型提示语法。类型提示有助于开发者阐述函数定义的核心目标,这里使用mypy工具分析类型提示。与提供单元测试和代码静态分析的pylint类似,mypy也是构建高质量软件工具链的重要组成部分。2.1 头等函数

总体而言,函数式编程简洁明了,因为函数可以用作其他函数的参数或者返回值,后续会给出很多这样的例子。

要做到这一点,函数必须是运行时环境中的头等对象。在C等语言中,函数不是运行时中的对象,然而在Python中,函数通常是通过def语句创建的对象,且其他函数可以使用。我们还可以通过创建可调用对象,或者将lambda表达式赋给变量来创建函数。

创建函数即创建一个带有属性的对象,如下所示。>>> def example(a, b, **kw):... return a*b...>>> type(example)>>> example.__code__.co_varnames('a', 'b', 'kw')>>> example.__code__.co_argcount2

这里我们创建了一个对象:example,其为function类。此对象包含很多属性,与该函数对象关联的__code__对象也含有自己的属性。其具体实现细节不重要,重要的是Python中的函数是头等对象,我们完全可以像处理其他对象一样处理函数,比如上面的代码示例展示了函数对象的其中两个属性。2.1.1 纯函数

为了提高程序可读性,使用的函数要尽量没有副作用,即所谓的“纯函数”。使用纯函数的好处包括可以通过改变求值顺序实现优化,而其最重要的优势在于概念简单、测试方便。

在Python中,编写纯函数式代码要求代码的作用域为本地,具体而言,就是避免使用global语句。nonlocal语句的使用也可能对作用域产生副作用,也应留意,虽然副作用限制在一个嵌套函数里。实际上,达到这些要求并不难。可以把纯函数看作普通的Python编程实践。

并没有简单的方法能保证Python函数没有副作用,编码时不小心违反了纯函数规则也是常有之事。如果实在担心可能违反规则,可以写一个函数,使用dis模块扫描给定函数的__code__.co_code属性,即编译后的代码,检查是否包含全局引用。它能对内部闭包和__code__.co_freevars元组方法的使用给出提示。然而为了避免极少出现的情形而运用这类复杂的技术有些得不偿失,因此后续不会展开讨论。

Python的lambda表达式是纯函数。虽然不太推崇,但确实可以通过lambda表达式创建纯函数。

将lambda表达式赋给变量以创建函数的示例如下。>>> mersenne = lambda x: 2 ** x - 1>>> mersenne(17)131071

将lambda表达式赋给变量mersenne,即可得到一个纯函数,实际上是一个包含单一参数x,并返回单个值的可调用对象。因为lambda表达式中不能包含赋值语句,所以它总为纯函数,适用于函数式编程。2.1.2 高阶函数

使用高阶函数可以使程序简洁明了。高阶函数以其他函数为参数,或者用函数作为返回值。我们可以使用高阶函数将简单的函数组合成复合函数。

以Python的max()函数为例,我们可以提供一个函数作为其参数,来改变max()函数的行为。

待处理的数据如下。>>> year_cheese = [(2000, 29.87), (2001, 30.12), (2002, 30.6), (2003,30.66),(2004, 31.33), (2005, 32.62), (2006, 32.73), (2007, 33.5),(2008, 32.84), (2009, 33.02), (2010, 32.92)]

可以如下所示使用max()函数。>>> max(year_cheese)(2010, 32.92)

其默认行为会比较列表中的每个元组,按元组下标为0的元素比较大小,返回最大的元素所在的元组。

由于max()函数是高阶函数,因此可以添加一个函数作为其参数。这里用一个lambda表达式作为它的函数参数,如下所示。>>> max(year_cheese, key=lambda yc: yc[1])(2007, 33.5)

在这个例子中,max()函数用lambda表达式定义的函数作为比较依据,返回了下标为1的最大元素所在的元组。

Python提供了许多高阶函数,后面(主要是第5章)会介绍Python提供的许多高阶函数以及编写高阶函数的方法。2.2 不可变数据结构

函数式编程中不能使用变量跟踪计算的状态,所以我们需要研究如何使用不可变对象,比如可以使用元组和命名元组构建复杂的不可变数据结构。

不可变对象的概念在Python中并不陌生。程序使用不可变元组比使用可变对象的性能要好。在某些情况下,使用不可变对象时,我们需要重新考虑算法,以避免改变对象所带来的开销。

我们将(几乎)完全避免使用类定义,虽然在一门面向对象编程的语言里这么做似乎不合逻辑。通过阅读本书你会了解,函数式编程并不需要有状态的对象。可以定义可调用对象,并通过它们把互相关联的函数放在同一个命名空间内,这类对象可以在多个级别上进行配置。通过可调用对象创建缓存也很简单,而且能大幅提升性能。

下面介绍一个处理不可变对象的常用设计模式:wrapper()函数。由元组组成的列表是常见的数据结构,我们经常用以下两种方式处理它们。● 使用高阶函数:如前所述,为max()提供一个lambda表达式——

max(year_cheese, key=lambda yc: yc[1])。● 使用“打包-处理-拆包”模式:在函数式语境中,这种模式可以

表述为unwrap(process (wrap(structure)))。

例如,看看以下命令片断。>>> max(map(lambda yc: (yc[1], yc), year_cheese))[1](2007, 33.5)

这个例子很好地展示了上面说的三部曲模式:打包数据结构,获取打包后数据结构的最大值,然后拆包。

首先是打包。map(lambda yc: (yc[1],yc), year_cheese)把列表中的每一项转换成一个二元组,其中用于比较的项后面跟着原始项,这里用于比较的项是yc[1]。

接下来用max()函数处理逻辑。因为之前已经把需要比较的项变成了二元组的第一个元素,所以使用max()的默认方式就可以了,不再需要它的高阶函数能力。

最后用下标[1]提取最终结果,即拆包。这里是从max()返回的结果中通过取第二个元素得到目标元组。

这类打包、拆包的操作在函数式编程中很常用,所以有些函数式语言为这类操作提供了专门的函数,例如fst()和snd()等,这样就可以使用前缀式语法,而不必使用[0]或[1]这样的下标了。我们可以实现这些函数,并将其应用于“打包-处理-拆包”模式中,如下所示。>>> snd = lambda x: x[1]>>> snd(max(map(lambda yc: (yc[1], yc), year_cheese)))(2007, 33.5)

上例中,通过定义snd()函数实现取元组第二个元素的功能,这样实现的“打包-处理-拆包”模式更易读。我们使用map(lambda... , year_cheese)打包原始数据项,使用max()进行处理,最后用snd()函数从返回的元组中提取第二个元素。

第13章将介绍上述基于lambda表达式的fst()函数和snd()函数的替代解决方案。2.3 严格求值与非严格求值

函数式编程高效,原因之一是将计算推迟到需要的时候进行。惰性(也称“非严格”)求值非常重要,Python内置了对它的支持。

Python中,逻辑运算符and、or和if-then-else都是非严格的。有时也称之为“短路”运算符,因为它们不需要计算全部参数就能得到最终结果。

以下命令片断展示了and运算符的惰性求值特性。>>> 0 and print("right")0>>> True and print("right")right

执行上面的代码时,如果and运算符左边的表达式值为False,不会对右边的表达式求值;只有当左边的表达式值为True时,才会对右边的表达式求值。

除此之外,Python使用严格求值规则。除了逻辑运算符,表达式都是严格地从左向右求值的。一组语句也是严格按顺序求值的,列表字面量和元组亦然。

当创建一个类时,它的各个方法是严格按顺序定义的。在类的定义中,所有方法在创建之后(默认)被放入一个字典,并不会保持之前的顺序。如果在一个类中创建两个名字相同的方法,那么由于严格的求值顺序,只会保留后面的方法,前面定义的方法会被覆盖掉。

Python的生成器表达式和生成器函数是惰性的,在求值时,这些表达式不会马上计算出所有的可能结果。如果不把计算过程显式打印出来,很难看到惰性求值的结果。下面的例子演示了通过引入带有副作用的range()函数生成值的过程。def numbers(): for i in range(1024): print(f"= {i}") yield i

每生成一个值,该函数就将其打印出来,以此给出调试提示。如果这个函数是严格求值的,将会打印出所有1024个值,但由于它是惰性的,所以只会按需生成值。 Python 2的range()函数是严格求值的,创建后就会

生成所有包含的值。Python 3的range()函数是惰性求值的,

不会创建大型数据结构。

可以用惰性求值的方式使用这个带日志功能的numbers()函数。下面编写一个只求部分值(而非全部)的函数。def sum_to(n: int) -> int: sum: int = 0 for i in numbers(): if i == n: break sum += i return sum

sum_to()函数的类型提示表明它接收整型值作为参数,并返回整型值。sum变量也使用了Python 3语法::int,表明它是一个整型值。sum_to()函数不对numbers()函数取所有值,在取了前几个值后,就通过break语句退出了。下面的日志展示了numbers()创建值的方式。>>> sum_to(5)= 0= 1= 2= 3= 4= 510

后面会讲到,Python生成器函数的一些特点使得它在应用于简单函数时会出现一些小麻烦,例如一个生成器只能用一次,因此在使用Python的惰性生成器表达式时要小心。2.4 用递归代替循环语句

函数式编程不依赖循环语句,也不产生跟踪循环状态的开销,而使用相对简单的递归语句。在一些语言中,代码中的递归会在编译阶段被编译器通过尾调用优化(tail call optimization,TCO)技术转换成循环语句。本节简单介绍一些递归用法,第6章会详细讲解递归技术。

接下来介绍如何通过遍历测试一个数是否为质数。质数是只能被1和本身整除的自然数。我们可以定义一个简单的低性能算法,检查2到这个数之间的所有自然数能否整除它。该算法的优点是简单,可以作为欧拉计划中问题的解法。如果你对求解该问题的高性能算法感兴趣,可以参考米勒-拉宾素性检验算法。

我们使用“互质”表示两个数除了1之外没有其他公约数,比如2和3是互质的,而6和9不是互质的,因为它们除了1之外,还有公约数3。

这样就可以把测试一个数是否为质数,转换为下面这个问题:自然数是否与自然数的任意取值互质,其中,或者简化成:是否与所有满足条件的数互质?

不妨把上面的陈述转换为如下数学公式。

对应的Python表达式如下。not any(n % p == 0 for p in range(2,int(math.sqrt(n)) + 1))

从数学公式转换而来的更准确的写法是all(n % p != 0 ... ),但它需要对所有的可能取值严格求值,而上面的not any版本会在碰到第一个True值的时候结束计算。

这个简单的表达式中包含一个for循环,所以不是纯粹的、无状态的函数式编程风格。我们可以把它转换成处理一个集合的函数:自然数是否与内的所有数互质?其中符号[)代表半开区间:下限在范围内,上限不在范围内,与Python函数range()的取值方式一致。由于我们只在自然数范围内讨论问题,所以平方根的值会自动转换为整数。

因此可以如下定义测试质数的公式。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载