Python 和 Mathematica 中的列表

刚刚接触 Python,真是没有料想 Python 里面也有列表这种东西,并且特性还和 Mathematica 中的十分相像。Python 和 Mathematica 相像的地方真的很多:比如 Python 的交互模式,输入输出和 Mathematica 很是相像;都有列表;都同时支持过程式编程和函数式编程……当然,现在还没有学到那么深。

下面简单做一个整理,总结一下 Python 中的列表和 Mathematica 中列表的异同。

怎么去看待列表?

Python

Python 提供了很多复合数据类型,列表是其中之一,Python 中的列表是序列(sequence)的一种,序列是可以通过整数下标获取元素(使用__getitem__()方法)、能够获得长度(使用__len__()方法)的可迭代对象(iterable)。可迭代对象指每次只能返回其一个成员的对象。

Python 的列表是有序的、可变长的、可以对元素进行修改的。列表元素可以是相同类型,也可以是不同类型,并且可以将列表作为列表元素。

Mathematica

Mathematica 中,列表是一个常见的普通目的表达式类型。Mathematica 的核心思想,是将所有对象用符号表达式表示,符号表达式的形式为:

头部[参数序列]

其中参数序列以逗号“,”间隔,Mathematica(或是说 Wolfram Language)语言中其他的句法可以说都是语法糖,无一例外地可以用符号表达式去替换。

列表也不例外,列表具有头部List,Mathematica 中列表的长度和深度是任意的,但不能通过某种规则去试图构造一个长度或深度为无穷的列表。

与 Python 相同,Mathematica 中的列表同样是有序的、可变长的、可以修改元素、列表元素类型不限也不必相同、可以嵌套列表。

怎么创建一个列表?

直观来说,Python 中的列表和 Mathematica 中的最大不同,就是用的括号不一样。Python 中,列表元素置于方括号中(在 Mathematica 中,方括号被用来区别符号表达式中的头部与参数序列),Mathematica 中,列表元素置于花括号中(在 Python 中,花括号被用于表示字典)。其实对于 Mathematica 来说,用花括号表示列表仅仅是为了简洁罢了。任何一对花括号{}都等价于List[]

Python

1
mylist = [1, 2, 3, 4, 5]

Mathematica

1
2
3
mylist = {1, 2, 3, 4, 5}
(* An alternative way *)
mylist2 = List[1, 2, 3, 4, 5]

列表操作

检索

给出下标去检索元素。Python 和 Mathematica 的元素访问方式很类似,都不允许越界访问,都是可以去倒数元素访问的。这可是比 C 那种动不动就越界友好得多的(或许数组和列表是不具可比性的)。但是在下标策略上,Python 和 Mathematica 又有很大的不同。

Python

Python 的索引是从 0 开始的,即下标 0 引用第一个元素。

1
2
>>> mylist[0]
1

倒数元素用负数,比如最后一个元素用下标 -1。

1
2
>>> mylist[-1]
5

Mathematica

Mathematica 的索引是从 1 开始的,这倒是十分与众不同。而 Mathematica、Matlab 和 R都是这个策略。

1
2
In[20]:= mylist[[1]]
Out[20]= 1

而倒数元素用负数时,与 Python 相同,同样是最后一个元素用下标 -1。

1
2
In[21]:= mylist[[-1]]
Out[21]= 5

这样设计显得十分对称。但是下标 0 哪去了?

1
2
In[22]:= mylist[[0]]
Out[22]= List

下标 0 获得的是表达式的头部。Mathematica 里面双方括号m[[i, j, …]]等价于Part[m, i, j, …]。该写法不仅可以用于列表,也可以用于任何表达式,Mathematica 中约定下标 0 的元素为表达式的头部。在 StackExchange 的 Mathematica 版上有个答案非常好,答主 evanb 画了一张图,可以说是秒懂。对于f = F[1, 2, 3, 4, 5],这个表达式可以看成:

1
2
3
4
5
6
7
------------
F
5 1

4 2
3
------------

F作为表达式的头部具有下标 0,其余的只要以此类推就可以得到下标值。

为什么要从 0 开始计数?(译自 Edsger Wybe Dijkstra 的手记)

为表示自然数序列 $2,3,\cdots ,12$ 又不想加那没有什么用的三个点,我们有四种表示习惯:

a) $2\leq i<13$
b) $1<i\leq 12$
c) $2\leq i\leq 12$
d) $1<i<13$

是否有理由让我们更加偏爱其中的某一种写法呢?没错,的确有。我们注意到 a) 和 b) 两种写法具有一种优势,那就是边界之间的差值和序列的长度是相等的。我们同样注意到,可以推出的一点是,这两种写法对于两个相邻的序列来说,都可以保证一者的上边界恰好是另一者的下边界。以上两个观察所得都是正确的,但是没能让我们在 a) 和 b) 中做出选择,所以我们还要再论。

存在最小的自然数。而 b) 和 d) 的写法会使下边界成为一个例外——它们使得从最小自然数开始的序列不得不用非自然数的数字去做下边界。这很丑,因此考虑到下边界,我们更偏爱 a) 和 c) 中 $\leq$ 的写法。今考虑序列从最小的自然数开始的情形,序列的上边界被包含在了区间之中,会使得当序列成为空集时,其上边界会达到一个非自然数。这很丑,因此考虑到上边界,我们更偏爱 a) 和 d) 中 $<$ 的写法。综上所述,a) 是我们更喜欢的一种写法。

【评注】由施乐帕克研究中心开发的程序设计语言 Mesa,在整数间隔方面的表示对应了以上四种写法。在充分接触 Mesa 语言后,我们发现另外三种写法一直都是混乱与错误的源头。出于这一经历的考量,我们强烈建议 Mesa 程序员不要用这三种写法,尽管语言是允许的。我在这里提到了这一实验证据,暂且先这样讲,是因为有些人不适应这个结论,也没有照做。【评注毕】

对于长度为 $N$ 的序列,我们想要通过下标去区别它的元素。那么下一个伤脑筋的问题出现了,起始元素应该用什么下标值呢?遵循写法 a),如果下标值始于 1 的话,下标的范围就会是 $1\leq i<N+1$;而从 0 开始的话,这个范围就漂亮多了,会是 $0\neq i<N$。所以让序数从 0 开始吧:元素序数(下标)和序列中在该元素之前的元素数目是相等的。这则故事的寓意是,(在这么多个世纪之后!)我们最好还是将 0 作为最自然的那个数吧。

【评注】许多程序设计语言在设计的时候没有考虑这一个细节,在 FORTRAN 中,下标总是起始于 1 的;在 ALGOL 60 和 PASCAL 中,采用了 c) 的写法;在最近的 SASL 中,又恢复了 FORTRAN 的写法:SASL 中的序列同时又是正整数的函数。可惜!【评注毕】

上述议论是由最近的一件小事所引起的。有一次,我的一位搞数学的同事(不搞计算机),大发雷霆,怒斥那些年轻的计算科学研究者“迂腐”,就是因为他们习惯从 0 开始计数。他有意地把这种最合理的写法看成一种挑衅。(我那“xx毕”的写法也被他看成了一种挑衅,可是这么写很有用:我认识一个学生,有一次考试他默认问题到页尾就结束了,结果差点不及格。)我觉得 Antoy Jay 说得很好,“对企业精神来说,就像和各种宗教一样,应该将异教徒剔除。这并不是因为他一定是错的,而是因为他有可能是对的。”

元素的修改、增添和删除

Python

Python 中的列表提供了appendinsertpop方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mylist = [1, 2, 3, 4, 5]
# To change an element
mylist[2] = 2
# mylist = [1, 2, 2, 4, 5]

# To append an element to the list
mylist.append(6)
# mylist = [1, 2, 2, 4, 5, 6]

# To insert an element to the list
mylist.insert(1, 1.5)
# mylist = [1, 1.5, 2, 2, 4, 5, 6]

# To delete the last element
mylist.pop()
# mylist = [1, 1.5, 2, 2, 4, 5]

# To delete a certain element
mylist.pop(1)
# mylist = [1, 2, 2, 4, 5]

Mathematica

Mathematica 也有对应的AppendInsertDelete函数,但是 Mathematica 中的列表操作函数一般返回一个新的列表,而保持原有的列表不变。这和Part返回的并不是新实例,而是原列表的一部分是不同的。但是 Mathematica 依旧保留了一部分带有To函数,比如AppendTo函数,作为特殊的赋值函数以实现对列表本身的修改。AppendTo[s, element]这种写法与s = Append[s, element]Set[s, Append[s, element]]完全等价的。根据使用Trace的观察,我们也能够验证这两种写法在 Mathematica 内部的等效性。

切片

Python 和 Mathematica 中的列表都可以去选取一部分,这在 Python 中称作“切片”,类似于 Mathematica 中的子列表。Python 中的切片和 Mathematica 中通过部分Take获得子列表的语法是相似的,并且都和元素检索的语法是几乎相同的(在 Mathematica 中检索元素本身就是Take函数实现的)。但是在 Python 中,切片总会创建一个新的实例,而在 Mathematica 中,部分则可以去进行赋值并作用于原对象。

Python 中的切片

mylist[m:n:s]为取出下标为m到下标为n但不包括n的元素,步长为s。对于mn负数也是同样被支持的。这里,创建切片中的下标上下边界的选取和上文中那段翻译所提到的策略是一致的,都是半闭半开的写法。

Mathematica 中的选取子列

Mathematica 中选取子列表使用Part函数,即双方括号[[…]]。其他列表操作函数,比如FirstLastTakeDropRestMost都是Part的特例。Part所取出的子列是允许进行赋值操作的。

Part 所操作的对象不限于列表,适用于所有非原子表达式。Mathematica 中原子表达式如Complex[1, 2]Rational[1, 2],分别表示复数$1+2i$和分数$\frac{1}{2}$,这类对象在AtomQ作用下返回True

具体的语法为expr[[m;;n;;s]],为exprm个元素到第n个元素,步长为s的子列。Mathematica 的策略是mn都是取到等于号的,这一点与 Python 的半闭半开的策略不同。其中;;为跨度Spanm;;n;;s等价于Span[m, n, s]


关于迭代与列表生成器有了领悟之后再续。

参考

  1. Why do Mathematica list indices start at 1?

  2. Why numbering should start at zero

  3. Python Documentations

  4. 廖雪峰的网站