Python 工程习惯
以下以 BPE 预分词代码为例,整理几项常用的Python工程习惯。
1. 类型标注(Type Annotations)
类型标注可以描述函数接收的参数类型和返回值类型,供编辑器以及 mypy、Pyright 等类型检查工具使用。
例如,原函数:
def pre_tokenize(corpus):
...
可标注为:
def pre_tokenize(corpus: str) -> dict[tuple[bytes, ...], int]:
...
为复杂的空容器标注类型
空容器本身没有足够的信息供类型检查工具推断元素类型,因此复杂容器通常应显式标注:
pretoken_freq: dict[tuple[bytes, ...], int] = {}
items: list[str] = []
seen: set[bytes] = set()
pair_freq: dict[tuple[bytes, bytes], int] = {}
如果右侧内容已经能清楚体现类型,通常不必重复标注。
2. 类型别名(Type Aliases)
当一个复杂类型反复出现,可在模块顶部定义类型别名,减少重复并提高可读性:
from typing import TypeAlias
Pretoken: TypeAlias = tuple[bytes, ...]
Pair: TypeAlias = tuple[bytes, bytes]
PretokenFreq: TypeAlias = dict[Pretoken, int]
PairFreq: TypeAlias = dict[Pair, int]
这样,原来的函数签名:
def find_max_pair(
pair_freq: dict[tuple[bytes, bytes], int],
) -> tuple[bytes, bytes]:
...
可以简化为:
def find_max_pair(pair_freq: PairFreq) -> Pair:
...
3. 命名习惯
函数和普通变量:snake_case
- 函数名一般使用动词或动词短语,如
pre_tokenize、count_pairs; - 普通变量一般使用名词或名词短语,如
pair_freq、pretoken。
类型别名:PascalCase
例如:Pretoken、PairFreq。
常量:UPPER_CASE
例如:TOKEN_PATTERN。
优先表达语义
变量名优先说明它在算法中的含义,而不只描述 Python 数据类型。如 pretoken 优于 tuple_byte 。
4. 封装测试代码
只应在直接运行当前文件时执行的测试代码,可以放在下面的判断中:
if __name__ == "__main__":
test = " hhh hhh"
print(find_max_pair(count_pairs(pre_tokenize(test))))
5. 其他注意事项
- 顶层函数之间保留两个空行;
- 文档字符串(docstring)使用三重双引号
"""; - 简单函数使用一句话说明用途即可,必要时再补充输入约束。
6. 改进示例
改进前
import regex as re
PAT = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
# 该函数接收字符串,返回字节元组到整数频率的字典
def pre_tokenize(corpus):
token_iter = re.finditer(PAT, corpus)
pretoken_freq = {}
for element in token_iter:
token_now = element.group(0)
encoded = token_now.encode("utf-8")
tuple_key = tuple(bytes([b]) for b in encoded)
if tuple_key in pretoken_freq:
pretoken_freq[tuple_key] += 1
else:
pretoken_freq[tuple_key] = 1
return pretoken_freq
# 该函数接收上一函数返回的字典,返回二元组到整数频率的字典
def calc_pair(pretoken_freq):
count_pair = {}
for tuple_byte, freq in pretoken_freq.items():
tuple_len = len(tuple_byte)
for i in range(tuple_len - 1):
pair_now = (tuple_byte[i], tuple_byte[i + 1])
if pair_now in count_pair:
count_pair[pair_now] += freq
else:
count_pair[pair_now] = freq
return count_pair
# 该函数返回频数最大的二元组;频数相同时,返回字典序更大的二元组
def find_maximum(count_pair):
return max(
count_pair.items(),
key=lambda item: (item[1], item[0]),
)[0]
test = " hhh hhh"
print(find_maximum(calc_pair(pre_tokenize(test))))
改进后
from typing import TypeAlias
import regex as re
TOKEN_PATTERN = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
Pretoken: TypeAlias = tuple[bytes, ...]
Pair: TypeAlias = tuple[bytes, bytes]
PretokenFreq: TypeAlias = dict[Pretoken, int]
PairFreq: TypeAlias = dict[Pair, int]
def pre_tokenize(corpus: str) -> PretokenFreq:
"""统计语料中各预分词结果的出现频数。"""
token_iter = re.finditer(TOKEN_PATTERN, corpus)
pretoken_freq: PretokenFreq = {}
for element in token_iter:
token = element.group(0)
encoded = token.encode("utf-8")
pretoken = tuple(bytes([byte]) for byte in encoded)
pretoken_freq[pretoken] = pretoken_freq.get(pretoken, 0) + 1
return pretoken_freq
def count_pairs(pretoken_freq: PretokenFreq) -> PairFreq:
"""统计所有相邻字节对的加权频数。"""
pair_freq: PairFreq = {}
for pretoken, freq in pretoken_freq.items():
for index in range(len(pretoken) - 1):
pair = (pretoken[index], pretoken[index + 1])
pair_freq[pair] = pair_freq.get(pair, 0) + freq
return pair_freq
def find_max_pair(pair_freq: PairFreq) -> Pair:
"""从非空频数字典中返回频数最高、字典序最大的字节对。"""
return max(
pair_freq.items(),
key=lambda item: (item[1], item[0]),
)[0]
if __name__ == "__main__":
test = "lower, lower? low! it's very low"
pretoken_freq = pre_tokenize(test)
print(pretoken_freq)
pair_freq = count_pairs(pretoken_freq)
print(pair_freq)
max_pair = find_max_pair(pair_freq)
print(max_pair)
7. 用类型检查工具验证
添加类型标注后,还需要运行类型检查工具才能发挥作用。例如:
mypy your_file.py
或者:
pyright your_file.py
类型标注不会改变程序的运行逻辑,但可以提前暴露参数类型、返回值类型和容器元素类型不一致等问题。
小结
整理 Python 代码时,可以优先检查以下几点:
- 为函数参数和返回值添加类型标注;
- 为复杂的空容器显式标注元素类型;
- 用类型别名表达重复出现的领域概念;
- 使用能体现算法语义的名称;
- 将测试代码放入
if __name__ == "__main__":; - 最后运行类型检查工具验证标注是否一致。


