进一步理解Python中的函数编程


Posted in Python onApril 13, 2015

我们最好从最难的问题开始:“到底什么是函数编程 (FP)?”一个答案可能会说 FP 就是您在使用例如 Lisp、Scheme、Haskell、ML、OCAML、Clean、Mercury、Erlang(或其它一些)语言进行编程时所做的。这是一个稳妥的答案,但不能很确切地阐明问题。不幸的是,即使是函数程序员他们自己也很难对 FP 究竟是什么有个一致的认识。“盲人摸象”的故事用来形容这一情况似乎很合适。还可以放心地将 FP 与“命令编程”(使用例如 C、Pascal、C++、Java、Perl、Awk、TCL 以及其它大多数语言所执行的操作,至少是在很大程度上)进行对比。

从个人角度来说,我会将函数编程粗略地描绘为至少具有以下几个特征。称得上函数性的语言使这些事情变得简单,而使其它事情变得困难或不可能:

  •     函数是第一类(对象)。即,可以对“数据”进行的每样操作都可以使用函数本身做到(例如将一个函数传递给另一个函数)。
  •     将递归用作主要的控制结构。在某些语言中,不存在其它“循环”构造。
  •     重点集中在列表 LISt 处理(例如,名称 Lisp )。列表经常和子列表的递归一起使用以替代循环。
  •     “纯”函数语言能够避免副作用。这不包括在命令语言中最普遍的模式,即指定第一个,然后将另一个值指定给同一个变量来跟踪程序状态。
  •     FP 不鼓励或根本不允许出现 语句,取而代之是使用表达式求值(换句话说,即函数加上自变量)。在很纯粹的情况下,一个程序就是一个表达式(加上支持的定义)。
  •     FP 关心的是计算 什么而不是 如何计算。
  •     许多 FP 利用了“更高等级”函数(换句话说,就是函数对一些函数操作,而这些函数又对其它函数操作)。

函数编程的提倡者认为所有这些特征都导致更快速的开发更短以及错误更少的代码。而且,计算机科学、逻辑和数学领域的高级理论学家发现证明函数语言和程序的正式性能比命令语言和程序容易得多。
固有的 Python 函数能力

自从 Python 1.0 以来,Python 具有上面列出的大多数 FP 特征。但对于大多数 Python 特性,它们以一种非常混合的语言呈现。很大程度上是因为 Python 的 OOP 特性,您可以使用希望使用的部分而忽略其余部分(直到在稍后需要它为止)。使用 Python 2.0, 列表内涵添加了一些 非常棒的“句法上的粉饰”。虽然列表内涵没有添加什么新的能力,但它们使许多旧的能力看起来好了 许多。

Python 中 FP 的基本元素是函数 map() 、 reduce() 和 filter() ,以及运算符 lambda 。在 Python 1.x 中, apply() 函数对于将一个函数的列表返回值直接应用于另一个函数也很方便。Python 2.0 为这一目的提供了改进的语法。可能让人吃惊,但很少的这几个函数(以及基本运算符)几乎足以编写任何 Python程序;特别是,所有的流控制语句( if 、 elif 、 else 、 assert 、 try 、 except 、 finally 、 for 、 break 、 continue 、 while 、 def )可以只使用 FP 函数和运算符以函数风格处理。虽然实际上消除程序中的所有流控制命令可能只对加入“混乱的 Python”竞争(与看上去非常象 Lisp 的代码)有用,但是理解 FP 是如何使用函数和递归来表示流控制是值得的。

消除流控制语句

在我们执行消除联系时要考虑的第一件事是 Python “短路”了布尔表达式的求值这一事实。这样就提供了表达式版本的 if / elif / else 块(假设每块都调用一个函数,通常总有可能这样安排)。下面是具体方法:
清单 1. Python 中的“短路”条件调用

# Normal statement-based flow control 
  
if
   <cond1>: func1() 
  elif
   <cond2>: func2() 
  else
  :   func3() 
 
  # Equivalent "short circuit" expression 
(<cond1> 
  and
   func1()) 
  or
   (<cond2> 
  and
   func2()) 
  or
   (func3()) 
 
  # Example "short circuit" expression 
>>> x = 3 
>>> 
  defpr
  (s): 
  return
   s 
>>> (x==1 
  and
   pr(
  'one')) 
  or
   (x==2 
  and
   pr(
  'two')) 
  or
   (pr(
  'other')) 
  'other' 
>>> x = 2 
>>> (x==1 
  and
   pr(
  'one')) 
  or
   (x==2 
  and
   pr(
  'two')) 
  or
   (pr(
  'other')) 
  'two'

表达式版本的条件性调用似乎不过是个位置诀窍;不过,如果我们注意到 lambda 运算符必须返回表达式时,就更有趣了。因为 -- 如前所示 -- 表达式可以通过短路来包含条件块,所以 lambda 表达式在表达条件返回值中非常普通。在我们的示例上构建:
清单 2. Python 中 Lambda 短路

>>> pr = 
  lambda
   s:s 
>>> namenum = 
  lambda
   x: (x==1 
  and
   pr(
  "one")) \ 
....     
  or
   (x==2 
  and
   pr(
  "two")) \ 
....     
  or
   (pr(
  "other")) 
>>> namenum(1) 
  'one' 
>>> namenum(2) 
  'two' 
>>> namenum(3) 
  'other'

函数作为第一类对象

上面的示例已经显示出函数在 Python 中所处的第一类的地位,但以很微妙的方式。在使用 lambda 操作创建 函数对象 时,我们有一些完全常规的事物。正是因为这样,我们可以将对象与名称 "pr" 和 "namenum" 绑定,使用的方法和将数字 23 或字符串 "spam" 与这些名称绑定的方法完全相同。但正如我们可以使用数字 23 而无需将它与任何名称绑定一样(换句话说,象函数自变量一样),我们可以使用用 lambda 创建的函数对象而不用将它与任何名称绑定。一个函数只是我们在 Python 中对其执行某些操作的另一个值。

我们对第一类对象所执行的主要操作是将它们传递给 FP 内置函数 map() 、 reduce() 和 filter() 。这些函数中的每一个都接受函数对象作为其第一个自变量。

    map() 对指定列表中每个对应的项执行传递的函数,并返回结果列表。
    reduce() 对每个后续项执行传递的函数,返回的是最终结果的内部累加;例如 reduce(lambda n,m:n*m, range(1,10)) 意味着“10 的阶乘”(换句话说,用每一项乘上前一次相乘的乘积)。
    filter() 使用传递的函数对列表中的每一项“求值”,然后返回经过甄别的,通过了传递函数测试的项的列表。

我们还经常将函数对象传递给自己的定制函数,但它们通常等同于上述内置函数的组合。

通过将这三种 FP 内置函数进行组合,可以执行惊人的一系列“流”操作(都不使用语句,而只使用表达式)。

Python 中的函数循环

替换循环与替换条件块一样简单。 for 可以直接转换成 map() 。对于我们的条件执行,我们需要将语句块简化成单一函数调用(我们正逐步接近通常的做法):
清单 3. Python 中的函数 'for' 循环

for
   e 
  in
   lst: func(e) 
  # statement-based loop
map(func,lst)  
  # map()-based loop

另外,对于连续程序流的函数方法有类似的技术。即,命令编程通常包含接近于“做这样,然后做那样,然后做其它事。”这样的语句。 map() 让我们正好做到这一点:
清单 4. Python 中的函数连续操作

# let's create an execution utility function
do_it = 
  lambda
   f: f()
  # let f1, f2, f3 (etc) be functions that perform actions
map(do_it, [f1,f2,f3]) 
  # map()-based action sequence

通常,我们的整个 main 程序可以是 map() 表达式和一系列完成程序所需要执行的函数。第一类函数的另一个方便的特性就是可以将它们放在一个列表中。

while 的转换稍微复杂了一些,但仍然可以直接进行:
清单 5. Python 中的函数 'while' 循环

# statement-based while loop
  
while
   <cond>:
 <pre-suite>
 
  if
   <break_condition>:
 
  break
 else
  :
 <suite>
  # FP-style recursive while loopp
  
defwhile_block
  ():
 <pre-suite>
 
  if
   <break_condition>:
 
  return
   1
 
  else
  :
 <suite>
 
  return
   0
while_FP = 
  lambda
  : (<cond> 
  and
   while_block()) 
  or
   while_FP()
while_FP()

while 的转换仍需要 while_block() 函数,它本身包含语句而不仅仅是表达式。但我们需要对该函数做进一步的消除(例如对模板中的 if/else 进行短路)。另外,因为循环主体(按设计)无法更改任何变量值,所以 <cond> 很难用在一般的测试中,例如 while myvar==7 (那么,将在 while_block() 中修改全部内容)。添加更有用条件的一个方法是让 while_block() 返回一个更有趣的值,然后将这个返回值与终止条件进行比较。有必要看一下这些消除语句的具体示例:
清单 6. Python 中的函数 'echo' 循环

# imperative version of "echo()"
  
defecho_IMP
  ():
 
  while
   1:
 x = raw_input(
  "IMP -- ")
 
  if
   x == 
  'quit':
  
  break
 else
  print
   x
echo_IMP()
  # utility function for "identity with side-effect"
  
defmonadic_print
  (x):
 
  print
   x
 
  return
   x
  # FP version of "echo()"
echo_FP = 
  lambda
  : monadic_print(raw_input(
  "FP -- "))==
  'quit' 
  or
   echo_FP()
echo_FP()

我们所完成的是设法将涉及 I/O、循环和条件语句的小程序表示成一个带有递归的纯表达式(实际上,如果需要,可以表示成能传递到任何其它地方的函数对象)。我们 的确 仍然利用了实用程序函数 monadic_print() ,但这个函数是完全一般性的,可以在我们以后创建的每个函数程序表达式中重用(它是一次性成本)。请注意,任何包含 monadic_print(x) 的表达式所 求值 的结果都是相同的,就象它只包含 x 一样。FP(特别是 Haskell)对于“不执行任何操作,在进程中有副作用”的函数具有“单一体”意思。

消除副作用

在除去完美的、有意义的语句不用而代之以晦涩的、嵌套的表达式的工作后,一个很自然的问题是:“为什么?!”我对 FP 的所有描述都是使用 Python 做到的。但最重要的特性 -- 可能也是具体情况中最有用的特性 -- 是它消除了副作用(或者至少对一些特殊领域,例如单一体,有一些牵制作用)。绝大部分程序错误 -- 和促使程序员求助于调试来解决的问题 -- 之所以会发生,是因为在程序执行过程期间,变量包含了意外的值。函数程序只不过根本就不为变量分配值,从而避免了这一特殊问题。

让我们看一段相当普通的命令代码。它的目的是打印出乘积大于 25 的几对数字的列表。组成各对的数字本身是从另外两个列表中挑选出的。这种操作与程序员在他们程序段中实际执行的操作差不多。实现这一目的的命令方法如下:
清单 7. “打印大乘积”的命令 Python 代码

# Nested loop procedural style for finding big products
xs = (1,2,3,4)
ys = (10,15,3,22)
bigmuls = []
  # ...more stuff...
  
for
   x 
  in
   xs:
 
  for
   y 
  in
   ys:
 
  # ...more stuff...
  
   if
   x*y > 25:
  bigmuls.append((x,y))
  
  # ...more stuff...
# ...more stuff...
  
print
   bigmuls

这个项目太小,以至于没有什么可能出错。但我们的目的可能嵌在要同时实现许多其它目的的代码中。用 "more stuff" 注释的那些部分是副作用可能导致错误发生的地方。在这些地方中的任何一处,变量 xs 、 ys 、 bigmuls 、 x 、 y 有可能获得假设节略代码中的意外值。而且,在执行完这一段代码后,所有变量都可能具有稍后代码可能需要也可能不需要的一些值。很明显,可以使用函数/实例形式的封装和有关作用域的考虑来防止出现这种类型的错误。而且,您总是可以在执行完变量后 del 它们。但在实际中,这些指出类型的错误非常普遍。

目标的函数方法完全消除了这些副作用错误。以下是可能的一段代码:
清单 8. “打印大乘积”的函数 Python 代码

bigmuls = 
  lambda
   xs,ys: filter(
  lambda
   (x,y):x*y > 25, combine(xs,ys))
combine = 
  lambda
   xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = 
  lambda
   lst,n: reduce(
  lambda
   s,t:s+t, map(
  lambda
   l,n=n: [l]*n, lst))
  print
   bigmuls((1,2,3,4),(10,15,3,22))

在示例中,我们将匿名 ( lambda ) 函数对象与名称进行绑定,但这不是一定必要的。我们可以只嵌套定义。这样做是出于可读性目的;但也是因为 combine() 是一种随处可得的很好实用程序函数(从两个输入列表中产生所有元素对的列表)。随后的 dupelms() 主要只是帮助 combine() 发挥作用的一种方法。即使这一函数示例比命令示例更冗长,但一旦考虑到实用程序函数可以重用,那么 bigmuls() 中的新代码本身可能比命令版本中的代码数量还要少一些。

这种函数示例真正的优势在于绝对不会有变量更改其中的任何值。稍后的代码中没有 可能的不曾预料到的副作用(较早的代码中也不会有)。很明显,它本身没有副作用并不能保证代码 正确,但即使这样,这也是个优点。不过请注意,Python(与许多函数语言不同) 不能 防止名称 bigmuls 、 combine 和 dupelms 的重新绑定。如果 combine() 在程序的稍后部分中开始有其它意义,则所有努力都前功尽弃。您可以逐步建立一个 Singleton 类来包含这种类型的不可变绑定(例如 s.bigmuls 等);但本专栏并不涉及这一内容。

特别值得注意的一个问题是我们的特定目的是对 Python 2 中的新特性进行定制。最好的(也是函数的)技术既不是上面提供的命令示例,也不是函数实例,而是:
清单 9. "bigmuls" 的列表内涵 Python 代码

print
   [(x,y) 
  for
   x 
  in
   (1,2,3,4) 
  for
   y 
  in
   (10,15,3,22) 
  if
   x*y > 25]

结束语

我已介绍了使用函数等价物替换每个 Python 流控制构造所使用的方法(在过程中消除了副作用)。对特定程序进行有效转换将带来一些额外的考虑,但我们已经知道内置函数是常规而完整的。在稍后的专栏中,我们将考虑一些更高级的函数编程技术;希望能够探索函数风格的更多利弊。

Python 相关文章推荐
python开发中range()函数用法实例分析
Nov 12 Python
在CentOS上配置Nginx+Gunicorn+Python+Flask环境的教程
Jun 07 Python
举例讲解Python编程中对线程锁的使用
Jul 12 Python
python清理子进程机制剖析
Nov 23 Python
python ansible服务及剧本编写
Dec 29 Python
Python3.4实现远程控制电脑开关机
Feb 22 Python
python高阶爬虫实战分析
Jul 29 Python
简单了解pytest测试框架setup和tearDown
Apr 14 Python
在python中list作函数形参,防止被实参修改的实现方法
Jun 05 Python
浅谈keras中的目标函数和优化函数MSE用法
Jun 10 Python
python输出结果刷新及进度条的实现操作
Jul 13 Python
基于Pygame实现简单的贪吃蛇游戏
Dec 06 Python
Python中的异常处理简明介绍
Apr 13 #Python
python中的装饰器详解
Apr 13 #Python
Python生成器(Generator)详解
Apr 13 #Python
Python中函数的多种格式和使用实例及小技巧
Apr 13 #Python
在Python中使用SimpleParse模块进行解析的教程
Apr 11 #Python
Python的动态重新封装的教程
Apr 11 #Python
简单的Python的curses库使用教程
Apr 11 #Python
You might like
session 的生命周期是多长
2006/10/09 PHP
php 生成WML页面方法详解
2009/08/09 PHP
SWFUpload与CI不能正确上传识别文件MIME类型解决方法分享
2011/04/18 PHP
php float不四舍五入截取浮点型字符串方法总结
2013/10/28 PHP
PHP批量上传图片的具体实现方法介绍.
2014/02/26 PHP
PHP中array_keys和array_unique函数源码的分析
2016/02/26 PHP
PHP用户验证和标签推荐的简单使用
2016/10/31 PHP
php 解决扫描二维码下载跳转问题
2017/01/13 PHP
thinkphp整合系列之极验滑动验证码geetest功能
2019/06/18 PHP
jQuery ajax在GBK编码下表单提交终极解决方案(非二次编码方法)
2010/10/20 Javascript
基于jquery的无刷新分页技术
2011/06/11 Javascript
suggestion开发小结以及对键盘事件的总结(针对中文输入法状态)
2011/12/20 Javascript
Jquery easyui 下loaing效果示例代码
2013/08/12 Javascript
使用JQuery实现智能表单验证功能
2016/03/08 Javascript
jquery实现无刷新验证码的简单实例
2016/05/19 Javascript
promise处理多个相互依赖的异步请求(实例讲解)
2017/08/03 Javascript
浅谈Angularjs中不同类型的双向数据绑定
2018/07/16 Javascript
JavaScript 反射和属性赋值实例解析
2019/10/28 Javascript
vue+springboot+element+vue-resource实现文件上传教程
2020/10/21 Javascript
[04:03]2014DOTA2西雅图国际邀请赛 LGD战队巡礼
2014/07/07 DOTA
在 Python 应用中使用 MongoDB的方法
2017/01/05 Python
python+unittest+requests实现接口自动化的方法
2018/11/29 Python
Python基于聚类算法实现密度聚类(DBSCAN)计算【测试可用】
2018/12/26 Python
20行python代码的入门级小游戏的详解
2019/05/05 Python
简单了解python中对象的取反运算符
2019/07/01 Python
介绍一款python类型检查工具pyright(推荐)
2019/07/03 Python
python二进制文件的转译详解
2019/07/03 Python
python pyqtgraph 保存图片到本地的实例
2020/03/14 Python
python 获取谷歌浏览器保存的密码
2021/01/06 Python
伦敦所有西区剧院演出官方票务代理:Theatre Tickets Direct
2017/05/26 全球购物
将一个文本文件的内容按倒序打印出来
2015/01/05 面试题
Linux常见面试题
2016/10/04 面试题
大学团支书的自我评价分享
2013/12/14 职场文书
警察先进个人事迹材料
2014/05/16 职场文书
建筑专业毕业生自荐信
2014/05/25 职场文书
基层党支部承诺书
2015/04/30 职场文书