Python 标准库进阶,进阶标准,一. 上下文管理1.


一. 上下文管理

1. 传统的类方式

Java 使用 try 来自动管理资源,只要实现了 AutoCloseable 接口,就可以部分摆脱手动 colse 的地狱了。
而 Python,则是定义了两个 Protocol:enterexit. 下面是一个 open 的模拟实现:

class OpenContext(object):    def __init__(self, filename, mode):  # 调用 open(filename, mode) 返回一个实例        self.fp = open(filename, mode)    def __enter__(self):  # 用 with 管理 __init__ 返回的实例时,with 会自动调用这个方法        return self.fp    # 退出 with 代码块时,会自动调用这个方法。    def __exit__(self, exc_type, exc_value, traceback):        self.fp.close()# 这里先构造了 OpenContext 实例,然后用 with 管理该实例with OpenContext('/tmp/a', 'a') as f:    f.write('hello world')

这里唯一有点复杂的,就是 __exit__ 方法。和 Java 一样,__exit__ 相当于 try - catch - finally 的 finally 代码块,在发生异常时,它也会被调用。

当没有异常发生时,__exit__ 的三个参数 exc_type, exc_value, traceback 都为 None,而当发生异常时,它们就对应异常的详细信息。
发生异常时,** __exit__ 的返回值将被用于决定是否向外层抛出该异常**,返回 True 则抛出,返回 False 则抑制(swallow it)。

Note 1:Python 3.6 提供了 async with 异步上下文管理器,它的 Protocol 和同步的 with 完全类似,是 __aenter__ 和 __aexit__ 两个方法。
Note 2:与 Java 相同,with 支持同时管理多个资源,因此可以直接写 with open(x) as a, open(y) as b: 这样的形式。

2. contextlib

2.1 @contextlib.contextmanager

对于简单的 with 资源管理,编写一个类可能会显得比较繁琐,为此 contextlib 提供了一个方便的装饰器 @contextlib.contextmanager 用来简化代码。

使用它,上面的 OpenContext 可以改写成这样:

from contextlib import contextmanager@contextmanagerdef make_open_context(filename, mode):    fp = open(filename, mode)    try:        yield fp  # 没错,这是一个生成器函数    finally:        fp.close()with make_open_context('/tmp/a', 'a') as f:    f.write('hello world')

使用 contextmanager 装饰一个生成器函数,yield 之前的代码对应 __enter__,finally 代码块就对应 __exit__.

Note:同样,也有异步版本的装饰器 @contextlib.asynccontextmanager

2.2 contextlib.closing(thing)

用于将原本不支持 with 管理的资源,包装成一个 Context 对象。

from contextlib import closingfrom urllib.request import urlopenwith closing(urlopen('http://www.python.org')) as page:    for line in page:        print(line)# closing 等同于from contextlib import contextmanager@contextmanagerdef closing(thing):    try:        yield thing    finally:        thing.close()  # 就是添加了一个自动 close 的功能

2.3 contextlib.suppress(*exceptions)

使 with 管理器抑制代码块内任何被指定的异常:

from contextlib import suppresswith suppress(FileNotFoundError):    os.remove('somefile.tmp')# 等同于try:    os.remove('somefile.tmp')except FileNotFoundError:    pass

2.4 contextlib.redirect_stdout(new_target)

将 with 代码块内的 stdout 重定向到指定的 target(可用于收集 stdout 的输出)

f = io.StringIO()with redirect_stdout(f):  # 将输出直接写入到 StringIO    help(pow)s = f.getvalue()# 或者直接写入到文件with open('help.txt', 'w') as f:    with redirect_stdout(f):        help(pow)

redirect_stdout 函数返回的 Context 是可重入的( reentrant),可以重复使用。

二、pathlib

提供了 OS 无关的文件路径抽象,可以完全替代 os.path 和 glob.

基本上,pathlib.Path 就是你需要了解的所有内容。

1. 路径解析与拼接

from pathlib import Pathdata_folder = Path("./source_data/text_files/")data_file = data_folder / "raw_data.txt"  # Path 重载了 / 操作符,路径拼接超级方便# 路径的解析data_file.parent  # 获取父路径,这里的结果就是 data_folderdata_foler.parent # 会返回 Path("source_data")data_file.parents[1] # 即获取到 data_file 的上上层目录,结果和上面一样是 Path("source_data")data_file.parents[2] # 上上上层目录,Path(".")dara_file.name # 文件名 "raw_data.txt"dara_file.suffix  # 文件的后缀(最末尾的)".txt",还可用 suffixes 获取所有后缀data_file.stem  # 去除掉最末尾的后缀后(只去除一个),剩下的文件名:raw_data# 替换文件名或者文件后缀data_file.with_name("test.txt")  # 变成 .../test.txtdata_file.with_suffix(".pdf")  # 变成 .../raw_data.pdf# 当前路径与另一路径 的相对路径data_file.relative_to(data_folder)  # PosixPath('raw_data.txt')

2. 常用的路径操作函数

if not data_folder.exist():    data_folder.mkdir(parents=True)  # 直接创建文件夹,如果父文件夹不存在,也自动创建if not filename.exists():  # 文件是否存在    filename.touch()  # 直接创建空文件,或者用 filename.open() 直接获取文件句柄# 路径类型判断if data_file.is_file():  # 是文件    print(data_file, "is a file")elif data_file.is_dir():  # 是文件夹    for child in p.iterdir():  # 通过 Path.iterdir() 迭代文件夹中的内容        print(child)# 路径解析filename.resolve()  # 获取文件的绝对路径(符号链接也会被解析到真正的文件)# 可以直接获取 Home 路径或者当前路径Path.home() / "file.txt" # 有时需要以 home 为 base path 来构建文件路径Path.cwd()  / "file.txt" # 或者基于当前路径构建

还有很多其它的实用函数,可在使用中慢慢探索。

3. glob

pathlib 也提供了 glob 支持,也就是广泛用在路径匹配上的一种简化正则表达式。

data_file.match(glob_pattern)  # 返回 True 或 False,表示文件路径与给出的 glob pattern 是否匹配for py_file in data_folder.glob("*/*.py"):  # 匹配当前路径下的子文件夹中的 py 文件,会返回一个可迭代对象    print(py_file)# 反向匹配,相当于 glob 模式开头添加 "**/"for py_file in data_folder.glob("*/*.py"):  # 匹配当前路径下的所有 py 文件(所有子文件夹也会被搜索),返回一个可迭代对象    print(py_file)

glob 中的 * 表示任意字符,而 ** 则表示任意层目录。(在大型文件树上使用 ** 速度会很慢!)

Python 标准库进阶

评论关闭