python编程进阶(13):兼容、缓存、上下文

兼容Python2+和Python3+

很多时候你可能希望你开发的程序能够同时兼容Python2+和Python3+。

试想你有一个非常出名的Python模块被很多开发者使用着,但并不是所有人都只使用Python2或者Python3。这时候你有两个办法。第一个办法是开发两个模块,针对Python2一个,针对Python3一个。还有一个办法就是调整你现在的代码使其同时兼容Python2和Python3。

本节中,我将介绍一些技巧,让你的脚本同时兼容Python2和Python3。

Future模块导入

第一种也是最重要的方法,就是导入__future__模块。它可以帮你在Python2中导入Python3的功能。这有一组例子:

上下文管理器是Python2.6+引入的新特性,如果你想在Python2.5中使用它可以这样做:

1
from __future__ import with_statement

在Python3中print已经变为一个函数。如果你想在Python2中使用它可以通过__future__导入:

1
2
3
4
5
6
7
print
# Output:

from __future__ import print_function
print(print)
# Output: <built-in function print>

模块重命名

首先,告诉我你是如何在你的脚本中导入模块的。大多时候我们会这样做:

1
2
3
import foo 
# or
from foo import bar

你知道么,其实你也可以这样做:

1
import foo as foo

这样做可以起到和上面代码同样的功能,但最重要的是它能让你的脚本同时兼容Python2和Python3。现在我们来看下面的代码:

1
2
3
4
try:
import urllib.request as urllib_request # for Python 3
except ImportError:
import urllib2 as urllib_request # for Python 2

过期的Python2内置功能

另一个需要了解的事情就是Python2中有12个内置功能在Python3中已经被移除了。要确保在Python2代码中不要出现这些功能来保证对Python3的兼容。这有一个强制让你放弃12内置功能的方法:

1
from future.builtins.disabled import *

现在,只要你尝试在Python3中使用这些被遗弃的模块时,就会抛出一个NameError异常如下:

1
2
3
4
from future.builtins.disabled import *

apply()
# Output: NameError: obsolete Python 2 builtin apply is disabled

标准库向下兼容的外部支持

有一些包在非官方的支持下为Python2提供了Python3的功能。例如,我们有:

  • enum pip install enum34
  • singledispatch pip install singledispatch
  • pathlib pip install pathlib

想更多了解,在Python文档中有一个全面的指南可以帮助你让你的代码同时兼容Python2和Python3。

函数缓存 (Function caching)

函数缓存允许我们将一个函数对于给定参数的返回值缓存起来。当一个I/O密集的函数被频繁使用相同的参数调用的时候,函数缓存可以节约时间。在Python 3.2版本以前我们只有写一个自定义的实现。在Python 3.2以后版本,有个lru_cache的装饰器,允许我们将一个函数的返回值快速地缓存或取消缓存

Python 3.2及以后版本

我们来实现一个斐波那契计算器,并使用lru_cache

1
2
3
4
5
6
7
8
9
10
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]

那个maxsize参数是告诉lru_cache,最多缓存最近多少个返回值。

我们也可以轻松地对返回值清空缓存,通过这样:

1
fib.cache_clear()

上下文管理器(Context managers)

上下文管理器允许你在有需要的时候,精确地分配和释放资源。上下文管理器的常用于一些资源的操作,需要在资源的正确获取与释放相关的操作 ,先看一个例子,我们经常会用到 try … catch … finally 语句确保一些系统资源得以正确释放。如:

1
2
3
4
5
6
7
8
try:
f = open('somefile')
for line in f:
print(line)
except Exception as e:
print(e)
finally:
f.close()

我们经常用到上面的代码模式,用复用代码的模式来讲,并不够好。于是 with 语句出现了,通过定义一个上下文管理器来封装这个代码块:

1
2
3
with open('somefile') as f:
for line in f:
print(line)

使用上下文管理器最广泛的案例就是with语句了。想象下你有两个需要结对执行的相关操作,然后还要在它们中间放置一段代码。 上下文管理器就是专门让你做这种事情的。上面这段代码打开了一个文件,往里面写入了一些数据,然后关闭该文件。如果在往文件写数据时发生异常,它也会尝试去关闭文件。这就是with语句的主要优势,它确保我们的文件会被关闭,而不用关注嵌套代码如何退出。

上下文管理器的一个常见用例,是资源的加锁和解锁,以及关闭已打开的文件(就像我已经展示给你看的)。

实际上,我们可以同时处理多个上下文管理器:

1
2
with A() as a, B() as b:
suite

上下文管理协议

与迭代器类似,实现了迭代协议的函数/对象即为迭代器。实现了上下文协议的函数/对象即为上下文管理器。迭代器协议是实现了__iter__方法。上下文管理协议则是一个类实现__enter__ (self)和__exit__(self, exc_type, exc_valye, traceback)方法就可以了。实行如下结构:

1
2
3
4
5
6
7
8
9
10
11
12
class Contextor:
# __enter__返回一个对象,通常是当前类的实例,也可以是其他对象。
def __enter__(self):
pass

def __exit__(self, exc_type, exc_val, exc_tb):
pass

contextor = Contextor()

with contextor [as var]:
with_body

Contextor 实现了__enter____exit__这两个上下文管理器协议,当Contextor调用/实例化的时候,则创建了上下文管理器contextor

通过定义__enter____exit__方法的类(包括自己定义的类,只要加上特定的两个方法即可),我们可以在with语句里使用它。我们来看看在底层都发生了什么。

执行步骤:

  1. 执行 contextor (实例化具有上下文协议的对象,这里也称为上下文表达式)以获取上下文管理器,上下文表达式就是 with 和 as 之间的代码。
  2. 加载上下文管理器对象的 exit()方法,备用。
  3. 调用上下文管理器的 enter() 方法
  4. 如果有 as var 从句,则将 enter() 方法的返回值赋给 var
  5. 执行子代码块 with_body
  6. with语句调用上下文管理器之前暂存的 exit() 方法,如果 with_body 的退出是由异常引发的,那么该异常的 type、value 和 traceback 会作为参数传给 exit(),否则传三个 None。然后,exit()需要明确地返回 True 或 False。当返回 True 时,异常不会被向上抛出,当返回 False 时曾会向上抛出。
  7. 如果 with_body 的退出由异常引发,它让exit()方法来处理异常,并且 exit() 的返回值等于 False,那么这个异常将被with语句重新引发抛出一次;如果 exit() 的返回值等于 True,那么这个异常就被无视掉,继续执行后面的代码。

上下文管理器工具

通过实现上下文协议定义创建上下文管理器很方便,Python为了更优雅,还专门提供了一个模块用于实现更函数式的上下文管理器用法。Python的contextlib模块专门用于这个目的。

AbstractContextManager : 此类在 Python3.6中新增,提供了默认的enter()和exit()实现。enter()返回自身,exit()返回 None。

contextmanager: 我们要实现上下文管理器,总是要写一个类。此函数则容许我们通过一个装饰一个生成器函数得到一个上下文管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import contextlib

@contextlib.contextmanager
def database():
db = Database()
try:
if not db.connected:
db.connect()
yield db # 生成器
except Exception as e:
db.close()

def handle_query():
with database() as db:
print 'handle ---', db.query()

使用contextlib 定义一个上下文管理器函数,通过with语句,database调用生成一个上下文管理器,然后调用函数隐式的__enter__方法,并将结果通yield返回。最后退出上下文环境的时候,在exception代码块中执行了__exit__方法。当然我们可以手动模拟上述代码的执行的细节。注意:yield 只能返回一次,返回的对象 被绑定到 as 后的变量,不需要返回时可以直接 yield,不带返回值。退出时则从 yield 之后执行。由于contextmanager继承自ContextDecorator,所以被contextmanager装饰过的生成器也可以用作装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [1]: context = database()    # 创建上下文管理器

In [2]: context
<contextlib.GeneratorContextManager object at 0x107188f10>

In [3]: db = context.__enter__() # 进入with语句

In [4]: db # as语句,返回 Database实例
Out[4]: <__main__.Database at 0x107188a10>

In [5]: db.query()
Out[5]: 'query data'

In [6]: db.connected
Out[6]: True

In [7]: db.__exit__(None, None, None) # 退出with语句

In [8]: db
Out[8]: <__main__.Database at 0x107188a10>

In [9]: db.connected
Out[9]: False

ContextDecorator: 我们可以实现一个上下文管理器,同时可以用作装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AContext(ContextDecorator):

def __enter__(self):
print('Starting')
return self

def __exit__(self, exc_type, exc_value, traceback):
print('Finishing')
return False

# 在 with 中使用
with AContext():
print('祖国伟大')

# 用作装饰器
@AContext()
def print_sth(sth):
print(sth)

print_sth('祖国伟大')

#在这两种写法中,有没有发现,第二种写法更好,因为我们减少了一次代码缩进,可读性更强

还有一种好处:当我们已经实现了某个上下文管理器时,只要增加一个继承类,该上下文管理器立刻编程装饰器。

1
2
3
4
5
6
7
from contextlib import ContextDecorator
class mycontext(ContextBaseClass, ContextDecorator):
def __enter__(self):
return self

def __exit__(self, *exc):
return False
-------------Thanks for Reading!-------------