5.1 读写文本数据
你需要读写各种不同编码的文本数据,比如ASCII,UTF-8或UTF-16编码等。
使用带有 rt 模式的 open() 函数读取文本文件。如下所示:
# Read the entire file as a single string
with open('somefile.txt', 'rt') as f:
data = f.read()
# Iterate over the lines of the file
with open('somefile.txt', 'rt') as f:
for line in f:
# process line
...
类似的,为了写入一个文本文件,使用带有 wt 模式的 open() 函数, 如果之前文件内容存在则清除并覆盖掉。如下所示:
# Write chunks of text data
with open('somefile.txt', 'wt') as f:
f.write(text1)
f.write(text2)
...
# Redirected print statement
with open('somefile.txt', 'wt') as f:
print(line1, file=f)
print(line2, file=f)
...
如果是在已存在文件中添加内容,使用模式为 at 的 open() 函数。
5.2 打印输出至文件中
你想将 print() 函数的输出重定向到一个文件中去。
在 print() 函数中指定 file 关键字参数,像下面这样:
with open('d:/work/test.txt', 'wt') as f:
print('Hello World!', file=f)
5.3 使用其他分隔符或行终止符打印
你想使用 print() 函数输出数据,但是想改变默认的分隔符或者行尾符。
可以使用在 print() 函数中使用 sep 和 end 关键字参数,以你想要的方式输出。比如:
>>> print('ACME', 50, 91.5)
ACME 50 91.5
>>> print('ACME', 50, 91.5, sep=',')
ACME,50,91.5
>>> print('ACME', 50, 91.5, sep=',', end='!!\n')
ACME,50,91.5!!
>>>
使用 end 参数也可以在输出中禁止换行。
5.4 读写字节数据
你想读写二进制文件,比如图片,声音文件等等。
使用模式为 rb 或 wb 的 open() 函数来读取或写入二进制数据。比如:
# Read the entire file as a single byte string
with open('somefile.bin', 'rb') as f:
data = f.read()
# Write binary data to a file
with open('somefile.bin', 'wb') as f:
f.write(b'Hello World')
在读取二进制数据时,需要指明的是所有返回的数据都是字节字符串格式的,而不是文本字符串。 类似的,在写入的时候,必须保证参数是以字节形式对外暴露数据的对象(比如字节字符串,字节数组对象等)。
5.5 文件不存在才能写入
你想像一个文件中写入数据,但是前提必须是这个文件在文件系统上不存在。 也就是不允许覆盖已存在的文件内容。
可以在 open() 函数中使用 x 模式来代替 w 模式的方法来解决这个问题。比如:
>>> with open('somefile', 'wt') as f:
... f.write('Hello\n')
...
>>> with open('somefile', 'xt') as f:
... f.write('Hello\n')
...
Traceback (most recent call last):
File "", line 1, in
FileExistsError: [Errno 17] File exists: 'somefile'
>>>
如果文件是二进制的,使用 xb 来代替 xt
5.6 字符串的I/O操作
你想使用操作类文件对象的程序来操作文本或二进制字符串。
使用 io.StringIO() 和 io.BytesIO() 类来创建类文件对象操作字符串数据。
当你想模拟一个普通的文件的时候 StringIO 和 BytesIO 类是很有用的。 比如,在单元测试中,你可以使用 StringIO 来创建一个包含测试数据的类文件对象, 这个对象可以被传给某个参数为普通文件对象的函数
5.7 读写压缩文件
你想读写一个gzip或bz2格式的压缩文件。
gzip 和 bz2 模块可以很容易的处理这些文件。 两个模块都为 open() 函数提供了另外的实现来解决这个问题。 比如,为了以文本形式读取压缩文件,可以这样做:
# gzip compression
import gzip
with gzip.open('somefile.gz', 'rt') as f:
text = f.read()
# bz2 compression
import bz2
with bz2.open('somefile.bz2', 'rt') as f:
text = f.read()
大部分情况下读写压缩数据都是很简单的。但是要注意的是选择一个正确的文件模式是非常重要的。 如果你不指定模式,那么默认的就是二进制模式,如果这时候程序想要接受的是文本数据,那么就会出错。 gzip.open() 和 bz2.open() 接受跟内置的 open() 函数一样的参数, 包括 encoding,errors,newline 等等。
5.8 固定大小记录的文件迭代
你想在一个固定长度记录或者数据块的集合上迭代,而不是在一个文件中一行一行的迭代。
通过下面这个小技巧使用 iter 和 functools.partial() 函数:
from functools import partial
RECORD_SIZE = 32
with open('somefile.data', 'rb') as f:
records = iter(partial(f.read, RECORD_SIZE), b'')
for r in records:
...
这个例子中的 records 对象是一个可迭代对象,它会不断的产生固定大小的数据块,直到文件末尾。 要注意的是如果总记录大小不是块大小的整数倍的话,最后一个返回元素的字节数会比期望值少。
5.9 读取二进制数据到可变缓冲区中
你想直接读取二进制数据到一个可变缓冲区中,而不需要做任何的中间复制操作。 或者你想原地修改数据并将它写回到一个文件中去。
为了读取数据到一个可变数组中,使用文件对象的 readinto() 方法。比如:
import os.path
def read_into_buffer(filename):
buf = bytearray(os.path.getsize(filename))
with open(filename, 'rb') as f:
f.readinto(buf)
return buf
文件对象的 readinto() 方法能被用来为预先分配内存的数组填充数据,甚至包括由 array 模块或 numpy 库创建的数组。 和普通 read() 方法不同的是, readinto() 填充已存在的缓冲区而不是为新对象重新分配内存再返回它们。 因此,你可以使用它来避免大量的内存分配操作。
使用 f.readinto() 时需要注意的是,你必须检查它的返回值,也就是实际读取的字节数。
5.10 内存映射的二进制文件
你想内存映射一个二进制文件到一个可变字节数组中,目的可能是为了随机访问它的内容或者是原地做些修改。
使用 mmap 模块来内存映射文件。 下面是一个工具函数,向你演示了如何打开一个文件并以一种便捷方式内存映射这个文件。
import os
import mmap
def memory_map(filename, access=mmap.ACCESS_WRITE):
size = os.path.getsize(filename)
fd = os.open(filename, os.O_RDWR)
return mmap.mmap(fd, size, access=access)
为了随机访问文件的内容,使用 mmap 将文件映射到内存中是一个高效和优雅的方法。 例如,你无需打开一个文件并执行大量的 seek() , read() , write() 调用, 只需要简单的映射文件并使用切片操作访问数据即可。
一般来讲, mmap() 所暴露的内存看上去就是一个二进制数组对象。 但是,你可以使用一个内存视图来解析其中的数据。
需要强调的一点是,内存映射一个文件并不会导致整个文件被读取到内存中。 也就是说,文件并没有被复制到内存缓存或数组中。相反,操作系统仅仅为文件内容保留了一段虚拟内存。 当你访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中。 而那些从没被访问到的部分还是留在磁盘上。所有这些过程是透明的,在幕后完成!
5.11 文件路径名的操作
你需要使用路径名来获取文件名,目录名,绝对路径等等。
使用 os.path 模块中的函数来操作路径名。 下面是一个交互式例子来演示一些关键的特性:
>>> import os
>>> path = '/Users/beazley/Data/data.csv'
>>> # Get the last component of the path
>>> os.path.basename(path)
'data.csv'
>>> # Get the directory name
>>> os.path.dirname(path)
'/Users/beazley/Data'
>>> # Join path components together
>>> os.path.join('tmp', 'data', os.path.basename(path))
'tmp/data/data.csv'
>>> # Expand the user's home directory
>>> path = '~/Data/data.csv'
>>> os.path.expanduser(path)
'/Users/beazley/Data/data.csv'
>>> # Split the file extension
>>> os.path.splitext(path)
('~/Data/data', '.csv')
>>>
5.12 测试文件是否存在
你想测试一个文件或目录是否存在。
使用 os.path 模块来测试一个文件或目录是否存在。比如:
import os
os.path.exists('/etc/passwd')
True
os.path.exists('/tmp/spam')
False
5.13 获取文件夹中的文件列表
你想获取文件系统中某个目录下的所有文件列表
使用 os.listdir() 函数来获取某个目录中的文件列表:
import os
names = os.listdir('somedir')
5.14 忽略文件名编码
你想使用原始文件名执行文件的I/O操作,也就是说文件名并没有经过系统默认编码去解码或编码过。
默认情况下,所有的文件名都会根据 sys.getfilesystemencoding() 返回的文本编码来编码或解码。比如:
>>> sys.getfilesystemencoding()
'utf-8'
>>>
如果因为某种原因你想忽略这种编码,可以使用一个原始字节字符串来指定一个文件名即可。
通常来讲,你不需要担心文件名的编码和解码,普通的文件名操作应该就没问题了。 但是,有些操作系统允许用户通过偶然或恶意方式去创建名字不符合默认编码的文件。 这些文件名可能会神秘地中断那些需要处理大量文件的Python程序。
读取目录并通过原始未解码方式处理文件名可以有效的避免这样的问题, 尽管这样会带来一定的编程难度。
5.15 打印不合法的文件名
你的程序获取了一个目录中的文件名列表,但是当它试着去打印文件名的时候程序崩溃, 出现了 UnicodeEncodeError 异常和一条奇怪的消息—— surrogates not allowed 。
当打印未知的文件名时,使用下面的方法可以避免这样的错误:
def bad_filename(filename):
return repr(filename)[1:-1]
try:
print(filename)
except UnicodeEncodeError:
print(bad_filename(filename))
5.16 增加或改变已打开文件的编码
你想在不关闭一个已打开的文件前提下增加或改变它的Unicode编码。
如果你想给一个以二进制模式打开的文件添加Unicode编码/解码方式, 可以使用 io.TextIOWrapper() 对象包装它。
如果你想修改一个已经打开的文本模式的文件的编码方式,可以先使用 detach() 方法移除掉已存在的文本编码层, 并使用新的编码方式代替。
5.17 将字节写入文本文件
你想在文本模式打开的文件中写入原始的字节数据。
将字节数据直接写入文件的缓冲区即可,例如:
>>> import sys
>>> sys.stdout.write(b'Hello\n')
Traceback (most recent call last):
File "", line 1, in
TypeError: must be str, not bytes
>>> sys.stdout.buffer.write(b'Hello\n')
Hello
5
>>>
类似的,能够通过读取文本文件的 buffer 属性来读取二进制数据。
5.18 将文件描述符包装成文件对象
你有一个对应于操作系统上一个已打开的I/O通道(比如文件、管道、套接字等)的整型文件描述符, 你想将它包装成一个更高层的Python文件对象。
一个文件描述符和一个打开的普通文件是不一样的。 文件描述符仅仅是一个由操作系统指定的整数,用来指代某个系统的I/O通道。 如果你碰巧有这么一个文件描述符,你可以通过使用 open() 函数来将其包装为一个Python的文件对象。 你仅仅只需要使用这个整数值的文件描述符作为第一个参数来代替文件名即可。
当高层的文件对象被关闭或者破坏的时候,底层的文件描述符也会被关闭。 如果这个并不是你想要的结果,你可以给 open() 函数传递一个可选的 colsefd=False 。
5.19 创建临时文件和文件夹
你需要在程序执行时创建一个临时文件或目录,并希望使用完之后可以自动销毁掉。
tempfile 模块中有很多的函数可以完成这任务。 为了创建一个匿名的临时文件,可以使用 tempfile.TemporaryFile :
from tempfile import TemporaryFile
with TemporaryFile('w+t') as f:
# Read/write to the file
f.write('Hello World\n')
f.write('Testing\n')
# Seek back to beginning and read the data
f.seek(0)
data = f.read()
# Temporary file is destroyed
为了创建一个临时目录,可以使用 tempfile.TemporaryDirectory() 。
在大多数Unix系统上,通过 TemporaryFile() 创建的文件都是匿名的,甚至连目录都没有。 如果你想打破这个限制,可以使用 NamedTemporaryFile() 来代替。
5.20 与串行端口的数据通信
你想通过串行端口读写数据,典型场景就是和一些硬件设备打交道(比如一个机器人或传感器)。
使用第三方包如 pySerial 的一个原因是它提供了对高级特性的支持 (比如超时,控制流,缓冲区刷新,握手协议等等)。举个例子,如果你想启用 RTS-CTS 握手协议, 你只需要给 Serial() 传递一个 rtscts=True 的参数即可。 其官方文档非常完善,因此我在这里极力推荐这个包。
5.21 序列化Python对象
你需要将一个Python对象序列化为一个字节流,以便将它保存到一个文件、存储到数据库或者通过网络传输它。
对于序列化最普遍的做法就是使用 pickle 模块。为了将一个对象保存到一个文件中,可以这样做:
import pickle
data = ... # Some Python object
f = open('somefile', 'wb')
pickle.dump(data, f)
为了将一个对象转储为一个字符串,可以使用 pickle.dumps() :
s = pickle.dumps(data)
第七章
7.1 可接受任意数量参数的函数
你想构造一个可接受任意数量参数的函数。
为了能让一个函数接受任意数量的位置参数,可以使用一个参数。
为了接受任意数量的关键字参数,使用一个以开头的参数。
如果你还希望某个函数能同时接受任意数量的位置参数和关键字参数,可以同时使用和。
一个参数只能出现在函数定义中最后一个位置参数后面,而参数只能出现在最后一个参数。 有一点要注意的是,在参数后面仍然可以定义其他参数。
7.2 只接受关键字参数的函数
你希望函数的某些参数强制使用关键字参数传递
将强制关键字参数放到某个参数或者单个后面就能达到这种效果。比如:
def recv(maxsize, *, block):
'Receives a message'
pass
recv(1024, True) # TypeError
recv(1024, block=True) # Ok
7.3 给函数参数增加元信息
你写好了一个函数,然后想为这个函数的参数增加一些额外的信息,这样的话其他使用者就能清楚的知道这个函数应该怎么使用。
使用函数参数注解是一个很好的办法,它能提示程序员应该怎样正确使用这个函数。 例如,下面有一个被注解了的函数:
def add(x:int, y:int) -> int:
return x + y
python解释器不会对这些注解添加任何的语义。它们不会被类型检查,运行时跟没有加注解之前的效果也没有任何差距。 然而,对于那些阅读源码的人来讲就很有帮助啦。第三方工具和框架可能会对这些注解添加语义。同时它们也会出现在文档中。
7.4 返回多个值的函数
你希望构造一个可以返回多个值的函数
为了能返回多个值,函数直接return一个元组就行了。例如:
>>> def myfun():
... return 1, 2, 3
...
>>> a, b, c = myfun()
>>> a
1
>>> b
2
>>> c
3
7.5 定义有默认参数的函数
你想定义一个函数或者方法,它的一个或多个参数是可选的并且有一个默认值。
定义一个有可选参数的函数是非常简单的,直接在函数定义中给参数指定一个默认值,并放到参数列表最后就行了。例如:
def spam(a, b=42):
print(a, b)
spam(1) # Ok. a=1, b=42
spam(1, 2) # Ok. a=1, b=2
如果默认参数是一个可修改的容器比如一个列表、集合或者字典,可以使用None作为默认值
首先,默认参数的值仅仅在函数定义的时候赋值一次。
其次,默认参数的值应该是不可变的对象,比如None、True、False、数字或字符串。
7.6 定义匿名或内联函数
你想为 sort() 操作创建一个很短的回调函数,但又不想用 def 去写一个单行函数, 而是希望通过某个快捷方式以内联方式来创建这个函数。
当一些函数很简单,仅仅只是计算一个表达式的值的时候,就可以使用lambda表达式来代替了。
尽管lambda表达式允许你定义简单函数,但是它的使用是有限制的。 你只能指定单个表达式,它的值就是最后的返回值。也就是说不能包含其他的语言特性了, 包括多个语句、条件表达式、迭代以及异常处理等等。
你可以不使用lambda表达式就能编写大部分python代码。 但是,当有人编写大量计算表达式值的短小函数或者需要用户提供回调函数的程序的时候, 你就会看到lambda表达式的身影了。
7.7 匿名函数捕获变量值
你用lambda定义了一个匿名函数,并想在定义时捕获到某些变量的值。
这其中的奥妙在于lambda表达式中的x是一个自由变量, 在运行时绑定值,而不是定义时就绑定,这跟函数的默认值参数定义是不同的。 因此,在调用这个lambda表达式的时候,x的值是执行时的值。
在这里列出来的问题是新手很容易犯的错误,有些新手可能会不恰当的使用lambda表达式。 比如,通过在一个循环或列表推导中创建一个lambda表达式列表,并期望函数能在定义时就记住每次的迭代值。
7.8 减少可调用对象的参数个数
你有一个被其他python代码使用的callable对象,可能是一个回调函数或者是一个处理器, 但是它的参数太多了,导致调用时出错。
如果需要减少某个函数的参数个数,你可以使用 functools.partial() 。 partial() 函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。 为了演示清楚,假设你有下面这样的函数:
def spam(a, b, c, d):
print(a, b, c, d)
现在我们使用 partial() 函数来固定某些参数值:
>>> from functools import partial
>>> s1 = partial(spam, 1) # a = 1
>>> s1(2, 3, 4)
1 2 3 4
>>> s1(4, 5, 6)
1 4 5 6
>>> s2 = partial(spam, d=42) # d = 42
>>> s2(1, 2, 3)
1 2 3 42
>>> s2(4, 5, 5)
4 5 5 42
>>> s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42
>>> s3(3)
1 2 3 42
>>> s3(4)
1 2 4 42
>>> s3(5)
1 2 5 42
>>>
可以看出 partial() 固定某些参数并返回一个新的callable对象。这个新的callable接受未赋值的参数, 然后跟之前已经赋值过的参数合并起来,最后将所有参数传递给原始函数。
7.9 将单方法的类转换为函数
你有一个除 init() 方法外只定义了一个方法的类。为了简化代码,你想将它转换成一个函数。
大多数情况下,可以使用闭包来将单个方法的类转换成函数。 举个例子,下面示例中的类允许使用者根据某个模板方案来获取到URL链接地址。
from urllib.request import urlopen
class UrlTemplate:
def __init__(self, template):
self.template = template
def open(self, **kwargs):
return urlopen(self.template.format_map(kwargs))
# Example use. Download stock data from yahoo
yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
for line in yahoo.open(names='IBM,AAPL,FB', fields='sl1c1v'):
print(line.decode('utf-8'))
7.10 带额外状态信息的回调函数
你的代码中需要依赖到回调函数的使用(比如事件处理器、等待后台任务完成后的回调等), 并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。
基于回调函数的软件通常都有可能变得非常复杂。一部分原因是回调函数通常会跟请求执行代码断开。 因此,请求执行和处理结果之间的执行环境实际上已经丢失了。如果你想让回调函数连续执行多步操作, 那你就必须去解决如何保存和恢复相关的状态信息了。
至少有两种主要方式来捕获和保存状态信息,你可以在一个对象实例(通过一个绑定方法)或者在一个闭包中保存它。 两种方式相比,闭包或许是更加轻量级和自然一点,因为它们可以很简单的通过函数来构造。 它们还能自动捕获所有被使用到的变量。因此,你无需去担心如何去存储额外的状态信息(代码中自动判定)。
7.11 内联回调函数
当你编写使用回调函数的代码的时候,担心很多小函数的扩张可能会弄乱程序控制流。 你希望找到某个方法来让代码看上去更像是一个普通的执行序列。