绪论

序对可以为我们提供用于构造复合数据的基本“粘接剂”,鉴于Python中tuple中元素不可变的性质,我们通过list来实现序对,如[1, 2]。Python的PyListObject对象中实际是存放的是PyObject*指针, 所以可以将PyListObject视为vecter<PyObject*>。这是一种盒子与指针表示方式(list内的元素表示为一个指向对象盒子的指针)。对于[1, 2],可将其视为以下结构:

我们不仅可以用[]去组合起各种数值,也可以用它取组合起其它序对。这样,序对就是一种通用的建筑砌块,通过它可以构造所有不同种类的数据结构来。比如想组合数值1, 2, 3, 4,我们可以用[[1, 2], [3, 4]]的方式(下图左),也可以用[[1, [2, 3]], 4](下图右):

可以构建元素本身也是序对的序对,这种能力称为[]闭包性质。注意,这里的闭包是来自抽象代数的术语(不是Python语法中那个闭包)。抽象代数中,如果将某个运算(操作)作用于某个集合的特定元素 ,产出的仍然是该集合的元素,则称该集合元素在该运算之下封闭。我们这里说组合数据对象的操作满足闭包性质,指通过它组合起数据对象得到的结果本身还可以通过同样的操作再进行组合。

闭包性质可以使我们构建层次性的结构,这种结构由一些部分构成,而其中的各个部分又是由它们的部分构成,并且可以继续下去。下面我们介绍用序对来表示序列

2.2.1 序列的表示

利用序对可以够造出的一类有用结构是序列——一批数据对象的有序汇集。利用序对表示序列的方式很多,一种最直接的表示方式为[1, [2, [3, [4, None]]]]如下图所示:

我们不妨将这种通过嵌套序对形成的序列称为链表。因为Python本身不内置链表结构,我们不妨用序对来实现链表:

class LinkedList():
def __init__(self, *items) -> None:
"""提供两种初始化方式:序对或多个元素
"""
if isinstance(items[0], list):
self.pair = items[0]
else:
self.pair = self._construct(*items) def _construct(self, *items):
"""递归地构造链表
"""
if items == ():
return None
else:
item, *rest = items
return [item, self._construct(*rest)] def __repr__(self):
"""重写打印函数
"""
return "-->".join(map(str, self._flatten(self.pair))) def _flatten(self, pair):
"""遍历链表,返回其一维展开
"""
if pair is None:
return []
else:
return [pair[0]] + self._flatten(pair[1]) @property
def head(self):
"""获取链表头部元素
"""
return self.pair[0] @property
def rest(self):
"""获取链表头部元素之外的元素,并以链表形式返回
"""
if self.pair[1] is None:
return None
else:
return LinkedList(self.pair[1])

这样,我们就可以方便地构造链表并将其打印输出了:

print(LinkedList(1, 2, 3, 4))
# 1-->2-->3-->4

注意,None用于表示序对的链结束。在语言设计上可能有以下争论:None应该是个普通的名字吗?None应该算是一个普通的名字吗?None应该算是一个符号吗?他应该算是一个空表吗?在Python中,解决此问题的手段是将None的类型规定为<class 'NoneType'>

表操作

利用序对将元素的序列表示为链表之后,我们就可以使用常规的程序设计技术,通过获取链表的headrest的方式完成对链表的各种操作了。如下面的过程list-ref实际参数是一个表和一个数n,它返回这个表中的第n项:

def list_ref(items, n):
if n == 0:
return items.head
else:
return list_ref(items.rest, n-1) print(list_ref(LinkedList(1, 4, 9, 16, 25), 3)) # 16

length过程则用于返回表中的项数:

def length(items):
if items is None:
return 0
else:
return 1 + length(items.rest) print(length(LinkedList(1, 3, 5, 7))) # 4

或者写为迭代的形式(此处用尾递归的形式,即递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这样就无需保存返回值,可在常数空间内执行迭代型计算):

# 以迭代的方式计算lengths(尾递归)
def length(items):
def length_iter(a, count):
if a is None:
return count
else:
return length_iter(a.rest, count + 1)
return length_iter(items, 0) print(length(LinkedList(1, 3, 5, 7))) # 4

当然, Python解释器默认是不开启尾递归优化的,需要用其他黑魔法实现,参考《Python开启尾递归优化!》

还有一种常见操作是append,如对odds[1, 3, 5, 7]squares:[1, 4, 9, 16, 25]append(odds, squares)[1 3 5 7 1 4 9 16 25]append(squares, odds)[1 4 9 16 25 1 3 5 7],也可以通过递归实现:

def append(lk_list1, lk_list2):
if lk_list1 is None:
return lk_list2.pair
else:
return [lk_list1.head, append(lk_list1.rest, lk_list2)] odds = LinkedList(1, 3, 5, 7)
squares = LinkedList(1, 4, 9, 16, 25)
print(LinkedList(append(odds, squares))) # 1-->3-->5-->7-->1-->4-->9-->16-->25
print(LinkedList(append(squares, odds))) # 1-->4-->9-->16-->25-->1-->3-->5-->7

对链表的映射

另外一个特别拥有用的操作时将某种操作应用于一个链表的所有元素,得到所有结果构成的表。下面的过程将一个链表中的所有元素按给定因子做一次缩放:

def scale_list(items, factor):
if items is None:
return None
else:
return [items.head * factor, scale_list(items.rest, factor)] print(LinkedList(scale_list(LinkedList(1, 2, 3, 4, 5), 10)))
# 10-->20-->30-->40-->50

我们可以抽象出这一具有一般性的想法,将其中的公共模式表述为一个高阶函数(接收其它函数做为参数)。

def my_map(proc, items):
if items is None:
return None
else:
return [proc(items.head), my_map(proc, items.rest)] print(LinkedList(my_map(abs, LinkedList(-10, 2.5, -11.6, 17))))
# 10-->2.5-->11.6-->17
print(LinkedList(my_map(lambda x: x**2, LinkedList(1, 2, 3, 4, 5))))
# 1-->4-->9-->16-->25

这里的公共模式,其实就类似于设计模式中的模板方法,参见设计模式:模板方法

现在我们可以用map给scale_list一个新定义:

def scale_list(items, factor):
return LinkedList(my_map(lambda x: x*factor, items)) print(scale_list(LinkedList(1, 2, 3, 4, 5), 10))
# 10-->20-->30-->40-->50

map是一种很重要的结构,不仅因为它代表了一种公共模式,而且因为它建立起了一种处理表的高层抽象(与今日的Scala何其相似!),在老版本的scale_list中,程序的递归结构将人的注意力吸引到对表中元素的逐个处理中。通过map定义的scale_list抑制了这种细节层面上的情况,强调的是从元素表到结果表的一个缩放变换。这两种定义形式之间的差异,并不在于计算机会执行不同的计算过程(其实不会),而在于我们对同一个过程的不同思考方式。 从作用上看,map帮我们建起了一层抽象屏障,将实现表转换过程的实现,与与如何提取表中元素以及组合结果的细节隔离开。

2.2.2 层次性结构

注意,由于下面由于我们会涉及更复杂的数据结构,我们统一将序列就用Python内置的列表表示

我们下面来看元素本身也是序列的序列。比如我们可以认为[[1, 2], 3, 4]是将[1, 2]做为元素加入序列[3, 4]而得。这种表结构可以看做是树,即序列中的元素就是树的分支,而那些本身也是序列的元素就形成了树中的子树:

递归是处理树结构的一种很自然的工具,因为我们常常可以将对于树的操作归结为对它们的分支的操作,再将这种操作归结为对分支的分支的操作,如此下去,直至达到了树的叶子。如类似2.2.1中用length统计序列长度,我们通过以下代码统计树叶数目:

def count_leaves(tree):
if not tree:
return 0
elif isinstance(tree, int):
return 1
else:
return count_leaves(tree[0]) + count_leaves(tree[1:])
tree = [[1, 2], 3, 4]
print(count_leaves(tree)) # 4

对树的映射

map是处理序列的一种强有力抽象,与此类似,map与递归结合也是处理树的一种强有力抽象。类似于2.2.1中用scale_list过程对序列元素进行缩放,我们也可以设计scale_tree过程,该过程以一个因子和一棵叶子为数值的树作为参数,返回一颗具有同样形状的树,该树中的每个数值都乘以了这个因子:

def scale_tree(tree, factor):
if not tree:
return []
if isinstance(tree, int):
return tree * factor
else:
return [scale_tree(tree[0], factor)] + scale_tree(tree[1:], factor) tree = [1, [2, [3, 4], 5], [6, 7]]
print(scale_tree(tree, 10))
# [10, [20, [30, 40], 50], [60, 70]]

实现scale_tree的另一种方法是将树看成子树的序列,并对它使用map。我们在这种序列上做映射,一次对各棵子树做缩放,并返回结果的表。对于基础情况,也就是当被处理的树是树叶时,就直接用因子去乘它:

def scale_tree(tree, factor):
return list(map(lambda sub_tree: scale_tree(sub_tree, factor)
if isinstance(sub_tree, list)
else sub_tree * factor, tree))
tree = [1, [2, [3, 4], 5], [6, 7]]
print(scale_tree(tree, 10))
# [10, [20, [30, 40], 50], [60, 70]]

此处的map我们直接采用Python语言内置的map,当然也可以自己实现my_map,如下:

def my_map(proc, items):
if items == []:
return []
else:
return [proc(items[0])] + my_map(proc, items[1:])

2.2.3 序列做为一种约定的界面

数据抽象可以让我们设计出不被数据表示细节纠缠的程序,使程序保持很好的弹性。在这一节里,我们将要介绍与数据结构有关的另一种强有力的设计原理——使用约定的界面。

在1.3节中我们看到,通过实现为高阶过程的程序抽象,可以让我们抓住处理数值数据的一些程序模式。而在复合数据上工作做出类似的操作,则对我们操控数据结构的方式有着深刻的依赖性。如考虑一个与2.2.2节中的count_leaves类似的过程,它以一棵树为参数,计算出那些值为奇数的叶子的平方和:

def sum_odd_squares(tree):
if not tree:
return 0
elif isinstance(tree, int):
if tree % 2 == 1:
return tree**2
else:
return 0
else:
return sum_odd_squares(tree[0]) + sum_odd_squares(tree[1:])

从表面上看,这一过程与下面的过程很不一样。下面的这个过程给定一个整数\(n\),对\(\forall k \leqslant n\)计算Fib(k)并筛选出其中为偶数的值,其中Fib(k)为第\(k\)个Fibonacci数(设第0个Fibonacci数为0):

def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)

该过程表示如下:

def even_fibs(n):  # 枚举从0到n的整数
def next(k):
if k > n:
return []
else:
f = fib(k) # 对每个整数计算其fib
if f % 2 == 0: # 过滤结果,选出其中偶数
return [f] + next(k + 1) # 累积结果
else:
return next(k+1)
return next(0)
print(even_fibs(5)) # [0, 2] (即[0 1 1 2 3 5]中的偶数为[0, 2])

虽然sum_odd_squares过程和even_fibs过程结构式差异非常大,但是对于两个计算的抽象描述却会揭露出它们间极大的相似性。sum_odd_squares过程:

  • 枚举出一棵树的树叶
  • 过滤它们,选出其中的奇数
  • 对选出的每一个数求平方
  • 用+累积起得到的结果

sum_odd_squares过程:

  • 枚举从\(0\)到\(n\)的整数
  • 对每个整数计算相应的Fibonacci数
  • 过滤它们,选出其中的偶数
  • connect累计得到的结果

注意,connect函数用于对将两个数值对象连接为列表或将数值对象加入一个列表,定义如下:

def con(x, y):
# y规定为int,x可以为int或list
if isinstance(x, int):
return [x] + [y]
else:
return x + [y]

信号工程师可能会发现,这种过程其实可以描述为信号流过一系列的级联处理步骤,每个步骤实现程序方案中的一部分。如下图所示:

遗憾的是,上面两个过程的定义并没有展现这种信号流结构。具体地说,我们的两个过程将enumerate工作散布在程序中各处,并将它与mapfilterreduce混在一起。如果我们能够重新组织这一程序,使信号流结构明显表现在写出的过程中,将会大大提高代码的清晰性。

其中mapfilterreduce算子可以采用Python内置函数,也可以自己实现。自己实现的话可以这样写:

def my_map(proc, sequence):
if not sequence:
return []
else:
return [proc(sequence[0])] + my_map(proc, sequence[1:]) print(my_map(lambda x: x**2, [1, 2, 3, 4, 5]))
# [1, 4, 9, 16, 25] def my_filter(predicate, sequence):
if not sequence:
return []
elif predicate(sequence[0]):
return [sequence[0]] + my_filter(predicate, sequence[1:])
else:
return my_filter(predicate, sequence[1:]) print(my_filter(lambda x: x % 2, [1, 2, 3, 4, 5]))
# [1, 3, 5] # print(list(accumulate([1,2,3]))) def my_reduce(op, sequence):
if sequence[-1] and not sequence[:-1]:
return sequence[-1]
else:
return op(my_reduce(op, sequence[:-1]), sequence[-1]) print(my_reduce(add, [1, 2, 3, 4, 5])) # 15
print(my_reduce(mul, [1, 2, 3, 4, 5])) # 120
print(my_reduce(con, [1, 2, 3, 4, 5])) # [1, 2, 3, 4, 5]

为了简便起见,我们下面mapfilterreduce算子统一采用Python内置函数。

除了这三个算子之外,我们还需要枚举(enumerate)出需要处理的数据序列。对于even-fibs,我们需要生成一个给定区间里的整数序列:

def enumerate_interval(low, high):
if low > high:
return []
else:
return [low] + enumerate_interval(low + 1, high) print(enumerate_interval(2, 7)) # [2, 3, 4, 5, 6, 7]

对于sum_odd_squares,则需要枚举出一棵树的所有树叶:

# 枚举一棵树所有的树叶:
def enumerate_tree(tree):
if not tree:
return []
elif isinstance(tree, int):
return [tree]
else:
return enumerate_tree(tree[0]) + enumerate_tree(tree[1:]) print(enumerate_tree([1, [2, [3, 4], 5]])) # [1, 2, 3, 4, 5]

现在,我们就可以像上面的信号流图那样重新构造sum_odd_squareseven-fibs了。

sum_odd_squares的构造方法如下:

def sum_odd_squares(tree):
return reduce(add,
map(lambda x: x**2,
filter(lambda x: x % 2,
enumerate_tree(tree)))) print(sum_odd_squares([1, 2, 3, 4, 5])) # 35

even-fibs的构造方法如下:

def even_fibs(n):
return reduce(con,
filter(lambda x: not x % 2,
map(fib,
enumerate_interval(0, n)))) print(even_fibs(5)) #[0, 2]

将程序表示为一些针对序列的操作,这样做的价值就爱在于能帮助我们得到模块化的程序设计。而在工程设计中,模块化结构是控制复杂性的一种威力强大的策略。如同信号处理中设计者从标准的过滤器和变换装置中选出一些东西来级联,从而构造出各种系统。同样地,序列操作也形成了一个可以混合和匹配使用的标准程序元素库。

如我们在另一个产生前\(n+1\)个Fibonacci数的平方的程序里,就可以使用取自过程sum_odd_squareseven-fibs的片段:

def list_fib_squares(n):
return reduce(con,
map(lambda x: x**2,
map(fib,
enumerate_interval(0, n)))) print(list_fib_squares(5)) # [0, 1, 1, 4, 9, 25]

也可以重新安排有关的各个片段,将它们用在产生一个序列中所有奇数的平方之乘积的程序里:

def product_of_squares_of_odd_elements_sequence(sequence):
return reduce(mul,
map(lambda x: x**2,
filter(lambda x: x % 2, sequence))) print(product_of_squares_of_odd_elements_sequence([1, 2, 3, 4, 5])) # [0, 1, 1, 4, 9, 25]

我们同样可以采用序列操作的方式,重新去形式化各种常规的数据处理应用。假定有一个人事记录的序列,现在希望找出其中薪水最高的程序员的工资。假定有一个salary返回记录中的工资,谓词函数is_programmer检查某个记录是不是程序员,此时我们就可以写:

def salary_of_hightest_paid_programmer(records):
return reduce(max,
map(salary,
filter(is_programmer, records)))

在这里,用表实现的序列被做为一种方便的界面,我们可以利用这种界面去组合起各种处理模块

参考

SICP 2.2: 层次性数据和闭包性质(Python实现)的更多相关文章

  1. python操作txt文件中数据教程[4]-python去掉txt文件行尾换行

    python操作txt文件中数据教程[4]-python去掉txt文件行尾换行 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考文章 python操作txt文件中数据教程[1]-使用pyt ...

  2. python操作txt文件中数据教程[3]-python读取文件夹中所有txt文件并将数据转为csv文件

    python操作txt文件中数据教程[3]-python读取文件夹中所有txt文件并将数据转为csv文件 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考文献 python操作txt文件中 ...

  3. python操作txt文件中数据教程[2]-python提取txt文件

    python操作txt文件中数据教程[2]-python提取txt文件中的行列元素 觉得有用的话,欢迎一起讨论相互学习~Follow Me 原始txt文件 程序实现后结果-将txt中元素提取并保存在c ...

  4. 大数据,why python

    大数据,why python ps, 2015-12-4 20:47:46 python" title="大数据,why python">http://www.op ...

  5. 让Chrome浏览器抓包接口数据秒变 python 代码

    简介 uncurl是一个库,允许您将curl请求转换为使用requests 的python代码.由于Chrome网络检查器具有的“copy as cURL”,因此该工具对于用python重新创建浏览器 ...

  6. 一行导出所有任意微软SQL server数据脚本-基于Python的微软官方mssql-scripter工具使用全讲解

    文章标题: 一行导出所有任意微软SQL serer数据脚本-基于Python的微软官方mssql-scripter工具使用全讲解 关键字 : mssql-scripter,SQL Server 文章分 ...

  7. [转]大数据时代,python竟是最好的语言?

      随着大数据疯狂的浪潮,新生代的工具Python得到了前所未有的爆发.简洁.开源是这款工具吸引了众多粉丝的原因.目前Python最热的领域,非数据分析和挖掘莫属了.从以Pandas为代表的数据分析领 ...

  8. 【数据科学】Python数据可视化概述

    注:很早之前就打算专门写一篇与Python数据可视化相关的博客,对一些基本概念和常用技巧做一个小结.今天终于有时间来完成这个计划了! 0. Python中常用的可视化工具 Python在数据科学中的地 ...

  9. 大数据时代的Python金融应用-Day1-Python与金融应用概述

    一.Python语言的主要特征 1.开源性 Python和大多数的支撑库和工具都是开源的,通常可以非常灵活的使用而且有开放的协议. 2.解释性 也可以使用Cpython完成将解释性语言转化为实施可执行 ...

随机推荐

  1. 列举 Spring Framework 的优点?

    由于 Spring Frameworks 的分层架构,用户可以自由选择自己需要的组件. Spring Framework 支持 POJO(Plain Old Java Object) 编程,从而具备持 ...

  2. Java 中用到的线程调度算法是什么?

    抢占式.一个线程用完 CPU 之后,操作系统会根据线程优先级.线程饥饿情况等 数据算出一个总的优先级并分配下一个时间片给某个线程执行.

  3. Goland环境配置——Goland上的第一个Go语言程序

    安装好goland后,开始编写一个简单程序测试环境是否可用. 新建项目:按File-new-project进入如图new project界面,在Go一栏内的Location里填写项目路径(D:\GOO ...

  4. c语言 相关小知识

    软件运行与内存关系(垃圾数据) 内存是在操作系统的统一管理下使用的! 1.软件在运行前需要向操作系统申请访问存储空间,在内存空闲空间足够时,操作系统将分配一段内存空间并将外存中软件拷贝一份存入该内存空 ...

  5. 4.1 ROS元功能包

    4.1 ROS元功能包 场景:完成ROS中一个系统性的功能,可能涉及到多个功能包,比如实现了机器人导航模块,该模块下有地图.定位.路径规划...等不同的子级功能包.那么调用者安装该模块时,需要逐一的安 ...

  6. 无单位数字和行高 —— 别说你懂CSS相对单位

    前段时间试译了Keith J.Grant的CSS好书<CSS in Depth>,其中的第二章<Working with relative units>,书中对relative ...

  7. JS+CSS实现数字滚动

    最近在实现一个显示RGB颜色数值的动画效果时,尝试使用了writing-mode(书写模式)及 text-orientation来实现文字的竖直方向的排列,并借助CSS的transition(过渡)来 ...

  8. ES 架构及基础 - 1

    Elasticsearch 是一款分布式,RESTful 风格的搜索和数据分析引擎,可以从海量的数据中高效的找到相关信息.如 wiki 用 ES 进行全文检索及其高亮,Github 用其检索代码,电商 ...

  9. Python入门-迭代器和生成器

    迭代演示 # 传统数据生成缺陷演示,编号操作未全部使用,会占用内存 #合适的做法,是需要的时候再生产,而不是全部生成好了再用 def generator(maxnum): print("[代 ...

  10. Servlet实现登录注册

    1.注册页面register.html <!DOCTYPE html> <html lang="en"> <head> <meta cha ...