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_tokenizecount_pairs
  • 普通变量一般使用名词或名词短语,如 pair_freqpretoken

类型别名:PascalCase

例如:PretokenPairFreq

常量: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 代码时,可以优先检查以下几点:

  1. 为函数参数和返回值添加类型标注;
  2. 为复杂的空容器显式标注元素类型;
  3. 用类型别名表达重复出现的领域概念;
  4. 使用能体现算法语义的名称;
  5. 将测试代码放入 if __name__ == "__main__":
  6. 最后运行类型检查工具验证标注是否一致。