Python 之旅 ()

/马儿 (marr@linux.org.tw)

上期内容当中,读者已经浏览过 Python 语言里的一些基本资料型别元件,我们将接续地看到更多实用而有趣的范例。本期的重点摆在字串、函式、模组、系统资讯的处理上,这些学习经验仍然是 Python 程式设计的重要基础。

连续数值的指定: 内建函式 range()

range 是一个便利的内建函式,可以传回一组整数值的串列。

>>> range(7) Œ

[0, 1, 2, 3, 4, 5, 6]

>>> (mon, tue, wed, thu, fri, sat, sun) = range(7) 

>>> mon 

0

>>> sun 

6

>>> range(-5, 5) Ž

[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]

>>> range(0, 10, 3) 

[0, 3, 6, 9]

Œ range 的预设值由 0 为起始。

 将一个值组与 range 函式所产生的串列进行对映,所以指定结果是 mon 0,而 sun 6

Ž 可以为 range 函式指定起始值与终止值,如 (-5, 5) 是指 -5 4 所形成的整数值串列。

 先前的例子都省略了间隔值的设定,而使用其预设间隔值 1,在此我们可以加入间隔值的设定,如例中的最后一个参数 3

下列的简单范例,可以将一组资料进行编号。

>>> weekday = ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')

>>> for day in range(len(weekday)):

... print day, weekday[day]

...

0 mon

1 tue

2 wed

3 thu

4 fri

5 sat

6 sun

实务程式写作上,range函式 for 回圈经常搭配。还记得上期内容里的 ascii.py 范例吗? 我们可以使用 for 回圈与 range 函式加以改写,来个温故知新。

example$ cat ascii.py

#!/usr/bin/python

for i in range(256) :

print chr(i),

if i != 0 and i % 16 == 0 :

print

相较之下,程式码本身显得简化了,但语法型态与 while 不同,读者应用时大抵挑自己习惯者即可。

资源与效率的抵换

除了 range() 之外,还有个 xrange() 内建函式,不但名称相近,功能可说完全一致,其传回值最后是相同的。差别在于 range() 会以串列资料型别来储存所产生的传回值,实际在记忆体里占有完整空间,而 xrange() 则是传回一个 XRangeType 的物件,并不会在记忆体里占有太多储存空间,而是等到实际存取资料时才继续算出其值。所以在大笔资料的实务应用上,使用 range() 会占用较多记忆体,但效率快些,而 xrange() 则节省记忆体空间,而牺牲了效率。请读者善用 Python 的设计特性,依场合需要自行选用合适的函式。

>>> range(1000000)

>>> xrange(1000000)

如果你的电脑系统资源丰富,上述两个例子‘看不出差别’,可以继续尝试更大的输入值来测试。

函式 (function) 的使用

函式功能在程式语言里是必备的,不管是函式或模组的写作,都要求程式员尽可能做到易读易懂、功能独立、可重复使用的程式片段,‘避免重造轮子’,这样的设计理念,在 Python 语言里便极受重视。相信读者至少已经熟悉一种其他程式语言的函式功能,我们可以直接观察下列的范例:

>>> def fact(n):

... """Return the factorial of the given number."""

... r = 1

... while n > 0:

... r = r * n

... n = n - 1

... return r

...

>>> fact.__doc__

'Return the factorial of the given number.'

>>> fact(5)

120

>>>

函式还是一个物件,其指定方式,是以保留字 def ( define 之意) 为首,余下的函式内容,同样要按照‘Python 的程式码缩排原则’,否则会产生‘IndentationError’的错误讯息。惯例上,函式的第二行会是一段‘三引号注释字串’,即 """ 所括夹的文字,称为‘文件字串’(documentation strings),我们可以透过如 fact.__doc__ 的物件方法,再把文件字串内容显示出来,这样的实务惯例,通常在大型而正式的 Python 程式开发专案里显得有用。

fact() 是一个阶乘函式范例,请注意到最后一行的保留字 return,如果少了 return 叙述 [1],则预设会以 None 值传回。以范例函式 fact() 来看,n 是函式的参数 (argument),其传回值 r n 的阶乘结果。

保留字、识别字、内建函式名称

程式语言里的保留字 (reserved words) 与识别字 (identifiers),是用来识别变数、函式、类别、模组、物件的名称,我们先前见过如 printiswhilefordefreturn 等,都是 Python 保留字的范例。另外,以 ___ 为开头的识别字名称,许多是 Python 的保留识别字,如 __doc____name____builtins__ 等,它们通常有特殊意义,日后我们会看到越来越多这样的例子。至于内建函式名称,读者已经见过的,如 chr()id()len()max()range() 等。值得注意的是,上述识别字相关名称,都是大小写有别的 (case sensitive)。对于打算长期与 Python 厮混的朋友而言,应该有必要手边准备一份保留字相关资讯。

Python 识别字指定规则:

http://www.python.org/doc/2.0/ref/identifiers.html

Python 所有保留字资讯:

http://www.python.org/doc/2.0/ref/keywords.html

>>> def fact(n=10):

... """Return the factorial of the given number, with the defult input value."""

... r = 1

... while n > 0:

... r = r * n

... n = n - 1

... return r

...

>>> fact(5)

120

>>> fact()

3628800

>>>

上述程式片段示范了函式预设输入值的设定方式,试试以 fact(5) 呼叫范例函式,会得到传回值 120,而以 fact() 呼叫,则会传回以 10 为预设输入值的 3628800

>>> def power(x, y=2):

... r = 1

... while y > 0:

... r = r * x

... y = y - 1

... return r

...

>>> power(2, 4)

16

>>> power(3)

9

>>> power()

Traceback (innermost list):

File "<stdin>", line 1, in ?

TypeError: not enough arguments: expected 1, got 0

>>> power(2, 4, 3)

Traceback (innermost list):

File "<stdin>", line 1, in ?

TypeError: too many arguments: expected 2, got 3

>>>

一个函式可以设定多个输入值,上例 power() 函式可以接受两个输入值,但至少需要 (expect) 一个输入值,因为参数 y 有个预设值 2,输入参数时可以省略之。以 power(2, 4) 呼叫时,会成功传回 16,以 power(3) 呼叫时,会自动以 power(3, 2) 为输入值,传回 9。如果以 power() 呼叫,则会产生 TypeError 的错误讯息,它会明确告知‘参数不足’,你必须至少输入几个参数,如果是以 power(2, 4, 3) 来呼叫,则会得到‘参数过多’的 TypeError 讯息。

>>> power(y=4, x=2)

16

>>>

另一个有用的应用技巧,称为‘关键字派定法’(keyword passing),是以类似 power(x=2, y=4) 方式来呼叫,明确地将参数值配合变数名称通知函式,甚至也可以用 power(y=4, x=2) 方式来呼叫,如此一来,两者的呼叫结果是完全相同,意即其参数值顺序可以任意改变。

我们接着来看一些任意个输入值的例子,相信其实用价值极高。

>>> def maximum(*numbers):

... if len(numbers) == 0:

... return(None)

... else:

... max = numbers[0]

... for n in numbers[1:]:

... if n > max:

... max = n

... return max

...

>>> maximum(3, 8, 5)

8

>>> maximum(2, -5, 9, 1, 7)

9

>>>

函式 maximum() 的用意很明显,我们可以输入任意个数的数值,而它最后会传回最大值。例如 maximum(3, 8, 5) 会传回 8,而 maximum(2, -5, 9, 1, 7) 会传回 9。值得注意的地方,就是其处理不定参数个数的技巧,参数指定以 *numbers 方式代表,而 numbers 本身是一个值组 ( tuple,而非 list)。函式的演算规则倒简单,先把第一个数 numbers[0] 设为最大值,再将剩下的数 numbers[1:] 所形成之值组,丢进 for 回圈,以‘暴力法’比大小。

下列的例子,将参数设定的可能状况大抵做了整合介绍,我们可以一窥函式参数派定的相关细节,值得读者反覆测试观察。

>>> def args_func(a, b, c=9, *other1, **other2): Œ

... return [a, b, c, other1, other2.items()] 

...

>>> args_func(1, 2, 3) Ž

[1, 2, 3, (), []]

>>> args_func(b=1, a=2) 

[2, 1, 9, (), []]

>>> args_func(1, 2, 3, 4, 5, 6, 7, 8, 9) 

[1, 2, 3, (4, 5, 6, 7, 8, 9), []]

>>> args_func(1, c=3, b=2, d=4, e=5)

[1, 2, 3, (), [('d', 4), ('e', 5)]]

>>> args_func(d=4, e=5)

Traceback (innermost last):

File "<stdin>", line 1, in ?

TypeError: not enough arguments; expected 2, got 0

Œ 范例函式 args_func() 可以输入三个 (以上的) 参数,参数名称分别是 abc,其中 c 有预设值为 9a b 是必要的输入参数。而 *other1 用以指定 abc 关键字之外的参数值 (不定个数)**other2 则是用以指定 abc 关键字之外的派定值 (同样是不定个数)

 函式 args_func() 非常简洁,直接把所有的输入参数值以串列资料型别传回。其中的 other1 属于值组资料型别,而 other2 则是辞典集 (dictionary) 资料型别。别慌,稍后会为读者解说辞典集的相关细节。

Ž 给定三个参数值,它们会分别成为函式 args_func() abc 的设定值,此时 other1 other2 都是空空如也。

 使用关键字派定法来指定 a b 的参数值,而使用 c 的预设参数值。

 给定了九个参数值,前三个依序成为 abc 的参数值,后六个数值则成为值组 other1 的元素内容。

给定了五个参数值,第一个成为 a 的参数值,b c 以关键字派定法来指定,而最后的 d e 则成为不定个数的关键字派定值,它们被辞典集 other2 所收留了。

内建资料型别 dictionary 的使用

辞典集是 Python 里的内建映射物件 (mapping object),也就是由一个物件集合来作为另一个物件集合的键值索引。映射物件和之前谈过的序数资料相较,在概念上有扩充、补强的涵意,善用映射物件的特性,可以协助我们将看似复杂的问题,以相当直觉的方式解决。两者具有若干不同之处,例如序数以索引运算作为其取值方式,映射物件的元素成份来源很有弹性,映射物件是不做排序的,而且映射物件是可变物件。

TABLE_BEGIN

: 序数型别与映射型别的比较

序数型别

映射型别

以索引运算作为其取值方式。

keys()values()items() 等内建函式来取值。

以整数作为其索引(index)

以不可变物件作为其键(key)

元素具排序效果。

元素储存时不做特定排序。

不一定都属于可变物件。

辞典集是可变物件。

TABLE_END

在功能上,辞典集的实作相当于资料结构里的杂凑表 (hash table) 或是相关阵列 (associative array),所以你会看到‘键-值’(key-value pairs)的表示法。通常我们会以字串来做为辞典集的 key,因为有意义的字串可以带来‘望文生义’的效果,不过,一定要以不可变物件来做辞典集的 key,而 value 的部份就全无限制了。

>>> x = [] Œ

>>> y = {} Œ

>>> x[0] = 'Beatles' Œ

Traceback (innermost last):

File "<stdin>", line 1, in ?

IndexError: list assignment index out of range

>>> y[0] = 'John Lennon' 

>>> y[1] = 'Paul McCartney' 

>>> y[2] = 'George Harrison' 

>>> y[3] = 'Ringo Starr' 

>>> y[0] + " and Yoko Ono" Ž

'John Lennon and Yoko Ono'

>>> y 

{3: 'Ringo Starr', 2: 'George Harrison', 1: 'Paul McCartney', 0: 'John Lennon'}

>>> y.keys() 

[3, 2, 1, 0]

>>> y.values() 

['Ringo Starr', 'George Harrison', 'Paul McCartney', 'John Lennon']

>>> y.items() 

[(3, 'Ringo Starr'), (2, 'George Harrison'), (1, 'Paul McCartney'), (0, 'John Lennon')]

>>>

Œ 分别起始建立一个空串列 x 与一个空辞典集 y。由于无法指定不存在的索引值给串列,所以 x[0] 的指定动作宣告失败,Python 回报 IndexError 错误讯息。

 对于辞典集而言,并无上述的困扰,修改或增添元素资料均无限制。此例中,我们分别指定了 y[0]y[1]y[2]y[3] 四笔元素资料。

Ž 可以针对元素进行各式运算,例如我们拿字串 y[0] 与另一字串合并显示。

 注意到辞典集的储存,并没有特定的顺序方式,如果想要依特定的排序方法处理资料,可以另寻变通方法。

 示范辞典集最常见的内建函式,即 keys()values()items(),它们都是传回串列物件。

>>> Beatles = {'leader':'John','bass':'Paul','guitar':'George','drum':'Pete'} Œ

>>> Hurricanes = {'drum':'Ringo'} 

>>> Beatles.update(Hurricanes) Ž

>>> Beatles

{'drum': 'Ringo', 'leader': 'John', 'bass': 'Paul', 'guitar': 'George'}

>>> Beatles.get('leader', "Not available") 

'John'

>>> Beatles.get('manager', "Not available") 

'Not available'

>>>

Œ 我们为一个叫做 Beatles 的乐团建立辞典集,指定其键值及元素值,共计四项元素。

 另一个叫做 Hurricanes 的乐团,其辞典集元素,只有鼓手的设定资料。

Ž 透过 update() 物件方法,我们更新了 Beatles 鼓手的设定资料。

 get() 物件方法是询问 Beatles 里是否有 leader 此一键值,若存在则传回其对应之元素值,否则会传回后头的字串资料。

相信读者至此已对辞典集有了基础认识,其他辞典集相关的物件方法及运算元,请参考表格内容说明。值得一提的是,Python 里的辞典集实作得相当有效率,就算和串列型别相较,你也应该会满意于它的便利与速度,适当的话,可以考虑多加使用。

TABLE_BEGIN

: 辞典集的方法和操作

项目

说明

len(dict)

传回辞典集 dict 里的元素个数。

dict[k]

传回键值 k 的元素内容。

dict[k]=v

dict[k] 的内容设定为 v

del dict[k]

dict[k] 元素项目移除。

dict.clear()

将辞典集 dict 所有元素项目全部移除。

dict.copy()

将辞典集 dict 整个复制。

dict.has_key[k]

如果辞典集 dict 含有键值 k 的话,则传回 1,否则传回 0

dict.items()

以值组 (key, value) 的串列型别传回辞典集中所有元素。

dict.keys()

传回辞典集 dict 的所有键值。

dict.values()

传回辞典集 dict 的所有元素值。

dict.update(other)

将辞典集 other 所有物件更新至辞典集 dict 当中。

dict.get(k [, other])

如果 dict[k] 存在则传回 dict[k],否则传回 other

TABLE_END

模组 (module) 的使用

简单地说,模组代表着某个档案名称,该档案名称必须以 .py 延伸档名作结,我们可以到 Python 的安装目录一窥究竟,以 Linux Mandrake 7.2 为例,其安装目录为 /usr/lib/python1.5。目录里包含类似 string.pyos.pyfind.py 的档案,我们可以透过 import string, os, find 之类的程式语法呼叫这些模组内容。也可以直接阅读这些 .py 档案的程式码,相信部份档案的内容,对你而言已不再全是天书。

别误会了,模组本身也可以是编译过的档案,如 .pyc .pyo 档案即属此类。在系统目录里存在这类档案,通常可以达到执行加速的效果。

实际动手撰写自己的模组之前,我们得先认识内建函式 dir() 的功能,它可以将许多物件内部的资讯显示出来。

>>> dir() Œ

['__builtins__', '__doc__', '__name__']

>>> dir(__doc__) 

[]

>>> print __doc__ 

None

>>> print __name__ Ž

__main__

>>> type(__builtins__)

<type 'module'>

>>> dir(__builtins__) 

['ArithmeticError', 'AssertionError', 'AttributeError', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'FloatingPointError', 'IOError', 'ImportError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplementedError', 'OSError', 'OverflowError', 'RuntimeError', 'StandardError', 'SyntaxError', 'SystemError', 'SystemExit', 'TypeError', 'ValueError', 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__', '__name__', 'abs', 'apply', 'buffer', 'callable', 'chr', 'cmp', 'coerce', 'compile', 'complex', 'delattr', 'dir', 'divmod', 'eval', 'execfile', 'exit', 'filter', 'float', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'len', 'list', 'locals', 'long', 'map', 'max', 'min', 'oct', 'open', 'ord', 'pow', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'round', 'setattr', 'slice', 'str', 'tuple', 'type', 'vars', 'xrange']

>>> print __builtins__.__doc__ 

Built-in functions, exceptions, and other objects.

Noteworthy: None is the `nil' object; Ellipsis represents `...' in slices.

>>>

Œ 当我们身处新启动之 Python 交谈环境里,输入 dir() 可以显示 local symbol table的名称串列,共计三个。

 进一步提供 __doc__ dir() 做为参数,传回空串列,表示 __doc__ 物件已无相关属性 (attributes)print __doc__ 显示其为 None 物件。

Ž __name__ 是一个字串物件,表示目前执行‘程式’的名称,其值为 __main__

 __builtins__ 则是一个模组物件,持续以 dir(__builtins__) 观察,可以显示模组 __builtins__ 的全部属性。

 显示 __builtins__ __doc__ 属性内容。

注意到,每个正在执行的主要程式,其程式名称 ( __name__ 属性) 会是 __main__,如果是以模组型态被 import 进来,那么该模组程式便会以原本档案名称为 __name__ 的值。请观察下列程式范例的说明,两个极其简化的‘土制模组’。

example$ cat other_mod.py

#!/usr/bin/python

print "this is from other_mod."

print __name__

example$ chmod 755 other_mod.py; ./other_mod.py

this is from other_mod.

__main__

example$ cat my_mod.py

#!/usr/bin/python

"""Simple module example for illustrating how __*__ works."""

import other_mod

print "this is from my_mod."

print __name__

example$ chmod 755 my_mod.py; ./my_mod.py

this is from other_mod.

other_mod

this is from my_mod.

__main__

 

import 的模组档案,其内容会被执行,所以范例 my_mod.py 在执行之后,会先读进 other_mod.py 的程式片段,接着才是 my_mod.py 的程式片段。请特别留意 __name__ 属性值的变化,这项控制技巧经常被使用。

模组的搜寻路径

Python 实际搜寻模组路径的设定值,可以由 sys 模组里的 path 变数值取得。

>>> import sys

>>> sys.path

['', '/usr/lib/python1.5/', '/usr/lib/python1.5/plat-linux-i386', '/usr/lib/python1.5/lib-tk', '/usr/lib/python1.5/lib-dynload', '/usr/lib/python1.5/site-packages', '/usr/lib/python1.5/site-packages/PIL']

>>> import marr

Traceback (innermost last):

File "<stdin>", line 1, in ?

ImportError: No module named marr

>>>

上述的搜寻路径是有顺序性的,也就是说,import 所呼叫的模组名称,依序第一个找到的路径便会马上回传使用。如果找不到 import 的模组名称,则会传回 ImportError 的错误讯息。以载入 foo 模组为例,其实际寻找模组的过程顺序如下:

1. 是否存在名为 foo 的目录,并且里头含有该模组的档案。

2. 是否存在 foo.sofoomodule.sofoomodule.sl 或是 foomodule.dll

3. 是否存在 foo.pyo

4. 是否存在 foo.pyc

5. 是否存在 foo.py

以一个 .py Python 原始码档案而言,经过编译后,会产生一个名为 .pyc bytecode执行档,当寻找某个模组名称时,要是 .py 档案的日期不比 .pyc 档案来得新,Python 直译器会直接将编译好的 .pyc 档案载入,若是 .py 档案的日期比 .pyc 档案来得新,通常就表示 .py 档案内容已更新,Python 直译器会重新编译之,以产生新的 .pyc 档案,然后才进入载入动作。而 .pyo 档案只有在直译器以 -O 选项启动之后才会产生,这类档案里的资讯通常比 .pyc 档案来得多,包含有原始程式的行号以及除错资讯,因此 .pyo 档案的载入速度会较慢,但程式的执行速度会较快。

.pyc 或是 .pyo 档案的编译动作,是在程式里头呼叫 import 后才会发生,对 Python语言来说,模组档案不止是设计概念的切割,它更从强化模组执行效率的角度,鼓励程式员善用模组档案功能。如果自制的模组档案越来越多,其应用自然越显重要,此时便要认真为自制模组找个适当的存放路径,比较常见的方式之一,是设定相关的环境变数值,例如变数 PYTHONPATH

TABLE_BEGIN

: Python 相关环境变数设定

变数名称

说明

PYTHONDEBUG

python -d 启动模式相同。可产生 Python 的语法解析除错资讯。

PYTHONHOME

与模组搜寻路径设定相关的变数。

PYTHONINSPECT

python -i 启动模式相同。以交谈模式来执行 Python 程式。

PYTHONOPTIMIZE

python -O 启动模式相同。以最佳化模执行 Python 程式。

PYTHONPATH

增加模组搜寻路径。

PYTHONSTARTUP

交谈模式就绪前所执行的程式路径。

PYTHONUNBUFFERED

python -u 启动模式相同。记录未做缓冲的二元标准输出输入。

PYTHONVERBOSE

python -v 启动模式相同。执行过程详列相关处理资讯。

TABLE_END

名称空间 (namespace) 及其有效领域 (scope)

回过头来,我们再次透过 dir() 来观察模组档案载入后的物件变化情况。请将现行目录设定在 my_mod.py 档案存放的目录,然后进入 Python 的交谈环境。

●图●

: 名称空间阶层示意

●图●

example$ python

Python 1.5.2 (#1, Sep 30 2000, 18:08:36) [GCC 2.95.3 19991030 (prerelease)] on linux-i386

Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam

>>> dir()

['__builtins__', '__doc__', '__name__']

>>> import my_mod

this is from other_mod.

other_mod

this is from my_mod file.

my_mod

>>> dir()

['__builtins__', '__doc__', '__name__', 'my_mod']

>>> dir(my_mod)

['__builtins__', '__doc__', '__file__', '__name__']

>>> print my_mod.__doc__

Simple module example for illustrating how __*__ works.

>>> print my_mod.__file__

my_mod.py

>>>

整个运作过程,依序可以分解如下:

1. 每次 Python 启动时,它会产生一个名为 __main__ 的模组物件,此物件本身当然有其相关资讯,所以我们可以透过 dir() 来取得。

2. 每次呼叫 import 内建函式之后,Python 会载入另一个模组物件,并将其物件相关资讯并进 __main__

3. 由于两个模组物件的变数名称,有若干重复之处,如上述例子里的 __builtins____doc____name__,在取用变数资讯时,必须有办法加以区别才行。

4. Python 区别变数资讯的方法,如上例所示,是类似 __doc__ my_mod.__doc__ 这样的不同表示法。

5. 每个模组物件,都会伴随一份物件资讯,以辞典集型别储存在系统当中,而这样的物件资讯内容,包括其变数、函式、物件等,都成为辞典集里所记录的键值或属性值。

6. 上述的辞典集物件资讯,我们把它简称为‘名称空间’(namespace)

7. 内建函式 dir() 的功能,可以把名称空间的名称资讯全部列出。

除了基本的资料型别之外,Python 里的每一个模组物件都拥有自己的名称空间。下列是一个范例程式,可以协助列出模组物件的名称资讯。

example$ cat namespace.py

#!/usr/bin/python

import sys

k = sys.modules.keys()

print "Keys:", k

print "-" * 30

for i in k:

if i == "__main__":

print ">>>", i, "__dict__", sys.modules[i].__dict__

print "-" * 30

print dir()

在实务设计上,Python 程式员会运用模组的方式,将物件做适当的切割,切割好的程式可以被包在一个 Python 类别库 (package) 里,以便进一步有效管理大型的软体专案。有了函式、模组、类别库等一系列的架构,我们便可更直觉地管理软体专案的物件体系。着名的 Zope 系统便是这样的专案管理实例 [2],其整套系统广泛应用类别库的技巧,至今仍在积极发展中。

其他有用的函式

为了方便接续的内容,我们先来认识一个实用的模组,称为 string,顾名思义,可用于协助处理字串物件。

>>> import string

>>> date = "Fri May 18 CST 2001"

>>> piece1 = string.split(date)

>>> piece1

['Fri', 'May', '18', 'CST', '2001']

>>> time = "12:03:27"

>>> piece2 = string.split(time, ':')

>>> piece2

['12', '03', '27']

>>> string.digits

'0123456789'

上述范例,让我们见识到模组 string 里有个 split() 的物件方法,可以将一个字串变数值,依空白字元 (预设状况) 为切割点,分解成数个小字串,形成一个字串串列传回。如果切割条件不是空白字元时,在 split() 所接参数中予以指定,如 split(time, ':') 就是指定要以 ':' 字元为切割点。最后则是显示模组 string 有个字串变数 digits,内容设定为 '0123456789'

如果我们想把上述字串串列里的‘数字’,如 '18' '2001',由字串型别转换成数值型别,可以怎么做呢? 下列是个方法。

>>> def try_ai(s):

... if s[0] in string.digits:

... return string.atoi(s)

... else:

... return s

...

>>> import string

>>> date = "Fri May 18 CST 2001"

>>> piece = string.split(date)

>>> finish_ai = map(try_ai, piece)

>>> print finish_ai

['Fri', 'May', 18, 'CST', 2001]

>>>

首先,定义一个叫做 try_ai() 的函式,它在读进字串后,会比对字串的第一个字元,如果第一个字元是属于阿拉伯数字,那么就会尝试将字串转换成整数,最后传回其整数型别资料。是的,你会发现它的演算规则有些天真,不过,我们暂时还不需要一个无懈可击的转换函式。

接着,我们载入模组 string 之后,利用内建函式 map() 将自制函式 try_ai 与字串串列 piece 连结起来,如此一来,便能如愿将 piece 里的部份字串,转换成数值型别。显然 map() 函式在此例中帮上大忙,简洁地协助我们将自制函式与序列资料做了巧妙结合。

接下来,我们就可以进一步稍微改良原本天真的 try_ai() 函式。

>>> def try_ai(s):

... if ':' in s:

... ts = string.split(s, ':')

... return map(string.atoi,ts)

... if s[0] in string.digits:

... return string.atoi(s)

... else:

... return s

...

>>> import string

>>> date = "Fri May 18 12:03:27 CST 2001"

>>> piece = string.split(date)

>>> finish_ai = map(try_ai, piece)

>>> print finish_ai

['Fri', 'May', 18, [12, 3, 27], 'CST', 2001]

>>>

这个改良过的版本,可以进一步处理像 '12:03:27' 这样的‘数字’,否则原本更天真的版本会传回 ValueError 的错误讯息。

>>> piece = ['Fri', 'May', '18', '12:03:24', 'CST', '2001']

>>> def strp(x, y):

... return x + ' ' + y

...

>>> r = reduce(strp, piece)

>>> r

'Fri May 18 12:03:24 CST 2001'

>>>

上述程式片段,处理效果刚好与之前的程式相反,它会把字串串列重组成一个长字串。重点就是利用了内建函式 reduce(),其运作方式同样要输入一个函式名称及一个序数资料,不过,目的是要把序数资料的元素‘合并减少’成一个。reduce() 也可以应用在数值串列上,以下便是这样的范例。

>>> n = range(1, 11)

>>> def mult(x, y):

... return x * y

...

>>> f = reduce(mult, n)

>>> f

3628800

>>>

说穿了,它还是一个阶乘的范例,每呼叫一次 mult() 函式,数值串列的个数会越来越少,最后传回一个阶乘结果,在此例中,即 10! 的答案。

下列是一个简化版本的闰年判断程式,我们将介绍另一个函式 filter()

>>> def leap(y):

... if (y%400) == 0:

... return 1

... elif (y%100) == 0:

... return 0

... elif (y%4) == 0:

... return 1

... return 0

...

>>> n = range(1900, 2001)

>>> leap_year = filter(leap, n)

>>> leap_year

[1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936, 1940, 1944, 1948, 1952, 1956, 1960, 1964, 1968, 1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000]

>>>

函式 filter() 同样是接一个函式名称与一个序数资料为参数,重点在于,在自制函式中,你必须把‘想留下来的资料’,其函式传回值设为 1 (代表 true),而把‘不想留下来的资料’,其函式传回值设为 0 (代表 false)。如此一来,filter() 函式在依序处理完序数资料后,还是会传回一个序数资料,而且应该只留下你想要的资料。

lambda 表示式

lambda 表示式是个实用而重要的工具,其功能及本质还是函式,差别在于 lambda 没有‘明确的函式名称’,而且通常只用于简洁的函式叙述。

>>> n = range(1, 11)

>>> f = reduce(lambda x, y: x * y, n)

>>> f

3628800

>>>

又是熟悉的阶乘函式,有趣的是,它的表示方式非常简洁,乍看之下,初学者可能以为是天书了。lambda 表示式的语法如下:

lambda 参数串列: 表示式

例如 x + yx * ys[9] 都是表示式的例子。早期,lambda 表示式的概念是取自 Lisp 语言,使用上有其便利及优势,不过,对初学者而言,使用 lambda 表示式通常还是得花时间熟悉,若是‘画虎不成反类犬’,搞到程式大乱就得不偿失了。

相关说明

[1] 有人把缺少 return 叙述的‘函式’称为 procedure。本质上,由于函式不明确描述 return 的话,会自动以 None (空型别) 传回,所以视之为函式并无不妥。

[2] Zope 是一套网站 Application Server 系统,完全由 Python 语言所开发出来,程式本身就是一个庞大的物件系统,有兴趣的朋友,可到网站 http://www.zope.org/ 观摩。

[3] 来点不一样的吧,欢迎访阅 Instant Python 网页,网址 http://www.hetland.org/python/instant-python.php

[4] 想让 Python 执行更有效率吗? 试试Python Performance Tips 网页,网址http://musi-cal.mojam.com/~skip/python/fastpython.html

[5] Python for Lisp Programmers 网页,网址 http://www.norvig.com/python-lisp.html

[6] A Comparison of Python and LISP 网页,网址http://www.strout.net/python/pythonvslisp.html

[7] A Python/Perl phrasebook 网页,网址http://starship.python.net/~da/jak/cookbook.html

[8] Major Python changes 网页,提供Python 改版增修重点说明,网址http://shell.rmi.net/~lutz/errata-python-changes.html

[9] Python Language Mapping 网页,网址http://www.informatik.hu-berlin.de/~loewis/python/pymap.htm