python书籍推荐1001python书籍推荐_利用Python进行数据分析 (O'Reilly精品图书系列) 附录A Python语言精要...

附录A Python语言精要

知识是一座宝库,而实践就是开启这座宝库的钥匙。

——Thomas Fuller

人们常常问我要有关学习Python数据处理方面的优质资源。虽然市面上有许多非常不错的讲解Python语言的图书,但我在推荐的时候经常还是会犹豫不决,因为它们都是针对普通读者的,没有为那些只想“加载点儿数据,做点计算,再画点儿图”的读者做专门的裁剪。其实有几本书确实是关于Python科学计算编程的,但它们是专为数值计算和工程应用而设计的:解微分方程、计算积分、做蒙特卡罗模拟,以及其他各种数学方面的主题,但就是没有数据分析和统计方面的。由于本书的目的是让大家成为Python数据处理方面的熟手,所以我认为有必要花点时间从结构化和非结构化数据处理的角度重点介绍一些有关Python内置数据结构和库的最重要的功能。我将只介绍一些大致的信息,只要对本书的学习够用就行。

本附录并没有打算成为Python语言的详尽指南,只会对书中反复用到的那些功能做一个基本的概述。对于Python新手而言,我建议在读完本附录后再看看Python的官方教程(http://docs.python.org),最好能再读一两本有关Python通用编程方面的优质图书。以我的观点来看,如果只需要用Python进行高效的数据分析工作,根本就没必要非得成为通用软件编程方面的专家不可。我强烈建议你用IPython实验所有的代码示例,并查看各种类型、函数以及方法的文档。注意,这些例子中所用到的一些代码暂时还没必要解释得那么详细。

本书主要关注的是能够处理大数据集的高性能数组计算工具。为了使用这些工具,你常常得先把那些乱七八糟的数据处理成漂亮点的结构化形式。好在Python是一种最易上手的数据整形语言。你的Python语言能力越强,数据分析的准备工作就越简单。

Python解释器

Python是一种解释型语言。Python解释器是通过“一次执行一条语句”的方式运行程序的。标准的交互式Python解释器可以在命令行上通过python命令启动:

$ python

Python 2.7.2 (default, Oct 4 2011, 20:06:09)

[GCC 4.6.1] on linux2

Type "help", "copyright", "credits" or "license" for more information.

>>> a = 5

>>> print a

5

上面的">>>"是提示符,你可以在那里输入表达式。要退出Python解释器并返回命令提示符,输入exit()或按下Ctrl-D即可。

运行Python程序的方式很简单,只需调用python并将一个.py文件作为其第一个参数即可。假设我们已经创建了一个hello_world.py,其内容如下:

print 'Hello world'

只需在终端上输入如下命令即可运行:

$ python hello_world.py

Hello world

虽然许多Python程序员用这种方式执行他们的所有Python代码,但Python科学计算程序员则更趋向于使用IPython(一种加强的交互式Python解释器)。第3章专门介绍了IPython系统。通过使用%run命令,IPython会在同一个进程中执行指定文件中的代码。因此,在这些代码执行完毕之后,你就可以通过交互的方式研究其结果了。

$ ipython

Python 2.7.2 |EPD 7.1-2 (64-bit)| (default, Jul 3 2011, 15:17:51)

Type "copyright", "credits" or "license" for more information.

IPython 0.12 -- An enhanced Interactive Python.

? -> Introduction and overview of IPython's features.

%quickref -> Quick reference.

help -> Python's own help system.

object? -> Details about 'object', use 'object??' for extra details.

In [1]: %run hello_world.py

Hello world

In [2]:

默认的IPython提示符采用的是一种编号的风格(如In [2]:),而不是标准的">>>"提示符。

基础知识

语言语义

Python语言的设计特点是重视可读性、简洁性以及明确性。有些人甚至将它看做“可执行的伪码”。

缩进,而不是大括号

Python是通过空白符(制表符或空格)来组织代码的,不像其他语言(如R、C++、Java、Perl等)用的是大括号。以for循环为例,要实现前面说的那个快速排序算法:

for x in array:

if x < pivot:

less.append(x)

else:

greater.append(x)

冒号表示一段缩进代码块的开始,其后的所有代码都必须缩进相同的量,直到代码块结束为止。在别的语言中,你可能会看到下面这样的东西:

for x in array {

if x < pivot {

less.append(x)

} else {

greater.append(x)

}

}

使用空白符的主要好处是,它能使大部分Python代码在外观上看起来差不多。也就是说,当你阅读某段不是自己编写的(或一年前匆忙编写的)代码时不怎么容易出现“认知失调”。在那些空白符无实际意义的语言中,你可能会发现一些格式不统一的代码,比如:

for x in array

{

if x < pivot

{

less.append(x)

}

else

{

greater.append(x)

}

}

无论对它是爱是恨,反正有意义的空白符就是Python程序员的生活现实。再说了,以我的经验来看,它能使Python代码具有更高的可读性(至少比我用过其他语言要高)。虽然第一眼看上去会觉得比较火星,但我相信不用多久你就会喜欢上它的。

注意:

我强烈建议用4个空格作为默认缩进量,这样,你的编辑器就会将制表符替换为4个空格。许多文本编辑器都有一个这样的设置项。有些人喜欢用制表符或其他数量的空格,但用2个空格的情况非常少见。4个空格其实就是一种标准,绝大部分Python程序员都这么用。所以我建议:除非有特殊的原因,否则就用4个空格吧。

到目前为止,你可以看到,Python语句还能不以分号结束。不过分号还是可以用的,比如在一行上分隔多条语句:

a = 5; b = 6; c = 7

在一行上放置多条语句的做法在Python中一般是不推荐的,因为这往往会使代码的可读性变差。

万物皆对象

Python语言的一个重要特点就是其对象模型的一致性。Python解释器中的任何数值、字符串、数据结构、函数、类、模块等都待在它们自己的“盒子”里,而这个“盒子”也就是Python对象。每个对象都有一个与之相关的类型(比如字符串或函数)以及内部数据。在实际工作当中,这使得Python语言变得非常灵活,因为即使是函数也能被当做其他对象那样处理。

注释

任何前缀为井号(#)的文本都会被Python解释器忽略掉。这通常用于在代码中添加注释。有时你可能只是想排除不运行某些代码块而不想删除它们。最简单的办法就是注释掉那些代码:

results = []

for line in file_handle:

# 暂时保留空行

# if len(line) == 0:

# continue

results.append(line.replace('foo', 'bar'))

函数调用和对象方法调用

函数的调用需要用到圆括号以及0个或多个参数,此外还可以将返回值赋值给一个变量:

result = f(x, y, z)

g()

几乎所有的Python对象都有一些附属函数(也就是方法),它们可以访问该对象的内部数据。方法的调用是这样写的:

obj.some_method(x, y, z)

函数既可以接受位置参数,也可以接受关键字参数:

result = f(a, b, c, d=5, e='foo')

稍后将详细介绍这个内容。

变量和按引用传递

在Python中对变量赋值时,你其实是在创建等号右侧对象的一个引用。用实际的例子来说吧,看看下面这个整数列表:

In [241]: a = [1, 2, 3]

假如我们将a赋值给一个新变量b:

In [242]: b = a

在某些语言中,该赋值过程将会导致数据[1,2,3]被复制。而在Python中,a和b现在都指向同一个对象,即原始列表[1,2,3](如图A-1所示)。你可以自己验证一下:对a添加一个元素,然后看看b的情况。

In [243]: a.append(4)

In [244]: b

Out[244]: [1, 2, 3, 4]

Image00148.jpg

图A-1:指向同一个对象的两个引用

理解Python引用的语义以及数据复制的条件、方式、原因等知识对于在Python中处理大数据集非常重要。

注意:

赋值(assignment)操作也叫做绑定(binding),因为我们其实是将一个名称和一个对象绑定到一起。已经赋过值的变量名有时也被称为已绑定变量(bound variable)。

当你将对象以参数的形式传入函数时,其实只是传入了一个引用而已,不会发生任何复制。因此,Python被称为是按引用传递的,而某些其他的语言则既支持按值传递(创建副本)又支持按引用传递。也就是说,Python函数可以修改其参数的内容。假设我们有下面这样的一个函数:

def append_element(some_list, element):

some_list.append(element)

根据刚才所说的,下面这样的结果应该是在意料之中的:

In [2]: data = [1, 2, 3]

In [3]: append_element(data, 4)

In [4]: data

Out[4]: [1, 2, 3, 4]

动态引用,强类型

跟许多编译型语言(如Java和C++)相反,Python中的对象引用没有与之关联的类型信息。下面这些代码不会有什么问题:

In [245]: a = 5

In [246]: type(a)

Out[246]: int

In [247]: a = 'foo'

In [248]: type(a)

Out[248]: str

变量其实就是对象在特定命名空间中的名称而已。对象的类型信息是保存在它自己内部的。有些人可能会轻率地认为Python不是一种“类型化语言”。其实不是这样的。看看下面这个例子:

In [249]: '5' + 5

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 '5' + 5

TypeError: cannot concatenate 'str' and 'int' objects

在有些语言中(比如Visual Basic),字符串'5'可能会被隐式地转换为整数,于是就会得到10。而在另一些语言中(比如JavaScript),整数5可能会被转换为字符串,于是就会得到'55'。而在这一点上,Python可以被认为是一种强类型语言,也就是说,所有对象都有一个特定的类型(或类),隐式转换只在很明显的情况下才会发生,比如下面这样:

In [250]: a = 4.5

In [251]: b = 2

# 这个操作是字符串格式化,稍后介绍

In [252]: print 'a is %s, b is %s' % (type(a), type(b))

a is , b is

In [253]: a / b

Out[253]: 2.25

了解对象的类型是很重要的。要想编写能够处理多个不同类型输入的函数就必须了解有关类型的知识。通过isinstance函数,你可以检查一个对象是否是某个特定类型的实例:

In [254]: a = 5

In [255]: isinstance(a, int)

Out[255]: True

isinstance可以接受由类型组成的元组。如果想检查某个对象的类型是否属于元组中所指定的那些:

In [256]: a = 5; b = 4.5

In [257]: isinstance(a, (int, float))

Out[257]: True

In [258]: isinstance(b, (int, float))

Out[258]: True

属性和方法

Python中的对象通常都既有属性(attribute,即存储在该对象“内部”的其他Python对象)又有方法(method,与该对象有关的能够访问其内部数据的函数)。它们都能通过obj.attribute_name这样的语法进行访问:

In [1]: a = 'foo'

In [2]: a.

a.capitalize a.format a.isupper a.rindex a.strip

a.center a.index a.join a.rjust a.swapcase

a.count a.isalnum a.ljust a.rpartition a.title

a.decode a.isalpha a.lower a.rsplit a.translate

a.encode a.isdigit a.lstrip a.rstrip a.upper

a.endswith a.islower a.partiti a.split a.zfill

a.expandtabs a.isspace a.replace a.splitlines

a.find a.istitle a.rfind a.startswith

属性和方法还可以利用getattr函数通过名称进行访问:

>>> getattr(a, 'split')

虽然本书没怎么用到getattr函数以及与之相关的hasattr和setattr函数,但是它们还是很实用的,尤其是在编写通用的、可复用的代码时。

“鸭子”类型译注1

一般来说,你可能不会关心对象的类型,而只是想知道它到底有没有某些方法或行为。比如说,只要一个对象实现了迭代器协议(iterator protocol),你就可以确认它是可迭代的。对于大部分对象而言,这就意味着它拥有一个__iter__魔术方法。当然,还有一个更好一些的验证办法,即尝试使用iter函数:

def isiterable(obj):

try:

iter(obj)

return True

except TypeError: # 不可迭代

return False

对于字符串以及大部分Python集合类型,该函数会返回True:

In [260]: isiterable('a string') In [261]: isiterable([1, 2, 3])

Out[260]: True Out[261]: True

In [262]: isiterable(5)

Out[262]: False

我常常在编写需要处理多类型输入的函数时用到这个功能。还有一种常见的应用场景:编写可以接受任何序列(列表、元组、ndarray)或迭代器的函数。你可以先检查对象是不是列表(或NumPy数组),如果不是,就将其转换成是:

if not isinstance(x, list) and isiterable(x):

x = list(x)

引入(import)

在Python中,模块(module)就是一个含有函数和变量定义以及从其他.py文件引入的此类东西的.py文件。假设我们有下面这样一个模块:

# some_module.py

PI = 3.14159

def f(x):

return x + 2

def g(a, b):

return a + b

如果想要引入some_module.py中定义的变量和函数,我们可以在同一个目录下创建另一个文件:

import some_module

result = some_module.f(5)

pi = some_module.PI

还可以写成这样:

from some_module import f, g, PI

result = g(5, PI)

通过as关键字,你可以引入不同的变量名译注2

import some_module as sm

from some_module import PI as pi, g as gf

r1 = sm.f(pi)

r2 = gf(6, pi)

二元运算符和比较运算符

大部分二元数学运算和比较运算都跟我们想象中的一样:

In [263]: 5 - 7 In [264]: 12 + 21.5

Out[263]: -2 Out[264]: 33.5

In [265]: 5 <= 2

Out[265]: False

表A-1中列出了所有可用的二元运算符。

要判断两个引用是否指向同一个对象,可以使用is关键字。如果想判断两个引用是否不是指向同一个对象,则可以使用is not:

In [266]: a = [1, 2, 3]

In [267]: b = a

# 注意,list函数始终会创建新列表

In [268]: c = list(a)

In [269]: a is b

Out[269]: True

In [270]: a is not c

Out[270]: True

注意,这跟比较运算"=="不是一回事,因为对于上面这个情况,我们将会得到:

In [271]: a == c

Out[271]: True

is和is not常常用于判断变量是否为None,因为None的实例只有一个:

In [272]: a = None

In [273]: a is None

Out[273]: True

Image00149.jpg

Image00150.jpg

严格与懒惰

无论使用什么编程语言,都必须了解表达式是何时被求值的。看看下面这两个简单的表达式:

a = b = c = 5

d = a + b * c

在Python中,只要这些语句被求值,相关计算就会立即(也就是严格)发生,d的值会被设置为30。而在另一种编程范式中(比如Haskell这样的纯函数编程语言),d的值在被使用之前是不会被计算出来的。这种将计算推迟的思想通常称为延迟计算(lazy evaluation译注3

)。而Python是一种非常严格的(急性子)语言。几乎在任何时候,计算过程和表达式都是立即求值的。即使是在上面那个简单的例子中,也是先计算b *c的结果然后再将其与a加起来的。

有一些Python技术(尤其是用到迭代器和生成器的那些)可以用于实现延迟计算。在数据密集型应用中,当执行一些负荷非常高的计算时(这种情况不太多),这些技术就能派上用场了。

可变和不可变的对象

大部分Python对象是可变的(mutable),比如列表、字典、NumPy数组以及大部分用户自定义类型(类)。也就是说,它们所包含的对象或值是可以被修改的。

In [274]: a_list = ['foo', 2, [4, 5]]

In [275]: a_list[2] = (3, 4)

In [276]: a_list

Out[276]: ['foo', 2, (3, 4)]

而其他的(如字符串和元组等)则是不可变的(immutable)译注4

In [277]: a_tuple = (3, 5, (4, 5))

In [278]: a_tuple[1] = 'four'

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 a_tuple[1] = 'four'

TypeError: 'tuple' object does not support item assignment

注意,仅仅因为“可以修改某个对象”并不代表“就该那么做”。这种行为在编程中也叫做副作用(side effect)。例如,在编写一个函数时,任何副作用都应该通过该函数的文档或注释明确地告知用户。即使可以使用可变对象,我也建议尽量避免副作用且注重不变性(immutability)。

标量类型

Python有一些用于处理数值数据、字符串、布尔值(True或False)以及日期/时间的内置类型。表A-2列出了主要的标量类型。后面我们将单独讨论日期/时间的处理,因为它们是由标准库中的datetime模块提供的。

Image00151.jpg

数值类型

用于表示数字的主要Python类型是int和float。能被保存为int的整数的大小由平台决定(是32位还是64位),但是Python会自动将非常大的整数转换为long,它可以存储任意大小的整数。

In [279]: ival = 17239871

In [280]: ival ** 6

Out[280]: 26254519291092456596965462913230729701102721L

浮点数会被表示为Python的float类型。浮点数会被保存为一个双精度(64位)值。它们也可以用科学计数法表示:

In [281]: fval = 7.243

In [282]: fval2 = 6.78e-5

在Python 3中,整数除法除不尽时就会产生一个浮点数:

In [284]: 3 / 2

Out[284]: 1.5

在Python 2.7及以下版本中(某些读者现在用的可能就是译注5

),只要将下面这条怪模怪样的语句添加到自定义模块的顶部即可修改这个默认行为:

from __future__ import division

如果没加这句的话,你也可以显式地将分母转换成浮点数译注6

In [285]: 3 / float(2)

Out[285]: 1.5

要得到C风格的整数除法(如果除不尽,就丢弃小数部分),使用除后圆整运算符(//)即可:

In [286]: 3 // 2

Out[286]: 1

复数的虚部是用j表示的:

In [287]: cval = 1 + 2j

In [288]: cval * (1 - 2j)

Out[288]: (5+0j)

字符串

很多人都是因为Python强大而灵活的字符串处理能力才使用它的。编写字符串字面量时,既可以用单引号(')也可以用双引号("):

a = 'one way of writing a string'

b = "another way"

对于带有换行符的多行字符串,可以使用三重引号(即'''或"""):

c = """

This is a longer string that

spans multiple lines

"""

Python字符串是不可变的。要修改字符串就只能创建一个新的:

In [289]: a = 'this is a string'

In [290]: a[10] = 'f'

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 a[10] = 'f'

TypeError: 'str' object does not support item assignment

In [291]: b = a.replace('string', 'longer string')

In [292]: b

Out[292]: 'this is a longer string'

许多Python对象都可以用str函数转换为字符串:

In [293]: a = 5.6 In [294]: s = str(a)

In [295]: s

Out[295]: '5.6'

由于字符串其实是一串字符序列,因此可以被当做某种序列类型(如列表、元组等)进行处理:

In [296]: s = 'python' In [297]: list(s)

Out[297]: ['p', 'y', 't', 'h', 'o', 'n']

In [298]: s[:3]

Out[298]: 'pyt'

反斜杠(\)是转义符(escape character),也就是说,它可用于指定特殊字符(比如新行\n或unicode字符)。要编写带有反斜杠的字符串字面量,也需要对其进行转义:

In [299]: s = '12\\34'

In [300]: print s

12\34

如果字符串带有很多反斜杠且没有特殊字符,你就会发现这个办法很容易让人抓狂。幸运的是,你可以在字符串最左边引号的前面加上r,它表示所有字符应该按照原本的样子进行解释:

In [301]: s = r'this\has\no\special\characters'

In [302]: s

Out[302]: 'this\\has\\no\\special\\characters'

将两个字符串加起来会产生一个新字符串:

In [303]: a = 'this is the first half '

In [304]: b = 'and this is the second half'

In [305]: a + b

Out[305]: 'this is the first half and this is the second half'

字符串格式化是另一个重要的主题。Python 3带来了一些新的字符串格式化手段,这里我简要说明一下其主要机制。以一个%开头且后面跟着一个或多个格式字符的字符串是需要插入值的目标(这非常类似于C语言中的printf函数)。看看下面这个字符串:

In [306]: template = '%.2f %s are worth $%d'

在这个字符串中,%s表示将参数格式化为字符串,%.2f表示一个带有2位小数的数字,%d表示一个整数。要用实参替换这些格式化形参,需要用到二元运算符%以及由值组成的元组:

In [307]: template % (4.5560, 'Argentine Pesos', 1)

Out[307]: '4.56 Argentine Pesos are worth $1'

字符串格式化是一个很大的主题,控制值在结果字符串中的格式化效果的方式非常多。我建议你在网上多找一些有关于此的资料来看看。

这里之所以要专门讨论通用字符串处理,是因为它有关于数据分析,更多细节请参阅第7章。

布尔值

Python中的两个布尔值分别写作True和False。比较运算和条件表达式都会产生True或False。布尔值可以用and和or关键字进行连接:

In [308]: True and True

Out[308]: True

In [309]: False or True

Out[309]: True

几乎所有内置的Python类型以及任何定义了__nonzero__魔术方法的类都能在if语句中被解释为True或False:

In [310]: a = [1, 2, 3]

...: if a:

...: print 'I found something!'

...:

I found something!

In [311]: b = []

...: if not b:

...: print 'Empty!'

...:

Empty!

Python中大部分对象都有真假的概念。比如说,如果空序列(列表、字典、元组等)用于控制流(就像上面的空列表b)就会被当做False处理。要想知道某个对象究竟会被强制转换成哪个布尔值,使用bool函数即可:

In [312]: bool([]), bool([1, 2, 3])

Out[312]: (False, True)

In [313]: bool('Hello world!'), bool('')

Out[313]: (True, False)

In [314]: bool(0), bool(1)

Out[314]: (False, True)

类型转换

str、bool、int以及float等类型也可用作将值转换成该类型的函数:

In [315]: s = '3.14159'

In [316]: fval = float(s) In [317]: type(fval)

Out[317]: float

In [318]: int(fval) In [319]: bool(fval) In [320]: bool(0)

Out[318]: 3 Out[319]: True Out[320]: False

None

None是Python的空值类型。如果一个函数没有显式地返回值,则隐式返回None。

In [321]: a = None

In [322]: a is None

Out[322]: True

In [323]: b = 5

In [324]: b is not None

Out[324]: True

None还是函数可选参数的一种常见默认值:

def add_and_maybe_multiply(a, b, c=None):

result = a + b

if c is not None:

result = result * c

return result

我们要牢记,None不是一个保留关键字,它只是NoneType的一个实例而已。

日期和时间

Python内置的datetime模块提供了datetime、date以及time等类型。datetime类型是用得最多的,它合并了保存在date和time中的信息:

In [325]: from datetime import datetime, date, time

In [326]: dt = datetime(2011, 10, 29, 20, 30, 21)

In [327]: dt.day In [328]: dt.minute

Out[327]: 29 Out[328]: 30

给定一个datetime实例,你可以通过调用其date和time方法提取相应的date和time对象:

In [329]: dt.date() In [330]: dt.time()

Out[329]: datetime.date(2011, 10, 29) Out[330]: datetime.time(20, 30, 21)

strftime方法用于将datetime格式化为字符串:

In [331]: dt.strftime('%m/%d/%Y %H:%M')

Out[331]: '10/29/2011 20:30'

字符串可以通过strptime函数转换(解析)为datetime对象:

In [332]: datetime.strptime('20091031', '%Y%m%d')

Out[332]: datetime.datetime(2009, 10, 31, 0, 0)

完整的格式化定义请参见表10-2。

在对时间序列数据进行聚合或分组时,可能需要替换datetime中的一些字段。例如,将分和秒字段替换为0,并产生一个新对象:

In [333]: dt.replace(minute=0, second=0)

Out[333]: datetime.datetime(2011, 10, 29, 20, 0)

两个datetime对象的差会产生一个datetime.timedelta类型:

In [334]: dt2 = datetime(2011, 11, 15, 22, 30)

In [335]: delta = dt2 - dt

In [336]: delta In [337]: type(delta)

Out[336]: datetime.timedelta(17, 7179) Out[337]: datetime.timedelta

将一个timedelta加到一个datetime上会产生一个新的datetime:

In [338]: dt

Out[338]: datetime.datetime(2011, 10, 29, 20, 30, 21)

In [339]: dt + delta

Out[339]: datetime.datetime(2011, 11, 15, 22, 30)

控制流

if、elif和else

if语句是一种最常见的控制流语句类型。它用于判断一个条件,如果为True,则执行紧跟其后的代码块:

if x < 0:

print 'It's negative'

一条if语句可以跟上一个或多个elif块以及一个“滴水不漏”的else块(如果所有条件都为False):

if x < 0:

print 'It's negative'

elif x == 0:

print 'Equal to zero'

elif 0 < x < 5:

print 'Positive but smaller than 5'

else:

print 'Positive and larger than or equal to 5'

如果任何一个条件为True,则其后的elif或else块就不会执行。对于用and或or组成的复合条件,各条件是按从左到右的顺序求值的,而且是短路型的:

In [340]: a = 5; b = 7

In [341]: c = 8; d = 4

In [342]: if a < b or c > d:

...: print 'Made it'

Made it

在本例中,比较运算c>d是不会被计算的,因为第一个比较运算为True。

for循环

for循环用于对集合(比如列表或元组)或迭代器进行迭代。for循环的标准语法是:

for value in collection:

# 对value做一些处理

continue关键字用于使for循环提前进入下一次迭代(即跳过代码块的剩余部分)。看看下面这段代码,其功能是对列表中的整数求和并跳过None值:

sequence = [1, 2, None, 4, None, 5]

total = 0

for value in sequence:

if value is None:

continue

total += value

break关键字用于使for循环完全退出。下面这段代码用于对列表的元素求和,遇到5就退出:

sequence = [1, 2, 0, 4, 6, 5, 2, 1]

total_until_5 = 0

for value in sequence:

if value == 5:

break

total_until_5 += value

后面我们还会看到,如果集合或迭代器的元素是序列类型(比如元组或列表),那么还可以非常方便地将这些元素拆散成for语句中的多个变量:

for a, b, c in iterator:

# 做一些处理

while循环

while循环定义了一个条件和一个代码块,只要条件不为False或循环没有被break显式终止,则代码块将一直不断地执行下去:

x = 256

total = 0

while x > 0:

if total > 500:

break

total += x

x = x // 2

pass

pass是Python中的“空操作”语句。它可以被用在那些没有任何功能的代码块中。由于Python是根据空白符划分代码块的,所以它的存在是很有必要的:

if x < 0:

print 'negative!'

elif x == 0:

# TODO: 在这里放点代码

pass

else:

print 'positive!'

在开发一个新功能时,常常会将pass用作代码中的占位符:

def f(x, y, z):

# TODO: 实现这个函数!

pass

异常处理

优雅地处理Python错误或异常是构建健壮程序的重要环节。在数据分析应用中,许多函数只对特定类型的输入有效。例如,Python的float函数可以将字符串转换为浮点数,但是如果输入值不正确就会产生ValueError:

In [343]: float('1.2345')

Out[343]: 1.2345

In [344]: float('something')

---------------------------------------------------------------------------

ValueError Traceback (most recent call last)

in ()

----> 1 float('something')

ValueError: could not convert string to float: something

假设我们想要编写一个在出错时能优雅地返回输入参数的改进版float函数。我们可以编写一个新函数,并把对float函数的调用放在一个try/except块中:

def attempt_float(x):

try:

return float(x)

except:

return x

只有当float(x)引发异常时,except块中的代码才会被执行:

In [346]: attempt_float('1.2345')

Out[346]: 1.2345

In [347]: attempt_float('something')

Out[347]: 'something'

你可能已经注意到了,float还可以引发ValueError以外的异常:

In [348]: float((1, 2))

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 float((1, 2))

TypeError: float() argument must be a string or a number

你可能只希望处理ValueError,因为TypeError(输入参数不是字符串或数值)可能意味着你的程序中存在合法性bug。要达到这个目的,在except后面加上异常类型即可:

def attempt_float(x):

try:

return float(x)

except ValueError:

return x

于是我们就有了:

In [350]: attempt_float((1, 2))

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 attempt_float((1, 2))

in attempt_float(x)

1 def attempt_float(x):

2 try:

----> 3 return float(x)

4 except ValueError:

5 return x

TypeError: float() argument must be a string or a number

只需编写一个由异常类型组成的元组(圆括号是必需的)即可捕获多个异常:

def attempt_float(x):

try:

return float(x)

except (TypeError, ValueError):

return x

有时你可能不想处理任何异常,而只是希望有一段代码不管try块代码成功与否都能被执行。使用finally即可达到这个目的:

f = open(path, 'w')

try:

write_to_file(f)

finally:

f.close()

这里,文件句柄f始终都会被关闭。同理,你也可以让某些代码只在try块成功时执行,使用else即可:

f = open(path, 'w')

try:

write_to_file(f)

except:

print 'Failed'

else:

print 'Succeeded'

finally:

f.close()

range和xrange

range函数用于产生一组间隔平均的整数:

In [352]: range(10)

Out[352]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

可以指定起始值、结束值以及步长等信息:

In [353]: range(0, 20, 2)

Out[353]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

如你所见,range所产生的整数不包括末端值。range常用于按索引对序列进行迭代:

seq = [1, 2, 3, 4]

for i in range(len(seq)):

val = seq[i]

对于非常长的范围,建议使用xrange,其参数跟range一样,但它不会预先产生所有的值并将它们保存到列表中(可能会非常大),而是返回一个用于逐个产生整数的迭代器。下面这段代码用于对0到9999之间所有3或5的倍数的数字求和:

sum = 0

for i in xrange(10000):

# %是求模运算符

if x % 3 == 0 or x % 5 == 0:

sum += i

注意:

在Python 3中,range始终返回迭代器,因此也就没必要使用xrange函数了。

三元表达式

Python的三元表达式(ternary expression)允许你将产生一个值的if-else块写到一行或一个表达式中。其语法如下所示:

value = true-expr if condition else false-expr

其中的true-expr和false-expr可以是任何Python表达式。它跟下面这种冗长格式的效果一样:

if condition:

value = true-expr

else:

value = false-expr

下面是一个具体点的例子:

In [354]: x = 5

In [355]: 'Non-negative' if x >= 0 else 'Negative'

Out[355]: 'Non-negative'

跟if-else块一样,只有一个表达式会被求值。虽然这可能会引诱你总是使用三元表达式去浓缩你的代码,但要意识到,如果条件以及true和false表达式非常复杂,就可能会牺牲可读性。

数据结构和序列

Python的数据结构简单而强大。精通其用法是成为专家级Python程序员的关键环节。

元组

元组(tuple)是一种一维的、定长的、不可变的Python对象序列。最简单的创建方式是一组以逗号隔开的值:

In [356]: tup = 4, 5, 6

In [357]: tup

Out[357]: (4, 5, 6)

在更复杂的表达式中定义元组时,常常需要用圆括号将值围起来,比如下面这个例子,它创建了一个由元组组成的元组:

In [358]: nested_tup = (4, 5, 6), (7, 8)

In [359]: nested_tup

Out[359]: ((4, 5, 6), (7, 8))

通过调用tuple,任何序列或迭代器都可以被转换为元组:

In [360]: tuple([4, 0, 2])

Out[360]: (4, 0, 2)

In [361]: tup = tuple('string')

In [362]: tup

Out[362]: ('s', 't', 'r', 'i', 'n', 'g')

跟大部分其他序列类型一样,元组的元素也可以通过方括号([])进行访问。跟C、C++、Java之类的语言一样,Python中的序列也是从0开始索引的:

In [363]: tup[0]

Out[363]: 's'

虽然存储在元组中的对象本身可能是可变的,但一旦创建完毕,存放在各个插槽中的对象就不能再被修改了:

In [364]: tup = tuple(['foo', [1, 2], True])

In [365]: tup[2] = False

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 tup[2] = False

TypeError: 'tuple' object does not support item assignment

# 不过

In [366]: tup[1].append(3)

In [367]: tup

Out[367]: ('foo', [1, 2, 3], True)

元组可以通过加号(+)运算符连接起来以产生更长的元组:

In [368]: (4, None, 'foo') + (6, 0) + ('bar',)

Out[368]: (4, None, 'foo', 6, 0, 'bar')

跟列表一样,对一个元组乘以一个整数,相当于是连接该元组的多个副本。

In [369]: ('foo', 'bar') * 4

Out[369]: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')

注意,对象本身是不会被复制的,这里涉及的只是它们的引用而已。

元组拆包

如果对元组型变量表达式进行赋值,Python就会尝试将等号右侧的值进行拆包(unpacking):

In [370]: tup = (4, 5, 6)

In [371]: a, b, c = tup

In [372]: b

Out[372]: 5

即使是嵌套元组也能被拆包:

In [373]: tup = 4, 5, (6, 7)

In [374]: a, b, (c, d) = tup

In [375]: d

Out[375]: 7

利用该功能可以非常轻松地交换变量名。这个任务在其他许多语言中可能是下面这个样子:

tmp = a

a = b

b = tmp

b, a = a, b

变量拆包功能常用于对由元组或列表组成的序列进行迭代:

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

for a, b, c in seq:

pass

另一个常见用法是处理从函数中返回的多个值。稍后将详细介绍。

元组方法

由于元组的大小和内存不能被修改,所以其实例方法很少。最有用的是count(对列表也是如此),它用于计算指定值的出现次数:

In [376]: a = (1, 2, 2, 2, 3, 4, 2)

In [377]: a.count(2)

Out[377]: 4

列表

跟元组相比,列表(list)是变长的,而且其内容也是可以修改的。它可以通过方括号([])或list函数进行定义:

In [378]: a_list = [2, 3, 7, None]

In [379]: tup = ('foo', 'bar', 'baz')

In [380]: b_list = list(tup) In [381]: b_list

Out[381]: ['foo', 'bar', 'baz']

In [382]: b_list[1] = 'peekaboo' In [383]: b_list

Out[383]: ['foo', 'peekaboo', 'baz']

列表和元组在语义上是差不多的,都是一维序列,因此它们在许多函数中是可以互换的。

添加和移除元素

通过append方法,可以将元素添加到列表的末尾:

In [384]: b_list.append('dwarf')

In [385]: b_list

Out[385]: ['foo', 'peekaboo', 'baz', 'dwarf']

利用insert可以将元素插入到列表的指定位置:

In [386]: b_list.insert(1, 'red')

In [387]: b_list

Out[387]: ['foo', 'red', 'peekaboo', 'baz', 'dwarf']

警告:

insert的计算量要比append大,因为后续的引用必须被移动以便为新元素腾地方。

insert的逆运算是pop,它用于移除并返回指定索引处的元素:

In [388]: b_list.pop(2)

Out[388]: 'peekaboo'

In [389]: b_list

Out[389]: ['foo', 'red', 'baz', 'dwarf']

remove用于按值删除元素,它找到第一个符合要求的值然后将其从列表中删除:

In [390]: b_list.append('foo')

In [391]: b_list.remove('foo')

In [392]: b_list

Out[392]: ['red', 'baz', 'dwarf', 'foo']

如果不考虑(使用append和remove时的)性能,Python列表可以是一种非常不错的“多重集合”数据结构。

通过in关键字,你可以判断列表中是否含有某个值:

In [393]: 'dwarf' in b_list

Out[393]: True

注意,判断列表是否含有某个值的操作比字典(dict)和集合(set)慢得多,因为Python会对列表中的值进行线性扫描,而另外两个(基于哈希表)则可以瞬间完成判断。

合并列表

跟元组一样,用加号(+)将两个列表加起来即可实现合并:

In [394]: [4, None, 'foo'] + [7, 8, (2, 3)]

Out[394]: [4, None, 'foo', 7, 8, (2, 3)]

对于一个已定义的列表,可以用extend方法一次性添加多个元素:

In [395]: x = [4, None, 'foo']

In [396]: x.extend([7, 8, (2, 3)])

In [397]: x

Out[397]: [4, None, 'foo', 7, 8, (2, 3)]

注意,列表的合并是一种相当费资源的操作,因为必须创建一个新列表并将所有对象复制过去。而用extend将元素附加到现有列表(尤其是在构建一个大列表时)就会好很多。因此,

everything = []

for chunk in list_of_lists:

everything.extend(chunk)

要比等价的合并操作快得多

everything = []

for chunk in list_of_lists:

everything = everything + chunk

排序

调用列表的sort方法可以实现就地排序(无需创建新对象):

In [398]: a = [7, 2, 5, 1, 3]

In [399]: a.sort()

In [400]: a

Out[400]: [1, 2, 3, 5, 7]

sort有几个很不错的选项。一个是次要排序键,即一个能够产生可用于排序的值的函数。例如,我们可以通过长度对一组字符串进行排序:

In [401]: b = ['saw', 'small', 'He', 'foxes', 'six']

In [402]: b.sort(key=len)

In [403]: b

Out[403]: ['He', 'saw', 'six', 'small', 'foxes']

二分搜索及维护有序列表

内置的bisect模块实现了二分查找以及对有序列表的插入操作。bisect.bisect可以找出新元素应该被插入到哪个位置才能保持原列表的有序性,而bisect.insort则确实地将新元素插入到那个位置上去:

In [404]: import bisect

In [405]: c = [1, 2, 2, 2, 3, 4, 7]

In [406]: bisect.bisect(c, 2)

Out[406]: 4

In [407]: bisect.bisect(c, 5)

Out[407]: 6

In [408]: bisect.insort(c, 6)

In [409]: c

Out[409]: [1, 2, 2, 2, 3, 4, 6, 7]

警告:

bisect模块的函数不会判断原列表是否是有序的,因为这样做的开销太大了。因此,将它们用于无序列表虽然不会报错,但可能会导致不正确的结果。

切片

通过切片标记法,你可以选取序列类型(数组、元组、NumPy数组等)的子集,其基本形式由索引运算符([])以及传入其中的start:stop构成:

In [410]: seq = [7, 2, 3, 7, 5, 6, 0, 1]

In [411]: seq[1:5]

Out[411]: [2, 3, 7, 5]

切片还可以被赋值为一段序列:

In [412]: seq[3:4] = [6, 3]

In [413]: seq

Out[413]: [7, 2, 3, 6, 3, 5, 6, 0, 1]

由于start索引处的元素是被包括在内的,而stop索引处的元素是未被包括在内的,所以结果中的元素数量是stop start。

start或stop都是可以省略的,此时它们分别默认为序列的起始处和结尾处:

In [414]: seq[:5] In [415]: seq[3:]

Out[414]: [7, 2, 3, 6, 3] Out[415]: [6, 3, 5, 6, 0, 1]

负数索引从序列的末尾开始切片:

In [416]: seq[-4:] In [417]: seq[-6:-2]

Out[416]: [5, 6, 0, 1] Out[417]: [6, 3, 5, 6]

切片的语法需要花点时间去适应,尤其是当你原来用的是R或MATLAB时。图A-2形象地说明了正整数和负整数的切片过程。

还可以在第二个冒号后面加上步长(step)。比如每隔一位取出一个元素:

In [418]: seq[::2]

Out[418]: [7, 3, 3, 6, 1]

在这里使用-1是一个很巧妙的办法,它可以实现列表或元组的反序:

In [419]: seq[::-1]

Out[419]: [1, 0, 6, 5, 3, 6, 3, 2, 7]

Image00152.jpg

图A-2:Python的切片方式

内置的序列函数

Python有一些很不错的序列函数,你应该熟悉它们,只要有机会就用。

enumerate

在对一个序列进行迭代时,常常需要跟踪当前项的索引。下面是一种DIY的办法:

i = 0

for value in collection:

# 用value做一些事情

i += 1

由于这种事情很常见,所以Python就内置了一个enumerate函数,它可以逐个返回序列的(i,value)元组:

for i, value in enumerate(collection):

# 用value做一些事情

在对数据进行索引时,enumerate还有一种不错的使用模式,即求取一个将序列值(假定是唯一的)映射到其所在位置的字典。

In [420]: some_list = ['foo', 'bar', 'baz']

In [421]: mapping = dict((v, i) for i, v in enumerate(some_list))

In [422]: mapping

Out[422]: {'bar': 1, 'baz': 2, 'foo': 0}

sorted

sorted函数可以将任何序列返回为一个新的有序列表:

In [423]: sorted([7, 1, 2, 6, 0, 3, 2])

Out[423]: [0, 1, 2, 2, 3, 6, 7]

In [424]: sorted('horse race')

Out[424]: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']

常常将sorted和set结合起来使用以得到一个由序列中的唯一元素组成的有序列表:

In [425]: sorted(set('this is just some string'))

Out[425]: [' ', 'e', 'g', 'h', 'i', 'j', 'm', 'n', 'o', 'r', 's', 't', 'u']

zip

zip用于将多个序列(列表、元组等)中的元素“配对”,从而产生一个新的元组列表:

In [426]: seq1 = ['foo', 'bar', 'baz']

In [427]: seq2 = ['one', 'two', 'three']

In [428]: zip(seq1, seq2)

Out[428]: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]

zip可以接受任意数量的序列,最终得到的元组数量由最短的序列决定:

In [429]: seq3 = [False, True]

In [430]: zip(seq1, seq2, seq3)

Out[430]: [('foo', 'one', False), ('bar', 'two', True)]

zip最常见的用法是同时迭代多个序列,还可以结合enumerate一起使用:

In [431]: for i, (a, b) in enumerate(zip(seq1, seq2)):

...: print('%d: %s, %s' % (i, a, b))

...:

0: foo, one

1: bar, two

2: baz, three

对于“已压缩的”(zipped)序列,zip还有一个很巧妙的用法,即对该序列进行“解压”(unzip)。其实就是将一组行转换为一组列。其语法看起来有点神秘:

In [432]: pitchers = [('Nolan', 'Ryan'), ('Roger', 'Clemens'),

...: ('Schilling', 'Curt')]

In [433]: first_names, last_names = zip(*pitchers)

In [434]: first_names

Out[434]: ('Nolan', 'Roger', 'Schilling')

In [435]: last_names

Out[435]: ('Ryan', 'Clemens', 'Curt')

稍后我将详细讨论函数调用中星号(*)的用法。其实它相当于:

zip(seq[0], seq[1], ..., seq[len(seq) - 1])

reversed

reversed用于按逆序迭代序列中的元素:

In [436]: list(reversed(range(10)))

Out[436]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

字典

字典(dict)可算是Python中最重要的内置数据结构。它更常见的名字是哈希映射(hash map)或相联数组(associative array)。它是一种大小可变的键值对集,其中的键(key)和值(value)都是Python对象。创建字典的方式之一是:使用大括号({})并用冒号分隔键和值。

In [437]: empty_dict = {}

In [438]: d1 = {'a' : 'some value', 'b' : [1, 2, 3, 4]}

In [439]: d1

Out[439]: {'a': 'some value', 'b': [1, 2, 3, 4]}

访问(以及插入、设置)元素的语法跟列表和元组是一样的:

In [440]: d1[7] = 'an integer'

In [441]: d1

Out[441]: {7: 'an integer', 'a': 'some value', 'b': [1, 2, 3, 4]}

In [442]: d1['b']

Out[442]: [1, 2, 3, 4]

你可以判断字典中是否存在某个键,其语法跟在列表和元组中判断是否存在某个值是一样的:

In [443]: 'b' in d1

Out[443]: True

使用del关键字或pop方法(删除指定值之后将其返回)可以删除值:

In [444]: d1[5] = 'some value'

In [445]: d1['dummy'] = 'another value'

In [446]: del d1[5]

In [447]: ret = d1.pop('dummy')

In [448]: ret

Out[448]: 'another value'

keys和values方法分别用于获取键和值的列表。虽然键值对没有特定的顺序,但这两个函数会以相同的顺序输出键和值:

In [449]: d1.keys() In [450]: d1.values()

Out[449]: ['a', 'b', 7] Out[450]: ['some value', [1, 2, 3, 4], 'an integer']

警告:

如果你正在使用Python 3,则dict.keys()和dict.values()会返回迭代器而不是列表。

利用update方法,一个字典可以被合并到另一个字典中去:

In [451]: d1.update({'b' : 'foo', 'c' : 12})

In [452]: d1

Out[452]: {7: 'an integer', 'a': 'some value', 'b': 'foo', 'c': 12}

从序列类型创建字典

有时你可能会想将两个序列中的元素两两配对地组成一个字典。粗略分析一下之后,你可能会写出这样的代码:

mapping = {}

for key, value in zip(key_list, value_list):

mapping[key] = value

由于字典本质上就是一个二元元组集,所以我们完全可以用dict类型函数直接处理二元元组列表:

In [453]: mapping = dict(zip(range(5), reversed(range(5))))

In [454]: mapping

Out[454]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}

稍后我们将讨论有关字典推导式的知识,这是构造字典的另一种优雅的方式。

默认值

下面这样的逻辑很常见:

if key in some_dict:

value = some_dict[key]

else:

value = default_value

其实dict的get和pop方法可以接受一个可供返回的默认值,于是,上面的if-else块就可以被简单地写成:

value = some_dict.get(key, default_value)

如果key不存在,则get默认返回None,而pop则会引发一个异常。在设置值的时候,常常会将字典中的值处理成别的集类型(比如列表)。例如,根据首字母对一组单词进行分类并最终产生一个由列表组成的字典:

In [455]: words = ['apple', 'bat', 'bar', 'atom', 'book']

In [456]: by_letter = {}

In [457]: for word in words:

...: letter = word[0]

...: if letter not in by_letter:

...: by_letter[letter] = [word]

...: else:

...: by_letter[letter].append(word)

...:

In [458]: by_letter

Out[458]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

字典的setdefault方法刚好能达到这个目的。上面的if-else块可以写成:

by_letter.setdefault(letter, []).append(word)

内置的collections模块有一个叫做defaultdict的类,它可以使该过程更简单。传入一个类型或函数(用于生成字典各插槽所使用的默认值)即可创建出一个defaultdict:

from collections import defaultdict

by_letter = defaultdict(list)

for word in words:

by_letter[word[0]].append(word)

defaultdict的初始化器只需要一个可调用对象(例如各种函数),并不需要明确的类型。因此,如果你想要将默认值设置为4,只需传入一个能够返回4的函数即可:

counts = defaultdict(lambda: 4)

字典键的有效类型

虽然字典的值可以是任何Python对象,但键必须是不可变对象,如标量类型(整数、浮点数、字符串)或元组(元组中的所有对象也必须是不可变的)。这里的术语是可哈希性(hashability)译注7

。通过hash函数,你可以判断某个对象是否是可哈希的(即可以用作字典的键):

In [459]: hash('string')

Out[459]: -9167918882415130555

In [460]: hash((1, 2, (2, 3)))

Out[460]: 1097636502276347782

In [461]: hash((1, 2, [2, 3])) # 这里会失败,因为列表是可变的

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 hash((1, 2, [2, 3])) # 这里会失败,因为列表是可变的

TypeError: unhashable type: 'list'

如果要将列表当做键,最简单的办法就是将其转换成元组:

In [462]: d = {}

In [463]: d[tuple([1, 2, 3])] = 5

In [464]: d

Out[464]: {(1, 2, 3): 5}

集合

集合(set)是由唯一元素组成的无序集。你可以将其看成是只有键而没有值的字典。集合的创建方式有二:set函数或用大括号包起来的集合字面量:

In [465]: set([2, 2, 2, 1, 3, 3])

Out[465]: set([1, 2, 3])

In [466]: {2, 2, 2, 1, 3, 3}

Out[466]: set([1, 2, 3])

集合支持各种数学集合运算,如并、交、差以及对称差等。表A-3列出了常用的集合方法:

In [467]: a = {1, 2, 3, 4, 5}

In [468]: b = {3, 4, 5, 6, 7, 8}

In [469]: a | b # 并(或)

Out[469]: set([1, 2, 3, 4, 5, 6, 7, 8])

In [470]: a & b # 交(与)

Out[470]: set([3, 4, 5])

In [471]: a - b # 差

Out[471]: set([1, 2])

In [472]: a ^ b # 对称差(异或)

Out[472]: set([1, 2, 6, 7, 8])

你还可以判断一个集合是否是另一个集合的子集(原集合包含于新集合)或超集(原集合包含新集合):

In [473]: a_set = {1, 2, 3, 4, 5}

In [474]: {1, 2, 3}.issubset(a_set)

Out[474]: True

In [475]: a_set.issuperset({1, 2, 3})

Out[475]: True

不难看出,如果两个集合的内容相等,则它们就是相等的:

In [476]: {1, 2, 3} == {3, 2, 1}

Out[476]: True

Image00153.jpg

列表、集合以及字典的推导式

列表推导式是最受欢迎的Python语言特性之一。它使你能够非常简洁地构造一个新列表:只需一条简洁的表达式,即可对一组元素进行过滤,并对得到的元素进行转换变形。其基本形式如下:

[expr for val in collection if condition]

这相当于下面这段for循环:

result = []

for val in collection:

if condition:

result.append(expr)

过滤器条件可以省略,只留下表达式。例如,给定一个字符串列表,我们可以滤除长度小于等于2的字符串,并将剩下的字符串转换成大写字母形式:

In [477]: strings = ['a', 'as', 'bat', 'car', 'dove', 'python']

In [478]: [x.upper() for x in strings if len(x) > 2]

Out[478]: ['BAT', 'CAR', 'DOVE', 'PYTHON']

集合和字典的推导式是该思想的一种自然延伸,它们的语法差不多,只不过产生的是集合和字典而已。字典推导式的基本形式如下:

dict_comp = {key-expr : value-expr for value in collection if condition}

集合推导式跟列表推导式非常相似,唯一的区别就是它用的是花括号而不是方括号:

set_comp = {expr for value in collection if condition}

跟列表推导式一样,集合和字典的推导式也都只是语法糖而已,但它们确实能使代码变得更容易读写。再以上面那个字符串列表为例,假设我们想要构造一个集合,其内容为原列表字符串的各种长度。使用集合推导式即可轻松实现此功能:

In [479]: unique_lengths = {len(x) for x in strings}

In [480]: unique_lengths

Out[480]: set([1, 2, 3, 4, 6])

再来看一个简单的字典推导式范例。我们可以为这些字符串创建一个指向其列表位置的映射关系:

In [481]: loc_mapping = {val : index for index, val in enumerate(strings)}

In [482]: loc_mapping

Out[482]: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}

实际上,该字典还可以这样构造:

loc_mapping = dict((val, idx) for idx, val in enumerate(strings))

依我看,字典推导式版的代码要更短也更清晰。

注意:

字典和集合的推导式是最近才加入到Python的(Python 2.7和Python 3.1+)。

嵌套列表推导式

假设我们有一个由男孩名列表和女孩名列表组成的列表(即列表的列表):

In [483]: all_data = [['Tom', 'Billy', 'Jefferson', 'Andrew', 'Wesley', 'Steven', 'Joe'],

...: ['Susie', 'Casey', 'Jill', 'Ana', 'Eva', 'Jennifer', 'Stephanie']]

这些名字可能是从多个文件中读取出来的,而且专门将男孩女孩的名字分开。现在,假设我们想要找出带有两个以上(含)字母e的名字,并将它们放入一个新列表中。我们当然可以用一个简单的for循环来实现:

names_of_interest = []

for names in all_data:

enough_es = [name for name in names if name.count('e') > 2]译注8

names_of_interest.extend(enough_es)

实际上,整个运算过程完全可以写成一条嵌套列表推导式,如下所示:

In [484]: result = [name for names in all_data for name in names

...: if name.count('e') >= 2]

In [485]: result

Out[485]: ['Jefferson', 'Wesley', 'Steven', 'Jennifer', 'Stephanie']

乍看起来,嵌套列表推导式确实不太好理解。推导式中for的部分是按嵌套顺序排列的,而过滤条件则还是跟之前一样是放在后面的。下面是另外一个例子,将一个由整数元组构成的列表“扁平化”为一个简单的整数列表:

In [486]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]

In [487]: flattened = [x for tup in some_tuples for x in tup]

In [488]: flattened

Out[488]: [1, 2, 3, 4, 5, 6, 7, 8, 9]

其实你可以这样来记:嵌套for循环中各个for的顺序是怎样的,嵌套推导式中各个for表达式的顺序就是怎样的。

flattened = []

for tup in some_tuples:

for x in tup:

flattened.append(x)

你可以编写任意多层的嵌套,但是如果嵌套超过两三层的话,可能你就得思考一下数据结构设计有没有问题了。一定要注意上面那种语法跟“列表推导式中的列表推导式”之间的区别。比如下面这条语句也是正确的,但结果不同:

In [229]: [[x for x in tup] for tup in some_tuples]

函数

函数是Python中最主要也是最重要的代码组织和复用手段。也许并不存在拥有超级多函数的东西。实际上,我严重认为大部分程序员在做数据分析工作时所编写的函数不够多!从前面的例子中不难看出,函数是用def关键字声明的,并使用return关键字返回:

def my_function(x, y, z=1.5):

if z > 1:

return z * (x + y)

else:

return z / (x + y)

同时拥有多条return语句也是可以的。如果到达函数末尾时没有遇到任何一条return语句,则返回None。

函数可以有一些位置参数(positional)和一些关键字参数(keyword)。关键字参数通常用于指定默认值或可选参数。在上面的函数中,x和y是位置参数,而z则是关键字参数。也就是说,该函数可以下面这两种方式进行调用:

my_function(5, 6, z=0.7)

my_function(3.14, 7, 3.5)

函数参数的主要限制在于:关键字参数必须位于位置参数(如果有的话)之后。你可以任何顺序指定关键字参数。也就是说,你不用死记硬背函数参数的顺序,只要记得它们的名字就可以了。

命名空间、作用域,以及局部函数

函数可以访问两种不同作用域中的变量:全局(global)和局部(local)。Python有一种更科学的用于描述变量作用域的名称,即命名空间(namespace)。任何在函数中赋值的变量默认都是被分配到局部命名空间(local namespace)中的。局部命名空间是在函数被调用时创建的,函数参数会立即填入该命名空间。在函数执行完毕之后,局部命名空间就会被销毁(会有一些例外的情况,具体请参见后面介绍闭包的那一节)。看看下面这个函数:

def func():

a = []

for i in range(5):

a.append(i)

调用func()之后,首先会创建出空列表a,然后添加5个元素,最后a会在该函数退出的时候被销毁。假如我们像下面这样定义a:

a = []

def func():

for i in range(5):

a.append(i)

虽然可以在函数中对全局变量进行赋值操作,但是那些变量必须用global关键字声明成全局的才行:

In [489]: a = None

In [490]: def bind_a_variable():

...: global a

...: a = []

...: bind_a_variable()译注9

...:

In [491]: print a

[]

警告:

我常常建议人们不要频繁使用global关键字。因为全局变量一般是用于存放系统的某些状态的。如果你发现自己用了很多,那可能就说明得要来点儿面向对象编程了(即使用类)。

可以在任何位置进行函数声明,即使是局部函数(在外层函数被调用之后才会被动态创建出来)也是可以的:

def outer_function(x, y, z):

def inner_function(a, b, c):

pass

pass

在上面的代码中,inner_function在outer_function被调用之前是不存在的。只要outer_function结束执行,则inner_function将会立即被销毁。

各个嵌套的内层函数可以访问其上层函数的局部命名空间,但不能绑定新变量。我将在讲解闭包的时候再对此问题进行讨论。

严格意义上来说,所有函数都是某个作用域的局部函数,这个作用域可能刚好就是模块级的作用域。

返回多个值

在我第一次用Python编程时(之前已经习惯了Java和C++),最喜欢的一个功能是:函数可以返回多个值。下面是一个简单的例子:

def f():

a = 5

b = 6

c = 7

return a, b, c

a, b, c = f()

在数据分析和其他科学计算应用中,你会发现自己常常这么干,因为许多函数都可能会有多个输出(在该函数内部计算出的数据结构或其他辅助数据)。如果回忆一下本章早前讲过的元组打包和拆包功能,你可能会明白这到底是怎么一回事:该函数其实只返回了一个对象,也就是一个元组,最后该元组会被拆包到各个结果变量中。在上面的例子中,我们还可以这样写:

return_value = f()

不难看出,这里的return_value将会是一个含有3个返回值的三元元组。此外,还有一种非常具有吸引力的多值返回方式——返回字典:

def f():

a = 5

b = 6

c = 7

return {'a' : a, 'b' : b, 'c' : c}

函数亦为对象

由于Python函数都是对象,因此,在其他语言中较难表达的一些设计思想在Python中就要简单很多了。假设我们有下面这样一个字符串数组,希望对其进行一些数据清理工作并执行一堆转换:

states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',

'south carolina##', 'West virginia?']

不管是谁,只要处理过由用户提交的调查数据,就能明白这种乱七八糟的数据是怎么一回事。为了得到一组能用于分析工作的格式统一的字符串,需要做很多事情:去除空白符、删除各种标点符号、正确的大写格式等。乍一看上去,我们可能会写出下面这样的代码:

import re # 正则表达式模块

def clean_strings(strings):

result = []

for value in strings:

value = value.strip()

value = re.sub('[!#?]', '', value) # 移除标点符号

value = value.title()

result.append(value)

return result

最终结果如下所示:

In [15]: clean_strings(states)

Out[15]:

['Alabama',

'Georgia',

'Georgia',

'Georgia',

'Florida',

'South Carolina',

'West Virginia']

其实还有另外一种不错的办法:将需要在一组给定字符串上执行的所有运算做成一个列表:

def remove_punctuation(value):

return re.sub('[!#?]', '', value)

clean_ops = [str.strip, remove_punctuation, str.title]

def clean_strings(strings, ops):

result = []

for value in strings:

for function in ops:

value = function(value)

result.append(value)

return result

然后我们就有了:

In [22]: clean_strings(states, clean_ops)

Out[22]:

['Alabama',

'Georgia',

'Georgia',

'Georgia',

'Florida',

'South Carolina',

'West Virginia']

这种多函数模式使你能在很高的层次上轻松修改字符串的转换方式。此时的clean_strings也更具可复用性!

还可以将函数用作其他函数的参数,比如内置的map函数,它用于在一组数据上应用一个函数:

In [23]: map(remove_punctuation, states)

Out[23]:

[' Alabama ',

'Georgia',

'Georgia',

'georgia',

'FlOrIda',

'south carolina',

'West virginia']

匿名(lambda)函数

Python有一种被称为匿名函数或lambda函数的东西,这其实是一种非常简单的函数:仅由单条语句组成,该语句的结果就是返回值。它们是通过lambda关键字定义的,这个关键字没有别的含义,仅仅是说“我们正在声明的是一个匿名函数”。

def short_function(x):

return x * 2

equiv_anon = lambda x: x * 2

本书其余部分一般将其称为lambda函数。它们在数据分析工作中非常方便,因为你会发现很多数据转换函数都以函数作为参数的。直接传入lambda函数比编写完整函数声明要少输入很多字(也更清晰),甚至比将lambda函数赋值给一个变量还要少输入很多字。看看下面这个简单得有些傻的例子:

def apply_to_list(some_list, f):

return [f(x) for x in some_list]

ints = [4, 0, 1, 5, 6]

apply_to_list(ints, lambda x: x * 2)

虽然你可以直接编写[x *2for x in ints],但是这里我们可以非常轻松地传入一个自定义运算给apply_to_list函数。

再来看另外一个例子。假设有一组字符串,你想要根据各字符串不同字母的数量对其进行排序:

In [492]: strings = ['foo', 'card', 'bar', 'aaaa', 'abab']

这里,我们可以传入一个lambda函数到列表的sort方法:

In [493]: strings.sort(key=lambda x: len(set(list(x))))

In [494]: strings

Out[494]: ['aaaa', 'foo', 'abab', 'bar', 'card']

注意:

lambda函数之所以会被称为匿名函数,原因之一就是这种函数对象本身是没有提供名称属性的。

闭包:返回函数的函数

闭包(closure)不是什么很可怕的东西。如果用对了地方,它们其实可以非常强大!简而言之,闭包就是由其他函数动态生成并返回的函数。其关键性质是,被返回的函数可以访问其创建者的局部命名空间中的变量。下面是一个非常简单的例子:

def make_closure(a):

def closure():

print('I know the secret: %d' % a)

return closure

closure = make_closure(5)

闭包和标准Python函数之间的区别在于:即使其创建者已经执行完毕,闭包仍能继续访问其创建者的局部命名空间。因此,在上面这种情况中,返回的闭包将可打印出"I know the secret:5"。虽然闭包的内部状态(在本例中,只有值a)一般都是静态的,但也允许使用可变对象(如字典、集合、列表等可以被修改的对象)。例如,下面这个函数可以返回一个能够记录其参数(曾经传入的一切参数)的函数:

def make_watcher():

have_seen = {}

def has_been_seen(x):

if x in have_seen:

return True

else:

have_seen[x] = True

return False

return has_been_seen

对一组整数使用该函数,可以得到:

In [496]: watcher = make_watcher()

In [497]: vals = [5, 6, 1, 5, 1, 6, 3, 5]

In [498]: [watcher(x) for x in vals]

Out[498]: [False, False, False, True, True, True, False, True]

但是要注意一个技术限制:虽然可以修改任何内部状态对象(比如向字典添加键值对),但不能绑定外层函数作用域中的变量。一个解决办法是:修改字典或列表,而不是绑定变量。

def make_counter():

count = [0]

def counter():

# 增加并返回当前的count

count[0] += 1

return count[0]

return counter

counter = make_counter()

你可能会想,这到底有什么用。在实际工作中,你可以编写带有大量选项的非常一般化的函数,然后再组装出更简单更专门化的函数。下面这个例子中创建了一个字符串格式化函数:

def format_and_pad(template, space):

def formatter(x):

return (template % x).rjust(space)

return formatter

然后,你可以创建一个始终返回15位字符串的浮点数格式化器,如下所示:

In [500]: fmt = format_and_pad('%.4f', 15)

In [501]: fmt(1.756)

Out[501]: ' 1.7560'

如果多学一些Python面向对象编程方面的知识,你就会发现这种模式其实也能用类来实现(虽然会更嗦一点)。

扩展调用语法和*args、**kwargs

在Python中,函数参数的工作方式其实很简单。当你编写func(a,b,c,d=some,e=value)时,位置和关键字参数其实分别是被打包成元组和字典的。函数实际接收到的是一个元组args和一个字典kwargs,并在内部完成如下转换:

a, b, c = args

d = kwargs.get('d', d_default_value)

e = kwargs.get('e', e_default_value)

这一切都是在幕后悄悄发生的。当然,它还会执行一些错误检查,还允许你将位置参数当成关键字参数那样进行指定(即使它们在函数定义中并不是关键字参数)。

def say_hello_then_call_f(f, *args, **kwargs):

print 'args is', args

print 'kwargs is', kwargs

print("Hello! Now I'm going to call %s" % f)

return f(*args, **kwargs)

def g(x, y, z=1):

return (x + y) / z

然后,如果我们通过say_hello_then_call_f调用g,就会得到:

In [8]: say_hello_then_call_f(g, 1, 2, z=5.)

args is (1, 2)

kwargs is {'z': 5.0}

Hello! Now I'm going to call

Out[8]: 0.6

柯里化:部分参数应用

柯里化(currying)是一个有趣的计算机科学术语,它指的是通过“部分参数应用”(partial argument application)从现有函数派生出新函数的技术。假设我们有一个执行两数相加的简单函数:

def add_numbers(x, y):

return x + y

通过这个函数,我们可以派生出一个新的只有一个参数的函数——add_five,它用于对其参数加5:

add_five = lambda y: add_numbers(5, y)

add_numbers的第二个参数称为“柯里化的”(curried)。这里没什么特别花哨的东西,因为我们其实就只是定义了一个可以调用现有函数的新函数而已。内置的functools模块可以用partial函数将此过程简化:

from functools import partial

add_five = partial(add_numbers, 5)

在讨论pandas和时间序列数据时,我们将会用该技术去创建专门的数据序列转换函数:

# 计算时间序列x的60日移动平均

ma60 = lambda x: pandas.rolling_mean(x, 60)

# 计算data中所有时间序列的60日移动平均

data.apply(ma60)

生成器

能以一种一致的方式对序列进行迭代(比如列表中的对象或文件中的行)是Python的一个重要特点。这是通过一种叫做迭代器协议(iterator protocol,它是一种使对象可迭代的通用方式)的方式实现的。比如说,对字典进行迭代可以得到其所有的键:

In [502]: some_dict = {'a': 1, 'b': 2, 'c': 3}

In [503]: for key in some_dict:

...: print key, 译注10

a c b

当你编写for key in some_dict时,Python解释器首先会尝试从some_dict创建一个迭代器:

In [504]: dict_iterator = iter(some_dict)

In [505]: dict_iterator

Out[505]:

迭代器是一种特殊对象,它可以在诸如for循环之类的上下文中向Python解释器输送对象。大部分能接受列表之类的对象的方法也都可以接受任何可迭代对象。比如min、max、sum等内置方法以及list、tuple等类型构造器:

In [506]: list(dict_iterator)

Out[506]: ['a', 'c', 'b']

生成器(generator)是构造新的可迭代对象的一种简单方式。一般的函数执行之后只会返回单个值,而生成器则是以延迟的方式返回一个值序列,即每返回一个值之后暂停,直到下一个值被请求时再继续。要创建一个生成器,只需将函数中的return替换为yeild即可:

def squares(n=10):

for i in xrange(1, n + 1):

print 'Generating squares from 1 to %d' % (n ** 2)译注11

yield i ** 2

调用该生成器时,没有任何代码会被立即执行:

In [2]: gen = squares()

In [3]: gen

Out[3]:

直到你从该生成器中请求元素时,它才会开始执行其代码:

In [4]: for x in gen:

...: print x,

...:

Generating squares from 0 to 100

1 4 9 16 25 36 49 64 81 100

假设我们希望找出“将1美元(即100美分)兑换成任意一组硬币”的所有唯一方式。你可能会想出很多种实现办法(包括“已找到的唯一组合”的保存方式)。下面我们编写一个生成器来产生这样的硬币组合(硬币面额用整数表示):

def make_change(amount, coins=[1, 5, 10, 25], hand=None):

hand = [] if hand is None else hand

if amount == 0:

yield hand

for coin in coins:

# 确保我们给出的硬币没有超过总额,且组合是唯一的

if coin > amount or (len(hand) > 0 and hand[-1] < coin):

continue

for result in make_change(amount - coin, coins=coins,

hand=hand + [coin]):

yield result

这个算法的细节并不重要(你能想出一个更短点的办法吗?)。然后我们可以编写:

In [508]: for way in make_change(100, coins=[10, 25, 50]):

...: print way

[10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

[25, 25, 10, 10, 10, 10, 10]

[25, 25, 25, 25]

[50, 10, 10, 10, 10, 10]

[50, 25, 25]

[50, 50]

In [509]: len(list(make_change(100)))

Out[509]: 242

生成器表达式

生成器表达式(generator expression)是构造生成器的最简单方式。生成器也有一个类似于列表、字典、集合推导式的东西,其创建方式为,把列表推导式两端的方括号改成圆括号:

In [510]: gen = (x ** 2 for x in xrange(100))

In [511]: gen

Out[511]: at 0x10a0a31e0>

它跟下面这个冗长得多的生成器是完全等价的:

def _make_gen():

for x in xrange(100):

yield x ** 2

gen = _make_gen()

生成器表达式可用于任何接受生成器的Python函数:

In [512]: sum(x ** 2 for x in xrange(100))

Out[512]: 328350

In [513]: dict((i, i **2) for i in xrange(5))

Out[513]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

itertools模块

标准库itertools模块中有一组用于许多常见数据算法的生成器。例如,groupby可以接受任何序列和一个函数。它根据函数的返回值对序列中的连续元素进行分组。下面是一个例子:

In [514]: import itertools

In [515]: first_letter = lambda x: x[0]

In [516]: names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']

In [517]: for letter, names in itertools.groupby(names, first_letter):

...: print letter, list(names) # names是一个生成器

A ['Alan', 'Adam']

W ['Wes', 'Will']

A ['Albert']

S ['Steven']

表A-4中列出了一些我经常用到的itertools函数。

Image00154.jpg

Image00155.jpg

注意:

许多在Python 2(itertools)中产生列表的内置函数(如zip、map、filter等),在Python 3中都被换成了其生成器版。

文件和操作系统

本书的代码示例大多使用诸如pandas.read_csv之类的高级工具将磁盘上的数据文件读入Python数据结构。但我们还是需要了解一些有关Python文件处理方面的基础知识。好在它本来就很简单,这也是Python在文本和文件处理方面的如此流行的原因之一。

为了打开一个文件以便读写,可以使用内置的open函数以及一个相对或绝对的文件路径:

In [518]: path = 'ch13/segismundo.txt'

In [519]: f = open(path)

默认情况下,文件是以只读模式('r')打开的。然后,我们就可以像处理列表那样来处理这个文件句柄f了,比如对行进行迭代:

for line in f:

pass

从文件中取出的行都带有完整的行结束符(EOL),因此你常常会看到下面这样的代码(得到一组没有EOL的行):

In [520]: lines = [x.rstrip() for x in open(path)]

In [521]: lines

Out[521]:

['Sue\xc3\xb1a el rico en su riqueza,',

'que m\xc3\xa1s cuidados le ofrece;',

'',

'sue\xc3\xb1a el pobre que padece',

'su miseria y su pobreza;',

'',

'sue\xc3\xb1a el que a medrar empieza,',

'sue\xc3\xb1a el que afana y pretende,',

'sue\xc3\xb1a el que agravia y ofende,',

'',

'y en el mundo, en conclusi\xc3\xb3n,',

'todos sue\xc3\xb1an lo que son,',

'aunque ninguno lo entiende.',

'']

如果输入f =open(path,'w'),就会有一个新文件被创建在ch13/segismundo.txt,并覆盖掉该位置原来的任何数据。表A-5列出了所有可用的文件读写模式。

Image00156.jpg

译注12:这的“名”包括路径。

要将文本写入文件,可以使用该文件的write或writelines方法。例如,我们可以创建一个无空行版的prof_mod.py译注13

,如下所示:

In [522]: with open('tmp.txt', 'w') as handle:

.....: handle.writelines(x for x in open(path) if len(x) > 1)

In [523]: open('tmp.txt').readlines()

Out[523]:

['Sue\xc3\xb1a el rico en su riqueza,\n',

'que m\xc3\xa1s cuidados le ofrece;\n',

'sue\xc3\xb1a el pobre que padece\n',

'su miseria y su pobreza;\n',

'sue\xc3\xb1a el que a medrar empieza,\n',

'sue\xc3\xb1a el que afana y pretende,\n',

'sue\xc3\xb1a el que agravia y ofende,\n',

'y en el mundo, en conclusi\xc3\xb3n,\n',

'todos sue\xc3\xb1an lo que son,\n',

'aunque ninguno lo entiende.\n']

表A-6列出了一些最常用的文件方法。

Image00157.jpg

译注1

:这里只是作者起的名字而已,不必介怀,你完全可以给它起个“真命天子类型”之类的名字。其实它是一个哲学和逻辑学概念,就是说“对于一只鸟类动物,不用管它到底是不是鸭子,只要看它像不像鸭子就可以了”。

译注2

:也就是定义别名。

译注3

:在函数式编程中,也常译作惰性求值。

译注4

:这个词指的是“不能修改原内存块的数据”。也就是说,即使修改操作成功了,也只是创建了一个新对象并将其引用赋值给原变量而已。

译注5

:作者用的比我现在用的版本还老。所以在阅读本书的过程中有些例子的计算结果不一定跟书上的完全一致。

译注6

:分子也可以的。

译注7

:或者翻译成可散列性。

译注8

:应该是">=",因为原文是"two and more"。

译注9

:注意缩进,别搞成递归了。

译注10

:注意这里的逗号。

译注11

:应该放到for循环之前,否则后面的执行结果与书上的不一样。

译注13

:应该是segismundo.txt。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值