Python3某些强大的特性与Lisp的关系

概述

  python作为解释性语言,拥有许多优秀和强大的特性,理解这些特性的本质和来源对我们学习这门语言有着很大的帮助。
  python吸收了历史上以及现存的许多语言的特性,比如结构化(structured)、
面向对象(OOP)、函数式编程(functional programming)、动态类型(dynamically typed)以及垃圾收集(garbage-collected)等。其中包括对Lisp语言函数式编程的传统,python提供map、filter、和reduce等函数;提供列表解析(list comprehensions)、字典(dictionaries)、集合(set)和生成器(generator)表达式等;同时在标准库中提供functools和itertools模块,这两个模块的特性来源于函数式编程语言Haskell和Standard ML。
  这些特性不仅赋予了语言强大的抽象能力,而且反映了一些关于计算科学的本质问题。

闭包(closure)

  在数学中,closure一般是指域的封闭性,比如整数域在加法运算下是封闭的。在数系发展的历史上,先出现的是非零自然数域,即1,2,3,4,…,负数是不存在也是不可理解和忽视的,这种情况下,我们说非零自然数域在减法上是不封闭的。后来一方面人们逐渐在生活中不自觉的使用负数,比如某个人的欠款,另一方面则来自数学本身内在的发展要求,促使人们扩展数系,引入零和负数。数学的内在动力就是呼吁这种内在逻辑的自洽性,而不关心现实世界是否可以找到对应的概念。后来数系逐渐扩展到整数域、有理数域、实数域以及复数域,数学的抽象能力逐渐增强。
  在计算机编程语言领域,closure有两种含义:一种是和数学中类似,即一个数据结构中可以包含它自身的嵌套结构,一个运算的结果可以作为它自己操作的参数;另外一种是指在lambda表达式中实现自由变量(free variables)的技术。
  python的基本数据结构和类型,如list,tuple,set, dict, str都支持闭包,例如:

1
2
3
>> a_list = [1, 2, 3, [4, 5, 6]]
>> a_tuple = ('a','b',(1, 2, 3), '3')
>> a_dict = ['a':123, 'b':['c':1,'d':2]]

另外在这种数据结构上的运算也满足闭包,例如:

1
2
>> added_list = a_list + a_list
[1, 2, 3, [4, 5, 6], 1, 2, 3, [4, 5, 6]]

  在Lisp中,只有一种组合数据的胶水(glue),即序对(pair)。由序对构造的数据对象称为表结构(list-structured)数据。这种结构当然也是满足闭包特性的,例如:

1
2
3
4
5
=> (define a (list 1 2 3))
=> (define b (list 1 (list 4 5) 2 3))
=> (define c (list a b))
=> c
(( 1 2 3)(1 (4 5) 2 3))

Lisp中所有的表达式也都可以看作表结构数据。list数据结构也是一种序对,或者说是序对的链,其中链的结尾使用约定的标记’()。例如:

1
2
3
4
=> (define a (list 1 2 (list 3 4) 5))
=> (define b (cons 1 (cons 2 (cons (cons 3 (cons 4 '())) (cons 5 '())))))
=> (equal? a b)
#t ;; a 与 b的值是相等的

  那么到目前为止,这种闭包特性带给了我们什么重要的东西呢?我们可以看到,在Lisp中,我们所知道的更为复杂的数据结构,例如链表、栈、队列、树、堆等都可以使用通用的pair来构造,闭包特性赋予了pair表达层次性数据的能力。另外,这种特性非常适合递归算法,例如对树的几乎所有遍历、查找、替换和映射的操作,都可以使用递归算法简单而优雅的解决。Lisp拥有非常简单的语法,或者说程序员几乎不需要考虑语法的问题,从而获得了在语义上的强大抽象能力。
  在python中,也可以基于通用的tuple或者list来构造复杂的数据结构,但python的做法则尽量是使用标准库来解决,在那里可以专心解决性能和效率的问题。这是一种小而强大、简单而强大的理念,python具有很强的可扩展性,且避免过早优化,其语言核心尽量保持小、整洁和简单,然后将大量功能放入标准库中,这种做法鼓励了模块化的设计。

装饰器(decorator)

  关于这个主题,可以参考链接 Python3 Decorators。装饰器的特性类似于宏(macro),基于该特性使得python可以获得动态获取、修改函数(functions)内部状态的强大能力。
  在Lisp中,函数具有第一级状态(first-class functions),即函数本身可以作为返回值,可以作为函数的参数,可以用变量命名,可以包含在数据结构中。我们也可以说,函数本身满足闭包性质。首先我们以SICP Exercise 1.41来表现这种特性:

1
2
3
(define (double f) (lambda (x) (f (f x))))
(define (inc x) (+ x 1))
(((double (double double)) inc) 5))

过程double以过程f作为参数,并返回一个过程(以lambda表达式的形式),该过程将f执行两次。所以,最后一行表达式的值是21,f被执行了16次。python中可以使用装饰器实现类似的函数:

1
2
3
4
5
6
7
8
def double(f):
def my_double(x):
return f(f(x))
return my_double

@double
def inc(x):
return x + 1

  Lisp中的这种特性可以非常方便地建立更一般的抽象,比如说高阶函数,它可以增加代码的重用性以及程序的模块化。而python的装饰器特性主要还是获得开始时所说得那种动态修改函数性质的能力,它不需要修改原来函数的代码却可以获得额外的功能,使得程序更加清晰和简洁,比如,定义property等。当然它也可以用来进行定义高阶抽象,比如表格化或者记忆化(Memoized),这种方法可以大大提高某些算法(动态规划、树形递归等)的效率,它针对一类问题而不是某个特定的问题。在 PythonDecoratorLibrary Memorized 例子中使用Memoized装饰器可以在不改动原有算法代码的基础上提高算法的效率。

高阶函数

  在Lisp中过程与数据可以相互转换,过程本身也可以成为对象。python中可以将函数本身看成对象,不同的变量可以引用(reference)同一个函数对象,一个函数也就可以作为另一个函数的参数。我们以map为例来表现这种特性的威力,假设我们要将一列数加倍,我们可以写下面的代码:

1
2
def double(a_list):
return [i * 2 for i in a_list]

那么如果需要将一列字母全部变为大写,我们可以写:

1
2
def capitalize(char_list):
return [a.upper() for a in char_list]

我们还可以给出大量这样的例子,那么这些例子中是否包含共同的、一般性的概念呢?是否可以用一种统一的方式解决一类的问题呢?如果我们抽象的看待这类问题,将对list中数据的操作函数作为参数由用户定义,就是说map是一种对list数据的映射,这种映射本身由用户自己定义。我们的代码如下:

1
2
3
4
5
6
def map(f, a_list):
return [f(i) for i in a_list]
def capitalize(char_list):
return map(str.upper, char_list)
def double(a_list):
return map(lambda x: x*2, a_list)

capitalize和double函数都可以基于map来定义,我们说map有更高的抽象能力。
  python中的map、filter、sorted和reduce函数都是基于上诉的思想,这种高阶函数的使用使得程序简单、清晰和模块化,更重要的是它们可以协作组合完成更加复杂的功能。

函数式编程模块

  functools库中主要的工具是partial类,它可以使用默认参数来包装(wrap)一个可调用的对象(callable object),结果对象可以看作原来的那个对象。这里的可调用对象既可以是单独的函数(standalone function)也可以是类(class)。在某些场景,对象会作为某些方法的参数,但该对象不能带有自己的默认参数,而partial通过wrap原有的对象和其默认参数获得一个新的对象,该对象可以作为无参对象传入其他方法。
total_ordering装饰器可以自动增加类的比较方法( rich comparison methods)。
  itertools库中的函数可以实现惰性求值(lazy evaluation),数据只有等到它被需要的时候才会产生,这样可以更有效率的使用存储资源,而且可以用来表达更加复杂的算法。
其中itertools.map和python内置的map函数有些不同,前者返回的是iterator,对输入数据的操作还没有进行,它只在需要的时候产生数据;而后者返回的就是映射后的最终数据。
  operator提供函数式的运算操作符。