spaCy 代码规范(Code Conventions)全指南:从兼容性到测试的开发者协作手册
spaCy 代码规范Code Conventions全指南从兼容性到测试的开发者协作手册【免费下载链接】spaCy Industrial-strength Natural Language Processing (NLP) in Python项目地址: https://gitcode.com/GitHub_Trending/sp/spaCy本文档系统梳理 spaCy 核心库的代码规范覆盖 Python/Cython 兼容策略、black 自动格式化、flake8 静态检查、docstring 与类型注解规范、逻辑结构设计、命名约定、错误处理体系以及 pytest 测试方法论。无论你是打算向 spaCy 提交 Pull Request 的贡献者还是希望在自己项目中借鉴工业级 NLP 库工程实践的开发者读完本文都能掌握一套可落地、可检查、可评审的代码质量流程。适用说明本指南针对 spaCy 仓库当前版本Python3.9,3.15见 setup.cfg文中所有命令与工具链black、flake8、mypy、pytest均以当前仓库 Makefile 与 setup.cfg 中的实际配置为准。目录代码兼容性Code compatibility自动格式化Auto-formatting静态检查Linting文档化代码Documenting code类型注解Type hints逻辑结构Structuring logic命名规范Naming错误处理Error handling编写测试Writing tests1. 代码兼容性spaCy 的核心库由纯 Python.py与 Cython.pyx/.pxd两部分构成。官方约定所有代码都必须保持向后兼容且在 Python 3.6 上可运行当前仓库已进一步将下限推进到 Python 3.9python_requires 3.9,3.15见 setup.cfg。需要遵守的原则不轻易使用较新的语法特性某些新语法会破坏旧解释器的解析能力除非已放弃对旧版本的支持否则不应使用。有条件地安装 backport部分新特性存在官方或社区的 backport 包仅在“绝对必要”时才通过条件依赖引入。条件导入必须收敛到compat.py凡是需要根据 Python 版本分支的导入、或自定义兼容性辅助逻辑统一放在compat.py中而不是散落在各模块里。spacy/compat.py 就是这一约定的直接落地它集中处理了# spacy/compat.py节选 import sys from thinc.util import copy_array try: import cPickle as pickle except ImportError: import pickle # type: ignore[no-redef] try: import copy_reg except ImportError: import copyreg as copy_reg # type: ignore[no-redef] try: from cupy.cuda.stream import Stream as CudaStream except ImportError: CudaStream None try: import cupy except ImportError: cupy None if sys.version_info[:2] (3, 8): # Python 3.8 from typing import Literal, Protocol, runtime_checkable else: from typing_extensions import Literal, Protocol, runtime_checkable # noqa: F401注意其中的几个细节它们正是规范的生动体现pickle/copy_reg这类模块在不同 Python 版本中名称不同通过try/except ImportError分支导入并用# type: ignore[no-redef]抑制类型检查器对重复定义的告警CuPyGPU 支持是可选依赖导入失败时降级为None文件末尾的pickle pickle、copy_reg copy_reg等重新赋值是为了让类型检查器能正确解析这些条件导入的符号同时导出了is_windows、is_linux、is_osx平台标志供核心代码做平台分支。给贡献者的建议当你发现需要做版本或平台分支时先检查compat.py是否已有可复用的 helper而不是新开一套逻辑。2. 自动格式化spaCy 使用black作为统一代码格式化工具并有以下配套机制pre-commit hookblack 已配置为提交前的自动检查/格式化钩子编辑器集成官方推荐在编辑器中配置保存时自动格式化或手动触发GitHub Action 自动格式化仓库配有定时运行 black 的 CI Action若检测到格式化差异会自动提交一个 PR适用范围目前仅覆盖.pyPython文件不覆盖.pyxCython文件——Cython 代码需要手工保持格式。一个重要的判断准则如果 black 格式化出来的代码看起来很乱通常说明这段代码本身存在更简洁的重构方式而不是去调整 black 配置。文档给出了一组对比- range_suggester registry.misc.get(spacy.ngram_range_suggester.v1)( - min_size1, max_size3 - ) suggester_factory registry.misc.get(spacy.ngram_range_suggester.v1) range_suggester suggester_factory(min_size1, max_size3)先取出工厂函数、再调用工厂比直接链式调用更利于 black 保持一行放得下的简洁格式可读性也更好。按块关闭格式化# fmt: off/# fmt: on在少数场景尤其是测试用例长对齐结构被 black 重排后反而难以阅读可以用# fmt: off与# fmt: on包裹让 black 跳过该区间 # fmt: off text I look forward to using Thingamajig. Ive been told it will make my life easier... deps [nsubj, ROOT, advmod, prep, pcomp, dobj, punct, , nsubjpass, aux, auxpass, ROOT, nsubj, aux, ccomp, poss, nsubj, ccomp, punct] # fmt: on这种手工对齐的“网格”式数据如依赖标签序列在测试中非常常见fmt: off保留了人工对齐的信息量。例如 spacy/tests/doc/test_doc_api.py 中就有多处# fmt: off的用法同样spacy/errors.py 的错误消息大块字符串区域也用# fmt: off保护因为重新换行会破坏长文本的阅读结构。注意fmt: off是“特例”不是“常态”。只有像测试数据表、错误消息这类确实需要人工对齐的内容才值得使用。3. 静态检查spaCy 使用flake8强制执行代码风格。它可以扫描单个或多个文件并输出错误与警告帮助发现潜在的失误与不一致。flake8 spacy这条命令扫描整个spacy核心包任何新提交的代码都不能引入 flake8 告警。最常见的 lint 问题及对策问题含义与处理尾随/缺失空白属于格式化问题运行 black 即可自动修复未使用的导入F401若确实未使用则删除若是为了“重新导出”符号让其他模块能从这里导入则加# noqa: F401并附注释说明未使用的变量F841 等往往暗示 bug变量声明了却没被正确传递/返回。解包时用_表示刻意忽略函数重定义往往暗示 bug例如复制粘贴后忘记改名导致覆盖原函数重复的字典键要么是 bug要么是冗余重复与True/False/None用比较应使用is例如if value is None针对特殊情况忽略 lint 规则忽略单行告警使用# noqa: CODE注释可以逗号分隔多个代码如# noqa: E731,E123。务必总是显式指定要忽略的代码——裸的# noqa会掩盖所有问题。# 该类在本文件中未使用但在此导入以便其他模块能从这里导入它 from .submodule import SomeClass # noqa: F401 try: do_something() except: # noqa: E722 # 这个裸 except 是合理的出于某个特定原因 do_something_else()实际例子spaCy 的 spacy/compat.py 中对typing_extensions的条件导入就标注了# noqa: F401spacy/tests/doc/test_doc_api.py 中clean_underscore的再导入同样如此。4. 文档化代码Docstring 规范所有函数和方法都应内联 docstring包含一句话摘要 参数概览含简化类型。现代编辑器会在调用处自动展示这些信息。如果该函数属于公开 API 且有对应文档章节通常会在 docstring 末尾附上DOCS:链接让 docstring 保持简洁同时把详细示例留给文档站def has_pipe(self, name: str) - bool: Check if a component name is present in the pipeline. Equivalent to name in nlp.pipe_names. name (str): Name of the component. RETURNS (bool): Whether a component of the name exists in the pipeline. DOCS: https://spacy.io/api/language#has_pipe ...这正是 spacy/language.py 中Language.has_pipe的真实实现——docstring、签名与实现完全一致。spaCy 特意选择将 docstring 与 API 参考文档分离维护而不是像其他包那样从 docstring 自动生成 API 文档。原因有三文档站需要承载更详尽的解释与示例并可以使用自定义标记塞进 docstring 会使其臃肿文档可以独立于代码库更新降低发布耦合虽然工作量更大但对用户体验与开发者体验都更值得。内联代码注释不为显而易见的事加注释——代码本身应当自解释如果注释“看起来有必要”先怀疑代码是否能写得更直白。复杂/反直觉逻辑必须注释包括容易踩坑的边界条件甚至是你修掉的隐蔽 bug都值得留下一句上下文说明token_index indices[value] # Index describes Token.i of last token but Span indices are inclusive span doc[prev_token_index:token_index 1] # To create the components we need to use the final interpolated config # so all values are available (if component configs use variables). # Later we replace the component config with the raw config again. interpolated filled.interpolate() if not filled.is_interpolated else filled修复具体 issue 时带上 issue 号尤其对相对简单的调整 # Ensure object is a Span, not a Doc (#1234) if isinstance(obj, Doc): obj obj[obj.start:obj.end]文档的态度很直白“不要羞于为那些你自己都觉得很难实现/很难写对的棘手部分加注释——它们对下一个接手的人甚至未来的你自己都可能派上用场。”使用 TODO 注释允许用TODO:前缀标注未来的改进点现代编辑器通常会给它不同的颜色以便识别。TODO 不一定是必须立刻修复的那些应该在 PR 就绪前解决但可以记录潜在的改进方向 # TODO: this is currently pretty slow dir_checksum hashlib.md5() for sub_file in sorted(fp for fp in path.rglob(*) if fp.is_file()): dir_checksum.update(sub_file.read_bytes())如果某个 TODO 重要且需要尽快处理应同时登记到内部任务看板或公开 issue 跟踪器确保不被遗忘。5. 类型注解所有.py文件中尽可能使用类型注解帮助理解函数输入输出并在编辑器中获得智能提示。.pyxCython代码目前不使用类型注解唯一例外是注册函数与组件工厂的定义——那里注解用于配置校验。开发时建议对代码库运行mypy spacy检查问题setup.cfg 中已配置[mypy]段启用ignore_missing_imports、no_implicit_optional并挂载了pydantic.mypy、thinc.mypy插件。用描述性类型替代裸类型尽可能使用List[str]甚至List[Any]而不是裸的list- def func(some_arg: dict) - None: def func(some_arg: Dict[str, Any]) - None: ...Callable的注解有两个参数按顺序排列的参数类型列表与返回值类型def create_callback(some_arg: bool) - Callable[[str, int], List[str]]: def callback(arg1: str, arg2: int) - List[str]: ... return callback变量注解为变量标注类型时优先使用显式格式PEP 526 风格- var value # type: Type var: Type valueThinc 自定义类型对于模型架构Thinc 提供了一组 自定义类型包括针对数组与模型输入输出的更精确类型。即使在静态检查之外这些类型也能大幅提升代码可读性——它明确表达了期望的数组类型以及输出与期望不符时可能出什么问题def build_tagger_model( tok2vec: Model[List[Doc], List[Floats2d]], nO: Optional[int] None ) - Model[List[Doc], List[Floats2d]]: ...前向引用与循环导入字符串注解 TYPE_CHECKING如果需要引用“稍后才定义”的类型如同模块后面的类、或方法所属的类自身可以用字符串形式class SomeClass: def from_bytes(self, data: bytes) - SomeClass: ...当跨模块类型会引发循环导入时同样用字符串注解并把导入放进typing.TYPE_CHECKING块使其只在类型检查器求值时执行from typing import TYPE_CHECKING if TYPE_CHECKING: from .language import Language def load_model(name: str) - Language: ...文档特别点名了 spacy/util.py 的场景它包含大量返回Language实例的 helper 函数但不能直接导入Language因为 spacy/language.py 本身又导入了util。字符串注解 TYPE_CHECKING正是解开这个环的钥匙。约定from typing import ...语句通常放在 Python 模块的最前面几行。6. 逻辑结构位置参数与仅关键字参数尽量避免函数/方法参数过多能用仅关键字keyword-only参数的地方就用。Python 通过, *分隔符把后续参数声明为 keyword-only- def do_something(name: str, validate: bool False): def do_something(name: str, *, validate: bool False): ... - do_something(some_name, True) do_something(some_name, validateTrue)好处有二调用点语义一目了然后续增删或调整参数顺序时不会因为依赖位置序的调用而连锁改动。避免可变默认参数可变默认参数[]、{}是经典 Python 陷阱默认值只在函数创建时构造一次之后每次调用若修改它实际改的是同一个对象。spaCy 的解法是使用SimpleFrozenList与SimpleFrozenDict两个 helper——它们是冻结实现一旦被修改就抛错从机制上杜绝“意外修改默认参数”的 bug- def to_bytes(self, *, exclude: List[str] []): def to_bytes(self, *, exclude: List[str] SimpleFrozenList()): ...def do_something(values: List[str] SimpleFrozenList()): if some_condition: - values.append(foo) # raises an error values [*values, foo] return values从源码看spacy/util.py 的SimpleFrozenDict覆写了__setitem__、pop、updatespacy/util.py 的SimpleFrozenList覆写了append、clear、extend、insert、pop、remove、reverse、sort全部直接raise NotImplementedError(self.error)。实际使用遍布核心库例如 spacy/language.py 中replace_pipe的config: Dict[str, Any] SimpleFrozenDict()默认参数。不要用try/except控制流程强烈反对把try/except当作控制流使用第三方错误处理或无法控制的外部错误除外。应显式检查实际问题这让代码更易读、更不易出 bug- try: - token doc[i] - except IndexError: - token doc[-1] if i len(doc): token doc[i] else: token doc[-1]即使需要显式检查多个条件也比笼统的try/except更可取——它会促使你思考每个步骤到底可能出什么问题从而写出更好的代码同时try/except很容易掩盖与你捕获的错误同类型的其他 bug。如果必须使用try/excepttry块内只放绝对必要的语句显式指定异常类型如except ValueError:而不是裸except:否则可能掩盖完全不同原因导致的异常- try: - value1 get_some_value() - value2 get_some_other_value() - score external_library.compute_some_score(value1, value2) - except: - score 0.0 value1 get_some_value() value2 get_some_other_value() try: score external_library.compute_some_score(value1, value2) except ValueError: score 0.0避免 lambdalambda适合单行匿名函数但存在实际问题pickle 序列化需要额外逻辑、类型注解写起来很丑。因此核心库通常避免使用 lambda只在序列化处理器与测试内部为简单起见保留优先重构为具名 helper 函数- split_string: Callable[[str], List[str]] lambda value: [v.strip() for v in value.split(,)] def split_string(value: str) - List[str]: return [v.strip() for v in value.split(,)]数值比较作为通用规则数值比较统一使用与避免使用与以保持“包含下界、排除上界”的一致性防止 off-by-one 错误。唯一例外是三元链式比较if value 0 and value max:可以简写为if 0 value max:即使它用到了。迭代与推导式避免使用filter、map这类内建函数优先使用列表/生成器推导式- filtered filter(lambda x: x in [foo, bar], values) filtered (x for x in values if x in [foo, bar]) - filtered list(filter(lambda x: x in [foo, bar], values)) filtered [x for x in values if x in [foo, bar]] - result map(lambda x: { x: x in [foo, bar]}, values) result ({x: x in [foo, bar]} for x in values) - result list(map(lambda x: { x: x in [foo, bar]}, values)) result [{x: x in [foo, bar]} for x in values]逻辑一旦复杂写普通循环反而更好——即使行数更多结果也更容易理解- result [{key: key, scores: {f{i}: score for i, score in enumerate(scores)}} for key, scores in values] result [] for key, scores in values: scores_dict {f{i}: score for i, score in enumerate(scores)} result.append({key: key, scores: scores_dict})组合优于继承虽然 spaCy 用了大量类但继承被视为“最后手段”对其持谨慎态度。扩展类层级前应先讨论方案。除非在实现新的数据结构或 pipeline 组件通常根本不需要使用类。核心库不print核心库从不调用print。调试时可以随意用print观察状态但提交 PR 前必须清理干净。需要向用户输出警告或调试信息时使用专门的机制警告→warnings.warn见第 8 节日志→ spaCy 的logger见第 8 节CLI 消息美化→ 使用轻量级 helper 库wasabi见 setup.cfg 依赖声明。唯一的例外是 CLI 函数它们为用户美化打印消息以及明确用于打印的方法例如Language.analyze_pipes在prettyTrue时的输出。7. 命名规范命名是公认的难题spaCy 不强求一次到位往往需要迭代与协作但遵循以下基本约定类名含 dataclass用CamelCase方法、函数、变量用snake_case常量用UPPER_SNAKE_CASE通常定义在模块顶部避免用变量名遮蔽内建函数名如input、help、list。变量命名变量名必须说清“它到底是什么、用来干什么”。常见类的实例应使用一致的命名避免把非Doc对象如普通字符串命名为doc。官方给出的类-变量映射表ClassVariableExampleLanguagenlpnlp spacy.blank(en)Docdocdoc nlp(Some text)Spanspan,ent,sentspan doc[1:4],ent doc.ents[0]Tokentokentoken doc[0]Lexemelexeme,lexlex nlp.vocab[foo]Vocabvocabvocab Vocab()Exampleexample,egexample Example.from_dict(doc, gold)Configconfig,cfgconfig Config().from_disk(config.cfg)此外避免引入过多临时变量污染命名空间可以重新赋值给已有变量但仅当新值类型相同ents get_a_list_of_entities() ents [ent for ent in doc.ents if ent.label_ PERSON] - ents {(ent.start, ent.end): ent.label_ for ent in ents} ent_mappings {(ent.start, ent.end): ent.label_ for ent in ents}这里第二次赋值ents实体的列表 → 实体映射改变了类型因此改用新变量名ent_mappings。方法与函数命名名字尽量简短且具描述性执行动作的方法使用祈使动词如disable_pipes、add_patterns、get_vector不面向用户 API 的私有方法/函数加下划线前缀_可以多看现有类找灵感。可序列化对象应实现一致的序列化方法族通常至少包含to_disk、from_disk、to_bytes、from_bytes部分对象还可以实现{to/from}_dict、{to/from}_str等更具体的变体。8. 错误处理集中式错误消息目录ErrorsspaCy 鼓励为所有可预见的错误编写详尽、有帮助的自定义错误消息并集中在 spacy/errors.py 中每个消息带唯一代码。这样做的收益代码库更简洁避免大段文本打断阅读流错误码便于在不同位置检索同一错误用户报告问题时只需搜索错误码即可定位。错误通过代码引用例如Errors.E123消息可含占位符用.format()填充class Errors: E123 Something went wrong E456 Unexpected value: {value}if something_went_wrong: - raise ValueError(Something went wrong!) raise ValueError(Errors.E123) if not isinstance(value, int): - raise ValueError(fUnexpected value: {value}) raise ValueError(Errors.E456.format(valuevalue))核心库中抛出的所有错误消息原则上都应加入Errors。唯一例外是spacy.cli模块——CLI 函数会美化打印大量输出很难与逻辑分离因此直接以字符串形式写错误与消息。源码印证Errors使用了元类ErrorsWithCodesspacy/errors.py它在访问任何非__开头的属性时自动把代码格式化为[{code}] {msg}保证用户看到的每条错误都带[E123]这样的可检索前缀而raise ValueError(Errors.E160.format(pathpath))这类调用遍布 spacy/util.py 等模块。重抛异常Re-raising在第三方代码或差异很大的上下文里预见错误时尽量提供自定义且更具体的消息。在try/except内重抛自定义异常时用raise ... from e链接原始异常让用户同时看到原始错误与自定义消息并附注 “The above exception was the direct cause of the following exception”try: run_third_party_code_that_might_fail() except ValueError as e: raise ValueError(Errors.E123) from e如果原始异常已知且无帮助可以用raise ... from None抑制它避免终端被一堆无关的链式异常刷屏try: run_our_own_code_that_might_fail_confusingly() except ValueError: raise ValueError(Errors.E123) from None避免裸assert开发阶段用assert做快速校验没问题但清理代码时应删除或替换为显式错误处理——否则用户只会看到光秃秃的AssertionError毫无信息量- assert score 0.0 if score 0.0: raise ValueError(Errors.E789.format(scorescore))与其给assert加消息不如对特定条件raise更明确的错误。若检查的是“理应正确、出错就是 spaCy 内部 bug”的不变量可以在错误消息中直说例如E161 (Found an internal inconsistency when predicting entity links. This is likely a bug in spaCy, so feel free to open an issue: https://github.com/explosion/spaCy/issues)警告Warnings部分场景不抛错而是用warnings.warn提示潜在问题消息定义在 spacy/errors.py 的Warnings类中同样经ErrorsWithCodes元类处理为[W123]前缀。是否显示警告可由用户控制包括用正则匹配内部代码如W123自定义过滤。- print(Warning: No examples provided for validation) warnings.warn(Warnings.W123)两个实操要点不要重复warnings.warn如在循环里会刷屏终端。应收集问题后只抛一次汇总警告问题严重则考虑直接抛错 n_empty 0 for spans in lots_of_annotations: if len(spans) 0: - warnings.warn(Warnings.456) n_empty 1 warnings.warn(Warnings.456.format(countn_empty))事实上 spacy/errors.py 的setup_default_warnings()会默认对特定警告做过滤如 numpy 大小变化、matcher/entity_ruler/span_ruler无 pattern 等只警告一次说明“何时、怎样显示警告”本身也是被管理的。日志Logging日志通过 spaCy 的logger输出底层是 Python 原生logging模块 logger.info(Set up nlp object from config) config nlp.config.interpolate()logger定义于 spacy/util.py配置了StreamHandler与[%(asctime)s] [%(levelname)s] %(message)s格式。使用原则INFO级日志仅用于调试信息用户可能选择查看或训练期间相关、但运行时无关的信息。spacy train等 CLI 命令默认启用全部INFO日志而运行时不会输出——这让核心库可以在训练时输出特定信息而不打扰正常使用DEBUG级日志只有用户训练时开启--verbose才显示用于更详细、更可能暴露 bug 或说明内部行为的细节克制使用只有绝对必要且重要时才加日志语句。源码印证spacy/cli/train.py 中spacy train的--verbose/-V/-VV选项在verbose为真时执行util.logger.setLevel(logging.DEBUG)与文档描述完全一致。9. 编写测试spaCy 使用pytest测试框架。测试组织原则各模块/类的测试位于同名目录下如spacy/tests/doc/、spacy/tests/matcher/、spacy/tests/parser/测试文件统一以test_前缀命名核心库测试只测代码本身不依赖任何训练好的 pipeline实现新功能或修 bug 时通常先写描述“应该发生什么”的测试再边写代码边跑相关测试直至全绿。测试套件结构测试名要具描述性一次只测一种行为测试按功能类型分组到专用模块较大的领域组织为测试文件目录如matcher、tokenizer回归测试针对具体 issue 报告的 bug放在对应模块中按 issue 号命名如test_issue1234.py并用自定义标记标注如pytest.mark.issue(1234)。这套体系让“测试 ↔ 原始 issue”可互相追溯——一旦引入回归此前通过的回归测试再次失败时能立刻定位。修 bug 时最好先创建回归测试。真实例spacy/tests/doc/test_doc_api.py中大量pytest.mark.issue(...)1547、1757、2396、2782、3869、3962、4903……的用法。测试套件提供各语言 tokenizer 的fixtures定义于 spacy/tests/conftest.py可直接作为同名函数参数自动注入还有 spacy/tests/util.py 提供公共工具如创建临时文件。这些 fixture 只用于对应语言的测试。测试 Cython 代码开发.pyx代码时扩展必须先编译否则测试运行器会跑上一次编译的陈旧代码。本地编译python setup.py build_ext -i构造对象与状态测试函数通常遵循同一结构设置状态 → 执行要测的操作 → 用assert断言期望成立操作前后都要。测试应只聚焦被测内容尽量避免依赖其他无关库功能。如果测试只需要一个带指定标注的Doc就手工构造def test_doc_creation_with_pos(): doc Doc(Vocab(), words[hello, world], pos[NOUN, VERB]) assert doc[0].pos_ NOUN assert doc[1].pos_ VERB参数化测试同一测试函数跑多组输入时用参数化而不是测试内循环。pytest.mark.parametrize接受两个参数一个逗号分隔的参数字符串以及对应的用例列表元组列表可提供多参数pytest.mark.parametrize(words, [[hello, world], [this, is, a, test]]) def test_doc_length(words): doc Doc(Vocab(), wordswords) assert len(doc) len(words)pytest.mark.parametrize(text,expected_len, [(hello world, 2), (I cant!, 4)]) def test_token_length(en_tokenizer, text, expected_len): # en_tokenizer is a fixture doc en_tokenizer(text) assert len(doc) expected_len参数化的好处用例与逻辑分离且 pytest 能精确告诉你哪个用例失败了。可以堆叠多个pytest.mark.parametrize但不推荐——堆叠会以所有参数值的全部组合运行测试通常并非本意且拖慢套件。处理失败测试xfail表示“应当通过但当前失败”期望失败。用pytest.mark.xfail标记仅用于尚未实现的测试不用于测试故意抛出的错误见“测试错误与警告”。可以传reason解释原因 pytest.mark.xfail(reasonIssue #225 - not yet implemented) def test_en_tokenizer_splits_em_dash_infix(en_tokenizer): doc en_tokenizer(Will this road take me to Puddleton?\u2014No.) assert doc[8].text \u2014xpass标记xfail却没失败。值得调查可能 bug 已被修复可移除装饰器在 ML 模型实现场景中也可能意味着测试不稳定flaky——时过时不过可能是 bug 或约束定义过窄。若测试单独跑与一起跑行为不同说明它受到了先前测试设置的全局状态影响应当避免。补充spacy/tests/README.md也强调——xfail只用于“应当通过但当前失败”的测试要测试期望的负面行为用assert not。编写慢测试有用但可能很慢的测试用pytest.mark.slow标记spaCy 引入的自定义标记注册于 setup.cfg 的[tool:pytest] markers段。带此标记的测试只在运行测试套件时加--slow才执行不属于主 CI 流程。引入慢测试前先确认没有更高效的方式最好再加一个只用部分用例、总是运行的简化版本保证至少有基础覆盖。跳过测试pytest.mark.skip完全跳过测试仅用于慢且可能出错、可能引发内存错误或段错误会终止整个进程xfail拦不住的失败测试以及已过时但想保留的旧回归测试。使用skip时必须提供reason。测试错误与警告错误用pytest.raises上下文管理器断言抛出指定异常。实现自定义错误处理时不但要测正确行为还要测错误输入导致的错误测试错误必须显式用pytest.raises不要用xfailwords [a, b, c, d, e] ents [Q-PERSON, I-PERSON, O, I-PERSON, I-GPE] with pytest.raises(ValueError): Doc(Vocab(), wordswords, entsents)警告用pytest.warns上下文管理器检查是否产生指定警告类型。第一个参数是警告类型或None后者会捕获警告列表可assert其为空def test_phrase_matcher_validation(en_vocab): doc1 Doc(en_vocab, words[Test], deps[ROOT]) doc2 Doc(en_vocab, words[Test]) matcher PhraseMatcher(en_vocab, validateTrue) with pytest.warns(UserWarning): # Warn about unnecessarily parsed document matcher.add(TEST1, [doc1]) with pytest.warns(None) as record: matcher.add(TEST2, [docs]) assert not record.list注意如果指定了警告类型但测试没有产生该警告测试会失败——所以pytest.warns只用于验证 spaCy 正确处理/输出警告。如果测试本身输出了预期但无关的警告用pytest.mark.filterwarnings忽略以特定代码开头的警告pytest.mark.filterwarnings(ignore:\\[W036) def test_matcher_empty(en_vocab): matcher Matcher(en_vocab) matcher(Doc(en_vocab, words[test]))setup.cfg中[tool:pytest]的filterwarnings error会把警告升级为错误因此这套过滤机制在 CI 中尤为重要。测试训练好的 pipeline常规测试套件不依赖任何训练好的 pipeline——其输出可变也不是测试库功能所必需的pipeline 的测试单独放在spacy-models仓库中每次训练新模型时运行主要验证包能正常加载、预测看起来合理并覆盖历史上遇到过的常见 bug 检查如果你的测试主要目的不是验证模型预测应归入核心库测试并手工构造所需对象而不是加进模型测试。需要牢记具体预测结果可能变化也无法穷尽用户报告的错误预测。不同模型会犯不同错误——即使整体准确率高得多的模型也可能做出以前没犯过的错误预测。但某些“出乎意料”的错误预测可能暗示深层 bug值得调查。附开发工作流速查将本文规范落地到日常开发可以形成一条完整的检查链# 1. 自动格式化仅 .py black spacy # 2. 静态检查 flake8 spacy # 3. 类型检查 mypy spacy # 4. 构建 Cython 扩展改动 .pyx 后必须执行否则测试跑的是旧代码 python setup.py build_ext -i # 5. 运行测试常规slow 标记的测试需加 --slow pytest spacy/tests仓库 Makefile 中的test目标展示了 spaCy 自己的测试流水线用pex构建发布包与独立 pytest 环境后执行pytest --pyargs spacy -x。规范的最终目的不是制造条条框框而是让一个体量庞大、Python 与 Cython 混合、被无数下游项目依赖的 NLP 库在数百位贡献者的协作下依然保持代码风格统一、错误信息可检索、测试行为可预期。对贡献者而言遵循这套约定意味着更顺畅的评审流程对自己项目而言这份指南本身就是一份高质量的开源工程实践清单。【免费下载链接】spaCy Industrial-strength Natural Language Processing (NLP) in Python项目地址: https://gitcode.com/GitHub_Trending/sp/spaCy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →