Python 进阶 —— 第五节
# open
函数
open (opens new window) 函数可以打开一个文件。超级简单吧?大多数时候,我们看到它这样被使用:
f = open('photo.jpg', 'r+')
jpgdata = f.read()
f.close()
2
3
我现在写这篇文章的原因,是大部分时间我看到open
被这样使用。有三个错误存在于上面的代码中。你能把它们全指出来吗?如不能,请读下去。在这篇文章的结尾,你会知道上面的代码错在哪里,而且,更重要的是,你能在自己的代码里避免这些错误。现在我们从基础开始:
open
的返回值是一个文件句柄,从操作系统托付给你的Python程序。一旦你处理完文件,你会想要归还这个文件句柄,只有这样你的程序不会超出一次能打开的文件句柄的数量上限。
显式地调用close
关闭了这个文件句柄,但前提是只有在read成功的情况下。如果有任意异常正好在f = open(...)
之后产生,f.close()
将不会被调用(取决于Python解释器的做法,文件句柄可能还是会被归还,但那是另外的话题了)。为了确保不管异常是否触发,文件都能关闭,我们将其包裹成一个with
语句:
with open('photo.jpg', 'r+') as f:
jpgdata = f.read()
2
open
的第一个参数是文件名。第二个(mode
打开模式)决定了这个文件如何被打开。
- 如果你想读取文件,传入
r
- 如果你想读取并写入文件,传入
r+
- 如果你想覆盖写入文件,传入
w
- 如果你想在文件末尾附加内容,传入
a
虽然有若干个其他的有效的mode
字符串,但有可能你将永远不会使用它们。mode
很重要,不仅因为它改变了行为,而且它可能导致权限错误。举个例子,我们要是在一个写保护的目录里打开一个jpg文件, open(.., 'r+')
就失败了。mode
可能包含一个扩展字符;让我们还可以以二进制方式打开文件(你将得到字节串)或者文本模式(字符串)
一般来说,如果文件格式是由人写的,那么它更可能是文本模式。jpg图像文件一般不是人写的(而且其实不是人直接可读的),因此你应该以二进制模式来打开它们,方法是在mode
字符串后加一个b
(你可以看看开头的例子里,正确的方式应该是rb
)。
如果你以文本模式打开一些东西(比如,加一个t
,或者就用r/r+/w/a
),你还必须知道要使用哪种编码。对于计算机来说,所有的文件都是字节,而不是字符。
可惜,在Pyhon 2.x版本里,open
不支持显示地指定编码。然而,io.open (opens new window)函数在Python 2.x中和3.x(其中它是open
的别名)中都有提供,它能做正确的事。你可以传入encoding
这个关键字参数来传入编码。
如果你不传入任意编码,一个系统 - 以及Python -指定的默认选项将被选中。你也许被诱惑去依赖这个默认选项,但这个默认选项经常是错误的,或者默认编码实际上不能表达文件里的所有字符(这将经常发生在Python 2.x和/或Windows)。
所以去挑选一个编码吧。utf-8
是一个非常好的编码。当你写入一个文件,你可以选一个你喜欢的编码(或者最终读你文件的程序所喜欢的编码)。
那你怎么找出正在读的文件是用哪种编码写的呢?好吧,不幸的是,并没有一个十分简单的方式来检测编码。在不同的编码中,同样的字节可以表示不同,但同样有效的字符。因此,你必须依赖一个元数据(比如,在HTTP头信息里)来找出编码。越来越多的是,文件格式将编码定义成UTF-8
。
有了这些基础知识,我们来写一个程序,读取一个文件,检测它是否是JPG(提示:这些文件头部以字节FF D8
开始),把对输入文件的描述写入一个文本文件。
import io
with open('photo.jpg', 'rb') as inf:
jpgdata = inf.read()
if jpgdata.startswith(b'\xff\xd8'):
text = u'This is a JPEG file (%d bytes long)\n'
else:
text = u'This is a random file (%d bytes long)\n'
with io.open('summary.txt', 'w', encoding='utf-8') as outf:
outf.write(text % len(jpgdata))
2
3
4
5
6
7
8
9
10
11
12
我敢肯定,现在你会正确地使用open
啦!
# 目标Python2+3
很多时候你可能希望你开发的程序能够同时兼容Python2+和Python3+。
试想你有一个非常出名的Python模块被很多开发者使用着,但并不是所有人都只使用Python2或者Python3。这时候你有两个办法。第一个办法是开发两个模块,针对Python2一个,针对Python3一个。还有一个办法就是调整你现在的代码使其同时兼容Python2和Python3。
本节中,我将介绍一些技巧,让你的脚本同时兼容Python2和Python3。
Future模块导入
第一种也是最重要的方法,就是导入__future__
模块。它可以帮你在Python2中导入Python3的功能。这有一组例子:
上下文管理器是Python2.6+引入的新特性,如果你想在Python2.5中使用它可以这样做:
from __future__ import with_statement
在Python3中print
已经变为一个函数。如果你想在Python2中使用它可以通过__future__
导入:
print
# Output:
from __future__ import print_function
print(print)
# Output: <built-in function print>
2
3
4
5
6
模块重命名
首先,告诉我你是如何在你的脚本中导入模块的。大多时候我们会这样做:
import foo
# or
from foo import bar
2
3
你知道么,其实你也可以这样做:
import foo as foo
这样做可以起到和上面代码同样的功能,但最重要的是它能让你的脚本同时兼容Python2和Python3。现在我们来看下面的代码:
try:
import urllib.request as urllib_request # for Python 3
except ImportError:
import urllib2 as urllib_request # for Python 2
2
3
4
让我来稍微解释一下上面的代码。
我们将模块导入代码包装在try/except
语句中。我们是这样做是因为在Python 2中并没有urllib.request
模块。这将引起一个ImportError
异常。而在Python2中urllib.request
的功能则是由urllib2
提供的。所以,当我们试图在Python2中导入urllib.request
模块的时候,一旦我们捕获到ImportError
我们将通过导入urllib2
模块来代替它。
最后,你要了解as
关键字的作用。它将导入的模块映射到urllib.request
,所以我们通过urllib_request
这个别名就可以使用urllib2
中的所有类和方法了。
过期的Python2内置功能
另一个需要了解的事情就是Python2中有12个内置功能在Python3中已经被移除了。要确保在Python2代码中不要出现这些功能来保证对Python3的兼容。这有一个强制让你放弃12内置功能的方法:
from future.builtins.disabled import *
现在,只要你尝试在Python3中使用这些被遗弃的模块时,就会抛出一个NameError
异常如下:
from future.builtins.disabled import *
apply()
# Output: NameError: obsolete Python 2 builtin apply is disabled
2
3
4
标准库向下兼容的外部支持
有一些包在非官方的支持下为Python2提供了Python3的功能。例如,我们有:
- enum
pip install enum34
- singledispatch
pip install singledispatch
- pathlib
pip install pathlib
想更多了解,在Python文档中有一个全面的指南 (opens new window)可以帮助你让你的代码同时兼容Python2和Python3。
# 协程
Python中的协程和生成器很相似但又稍有不同。主要区别在于: *生成器是数据的生产者 *协程则是数据的消费者
首先我们先来回顾下生成器的创建过程。我们可以这样去创建一个生成器:
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a+b
2
3
4
5
然后我们经常在for
循环中这样使用它:
for i in fib():
print i
2
这样做不仅快而且不会给内存带来压力,因为我们所需要的值都是动态生成的而不是将他们存储在一个列表中。更概括的说如果现在我们在上面的例子中使用yield
便可获得了一个协程。协程会消费掉发送给它的值。Python实现的grep
就是个很好的例子:
def grep(pattern):
print("Searching for", pattern)
while True:
line = (yield)
if pattern in line:
print(line)
2
3
4
5
6
等等!yield
返回了什么?啊哈,我们已经把它变成了一个协程。它将不再包含任何初始值,相反要从外部传值给它。我们可以通过send()
方法向它传值。这有个例子:
search = grep('coroutine')
next(search)
#output: Searching for coroutine
search.send("I love you")
search.send("Don't you love me?")
search.send("I love coroutine instead!")
#output: I love coroutine instead!
2
3
4
5
6
7
发送的值会被yield
接收。我们为什么要运行next()
方法呢?这样做正是为了启动一个协程。就像协程中包含的生成器并不是立刻执行,而是通过next()
方法来响应send()
方法。因此,你必须通过next()
方法来执行yield
表达式。
我们可以通过调用close()
方法来关闭一个协程。像这样:
search = grep('coroutine')
search.close()
2
更多协程相关知识的学习大家可以参考David Beazley的这份精彩演讲 (opens new window)。
# 函数缓存 (Function caching)
函数缓存允许我们将一个函数对于给定参数的返回值缓存起来。
当一个I/O密集的函数被频繁使用相同的参数调用的时候,函数缓存可以节约时间。
在Python 3.2版本以前我们只有写一个自定义的实现。在Python 3.2以后版本,有个lru_cache
的装饰器,允许我们将一个函数的返回值快速地缓存或取消缓存。
我们来看看,Python 3.2前后的版本分别如何使用它。
# Python 3.2及以后版本
我们来实现一个斐波那契计算器,并使用lru_cache
。
from functools import lru_cache
@lru_cache(maxsize=32)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
>>> print([fib(n) for n in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
2
3
4
5
6
7
8
9
10
那个maxsize
参数是告诉lru_cache
,最多缓存最近多少个返回值。
我们也可以轻松地对返回值清空缓存,通过这样:
fib.cache_clear()
# Python 2系列版本
你可以创建任意种类的缓存机制,有若干种方式来达到相同的效果,这完全取决于你的需要。 这里是一个一般的缓存:
from functools import wraps
def memoize(function):
memo = {}
@wraps(function)
def wrapper(*args):
if args in memo:
return memo[args]
else:
rv = function(*args)
memo[args] = rv
return rv
return wrapper
@memoize
def fibonacci(n):
if n < 2: return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(25)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这里有一篇Caktus Group的不错的文章 (opens new window),在其中他们发现一个Django框架的由lru_cache导致的bug。读起来很有意思。一定要打开去看一下。
# 上下文管理器(Context managers)
上下文管理器允许你在有需要的时候,精确地分配和释放资源。
使用上下文管理器最广泛的案例就是with
语句了。
想象下你有两个需要结对执行的相关操作,然后还要在它们中间放置一段代码。
上下文管理器就是专门让你做这种事情的。举个例子:
with open('some_file', 'w') as opened_file:
opened_file.write('Hola!')
2
上面这段代码打开了一个文件,往里面写入了一些数据,然后关闭该文件。如果在往文件写数据时发生异常,它也会尝试去关闭文件。上面那段代码与这一段是等价的:
file = open('some_file', 'w')
try:
file.write('Hola!')
finally:
file.close()
2
3
4
5
当与第一个例子对比时,我们可以看到,通过使用with
,许多样板代码(boilerplate code)被消掉了。 这就是with
语句的主要优势,它确保我们的文件会被关闭,而不用关注嵌套代码如何退出。
上下文管理器的一个常见用例,是资源的加锁和解锁,以及关闭已打开的文件(就像我已经展示给你看的)。
让我们看看如何来实现我们自己的上下文管理器。这会让我们更完全地理解在这些场景背后都发生着什么。
# 基于类的实现
一个上下文管理器的类,最起码要定义__enter__
和__exit__
方法。
让我们来构造我们自己的开启文件的上下文管理器,并学习下基础知识。
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()
2
3
4
5
6
7
通过定义__enter__
和__exit__
方法,我们可以在with
语句里使用它。我们来试试:
with File('demo.txt', 'w') as opened_file:
opened_file.write('Hola!')
2
我们的__exit__
函数接受三个参数。这些参数对于每个上下文管理器类中的__exit__
方法都是必须的。我们来谈谈在底层都发生了什么。
with
语句先暂存了File
类的__exit__
方法- 然后它调用
File
类的__enter__
方法 __enter__
方法打开文件并返回给with
语句- 打开的文件句柄被传递给
opened_file
参数 - 我们使用
.write()
来写文件 with
语句调用之前暂存的__exit__
方法__exit__
方法关闭了文件
# 处理异常
我们还没有谈到__exit__
方法的这三个参数:type
, value
和traceback
。
在第4步和第6步之间,如果发生异常,Python会将异常的type
,value
和traceback
传递给__exit__
方法。
它让__exit__
方法来决定如何关闭文件以及是否需要其他步骤。在我们的案例中,我们并没有注意它们。
那如果我们的文件对象抛出一个异常呢?万一我们尝试访问文件对象的一个不支持的方法。举个例子:
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function('Hola!')
2
我们来列一下,当异常发生时,with
语句会采取哪些步骤。 1. 它把异常的type
,value
和traceback
传递给__exit__
方法 2. 它让__exit__
方法来处理异常 3. 如果__exit__
返回的是True,那么这个异常就被优雅地处理了。 4. 如果__exit__
返回的是True以外的任何东西,那么这个异常将被with
语句抛出。
在我们的案例中,__exit__
方法返回的是None
(如果没有return
语句那么方法会返回None
)。因此,with
语句抛出了那个异常。
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'file' object has no attribute 'undefined_function'
2
3
我们尝试下在__exit__
方法中处理异常:
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
print("Exception has been handled")
self.file_obj.close()
return True
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function()
# Output: Exception has been handled
2
3
4
5
6
7
8
9
10
11
12
13
14
我们的__exit__
方法返回了True
,因此没有异常会被with
语句抛出。
这还不是实现上下文管理器的唯一方式。还有一种方式,我们会在下一节中一起看看。
# 基于生成器的实现
我们还可以用装饰器(decorators)和生成器(generators)来实现上下文管理器。
Python有个contextlib
模块专门用于这个目的。我们可以使用一个生成器函数来实现一个上下文管理器,而不是使用一个类。
让我们看看一个基本的,没用的例子:
from contextlib import contextmanager
@contextmanager
def open_file(name):
f = open(name, 'w')
yield f
f.close()
2
3
4
5
6
7
OK啦!这个实现方式看起来更加直观和简单。然而,这个方法需要关于生成器、yield
和装饰器的一些知识。在这个例子中我们还没有捕捉可能产生的任何异常。它的工作方式和之前的方法大致相同。
让我们小小地剖析下这个方法。 1. Python解释器遇到了yield
关键字。因为这个缘故它创建了一个生成器而不是一个普通的函数。 2. 因为这个装饰器,contextmanager
会被调用并传入函数名(open_file
)作为参数。 3. contextmanager
函数返回一个以GeneratorContextManager
对象封装过的生成器。 4. 这个GeneratorContextManager
被赋值给open_file
函数,我们实际上是在调用GeneratorContextManager
对象。
那现在我们既然知道了所有这些,我们可以用这个新生成的上下文管理器了,像这样:
with open_file('some_file') as f:
f.write('hola!')
2