推荐阅读
- 先巩固语法与标准库核心用法
- 再看虚拟环境、测试与打包实践
- 最后看性能优化与异步并发
在阅读 Repository 或 Service 代码时,经常会同时看到 @staticmethod 和 @abstractmethod。它们都写在方法上方,很容易被误认为是同一类装饰器。 实际上,两者解决的是不同问题: @staticmethod 解决方法如何被调用 @abstractmethod 约束子类必须提供什么行为 理解这个区别后,方法应该放在类中、模块中,还是抽象基类中,会清楚很多。 @staticmethod:不依赖实例状态的方法 普通实例方法默认接收 self: class KeywordMatcher: def contains(self, text: str, keyword: str) -> bool: return keyword.casefold() in text.casefold() 如果方法体不读取或修改 self,可以写成静态方法: class KeywordMatcher: @staticmethod def contains(text: str, keyword: str) -> bool: return keyword.casefold() in text.casefold() 调用时不需要创建实例: KeywordMatcher.contains("变压器采购项目", "变压器") 静态方法没有自动传入的 self,因此不能访问实例属性: class KeywordMatcher: def __init__(self, case_sensitive: bool): self.case_sensitive = case_sensitive @staticmethod def contains(text: str, keyword: str) -> bool: # 这里不能访问 self.case_sensitive return keyword in text 如果函数只是通用计算,并不属于某个类的概念,直接定义为模块级函数通常更自然: def contains(text: str, keyword: str) -> bool: return keyword.casefold() in text.casefold() 所以“不需要 self”是使用 @staticmethod 的重要条件,但还要问一句:这个行为是否真的属于该类?如果答案是否定的,就不必为了使用装饰器而把函数塞进类里。 ...
JSON 不只有键值对,还可以表示数组、字符串、数字和布尔值。本文用关键词订阅示例解释 Python tuple、list、dict 与 SQLAlchemy JSON 列之间的映射关系。
Python 类型标注中的 tuple[T, ...]:任意长度和固定长度 副标题: tuple[DocumentBlock, ...] 中的 ... 不是一个要放进元组的值,而是 类型标注中的“任意数量”记号。理解它,才能看懂解析结果为什么使用元组,以及它和列表的边界。 适读人群: 正在使用 Python 类型标注、dataclass 或文档解析模型的开发者 阅读时间: 6 分钟 从 ParsedDocument 的 blocks 字段说起 在文档解析服务中,可以把解析结果表示成一个不可变的数据对象: from dataclasses import dataclass @dataclass(frozen=True) class DocumentBlock: block_id: str raw_text: str @dataclass(frozen=True) class ParsedDocument: blocks: tuple[DocumentBlock, ...] 这里最容易让人疑惑的是: blocks: tuple[DocumentBlock, ...] 这个标注到底表示什么?为什么不是: blocks: tuple[DocumentBlock] 答案是:前者表示“任意长度、每一项都是 DocumentBlock”,后者表示“固定只有一项, 而且这一项是 DocumentBlock”。 tuple[T, ...] 表示任意长度 把它拆开看: tuple[T, ...] ^ ^ | └── 元素数量不限 └────── 每一项的类型是 T 因此: ...
Python 中 if not value 与 is None 的区别 副标题: if not line 判断的是“这个值是否为假值”,if line is None 判断的是“这个值是否就是 None”。两者可能在某些输入上得到相同结果,但表达的业务语义并不相同。 适读人群: 正在学习 Python 条件判断,或需要处理文本、接口参数和可选字段的开发者 阅读时间: 6 分钟 从文档解析中的空行过滤说起 逐行解析文档时,经常会看到下面的代码: for raw_line in lines: line = raw_line.strip() if not line: continue # 继续处理非空内容 这里为什么使用: if not line: 而不是: if line is None: 关键不在于哪种写法更简短,而在于程序此时要识别的是空字符串,不是 缺失值 None。 先跟踪 line 是怎么产生的 这一段代码的执行顺序是: raw_line = " " line = raw_line.strip() str.strip() 会删除字符串首尾的空白字符,并返回一个字符串: assert " 投标人资格要求 ".strip() == "投标人资格要求" assert " ".strip() == "" assert "".strip() == "" 只包含空格的行经过 strip() 后会变成空字符串 "",不会变成 None。 因此后面的条件实际需要回答: ...
Python 字符串为什么不可变:从 replace 到变量重绑定 副标题: replace 看起来修改了字符串,实际发生的是创建结果并让变量改指向它。理解这一区别,才能看懂字符串方法、别名行为和循环拼接的成本。 适读人群: 刚开始学习 Python 字符串、变量与对象关系的读者 阅读时间: 6 分钟 从一段括号消除代码说起 下面的函数反复删除成对的括号,直到字符串不再变化: def is_valid_by_elimination(s: str) -> bool: while True: previous = s s = s.replace("()", "").replace("[]", "").replace("{}", "") if len(s) == len(previous): return s == "" 这里容易产生一个疑问: previous = s s = s.replace("()", "") 既然 previous 和 s 原来指向同一个字符串,第二行为什么不会同时改变 previous? 答案是:Python 字符串是不可变对象。replace 没有修改原字符串,而是计算出一个 结果,随后赋值语句让变量 s 改为指向这个结果。previous 仍然指向原字符串。 “不可变”到底是什么意思 字符串不可变,指的是: 一个 str 对象创建以后,它所表示的字符序列不能被原地修改。 例如: text = "cat" text[0] = "b" Python 会抛出异常: TypeError: 'str' object does not support item assignment 不能把已有字符串中的 c 原地改成 b。如果需要 "bat",必须得到另一个字符串值: ...
Python Callable 类型标注:怎么描述一个 handler 函数? 副标题: Callable[[dict[str, Any]], Any] 不是一个新类,而是在类型层面说明: 这个参数是一个“能被调用的对象”,它接收什么参数,返回什么结果。 标签: Python / 类型标注 / Callable / Type Alias / Pipeline 适读人群: 正在写回调、handler、pipeline step、插件机制的 Python 开发者 阅读时间: 6 min 背景:为什么 handler 类型看起来这么长? 在 pipeline runner 里,经常会有这种 handler 表: handlers = { "prepare": prepare, "enrich": enrich, } 每个 handler 都接收同一个运行时上下文: def prepare(context: dict[str, Any]) -> dict[str, bool]: context["prepared"] = True return {"prepared": True} 如果直接给 handler 表写类型,会变成: from collections.abc import Callable, Mapping from typing import Any handlers: Mapping[str, Callable[[dict[str, Any]], Any]] 这一串不难,但读起来吵。它把“workflow name 到 workflow handler 的映射”写成了一段类型语法。 ...
Python 从 dict/list 原型到稳定接口:什么时候该整理类型? 副标题: 验证想法时先用普通变量和 dict/list 跑通行为;模型稳定后,再把它们整理成函数参数、dataclass、helper 和类型标注。 标签: Python / 原型验证 / 类型标注 / 接口设计 / Pipeline 适读人群: 正在从脚本式代码过渡到可维护模块的 Python 开发者 阅读时间: 7 min 背景:为什么一开始不用把类型设计得很完整? 写一个新功能时,经常会卡在这个问题上: 我应该一开始就写 dataclass、class、Mapping、Callable 吗? 还是先用普通 dict/list 把行为跑通? 答案不是固定的,但有一个很实用的判断: 模型还不确定时,先用 dict/list 验证行为; 模型稳定后,再把稳定的东西整理成接口。 这不是偷懒,而是在降低错误抽象的成本。 比如你想验证一个 graph pipeline runner,最早可以只写: graph_plans = { "parallel": { "prepare": (), "enrich": ("prepare",), "extract": ("prepare",), } } 这个变量一开始不是“最终架构”,只是为了回答一个问题: dependency map 能不能表达 prepare 之后 enrich 和 extract 同时 ready? 如果这个问题都没验证清楚,提前设计完整类层次反而会遮住真正的行为压力。 第一阶段:用普通变量验证模型 最早的脚本里,很多东西看起来像“常量”: ...
Python 类型标注里为什么用 Mapping、Sequence 这类抽象接口? 副标题: 当一个函数只读取配置,不修改调用方传入的字典时,用 Mapping 比直接写 dict 更能表达接口意图;同理,Iterable、Sequence 和 Callable 也在表达“我需要什么能力”。 标签: Python / 类型标注 / Mapping / Sequence / Callable / 接口设计 适读人群: 正在写 Python 配置对象、runner、pipeline、SDK 参数的开发者 阅读时间: 7 min 背景:为什么不是所有字典参数都写成 dict? 写 Python 配置型代码时,经常会有这种参数: graph_plans = { "parallel": { "prepare": (), "enrich": ("prepare",), "extract": ("prepare",), } } 如果一个 runner 只是读取它: runner = GraphPipelineRunner(graph_plans=graph_plans, handlers=handlers) 那么类型标注可以写成: from collections.abc import Mapping def __init__( self, *, graph_plans: Mapping[str, Mapping[str, tuple[str, ...]]], handlers: Mapping[str, WorkflowHandler], ) -> None: ... 刚开始看会觉得复杂:为什么不直接写 dict? ...
函数命名不是取名字:它是在描述系统边界 副标题: 好的函数名不只是“好听”。它应该让读者看出:这个函数在做读取、 解析、校验、筛选、创建,还是修改状态。 适读人群: 正在写 Python service、runner、pipeline、SDK 或业务模块的开发者 阅读时间: 8 min 背景:为什么 get_dependencies 不是总比 resolve_graph 直观? 看一个小型 graph pipeline runner: def trigger(self, mode, context): dependencies = self._resolve_graph(mode) self._validate_graph(dependencies) record = self._create_graph_record(mode, dependencies, context) while True: ready_nodes = self._ready_nodes(record) if not ready_nodes: break for node_name in ready_nodes: ... 刚开始可能会问: 为什么不叫 get_dependencies? 为什么不叫 get_handler? 为什么 ready_nodes 没有动词? 为什么 skip_dependents 一看就像会改状态? 这些不是风格洁癖。函数命名背后其实是在表达系统边界: mode 是外部语言,graph 是内部执行结构。 handler name 是配置语言,handler 是可调用对象。 ready node 是运行时状态,不是普通列表。 skip dependents 是副作用,不是查询。 这篇文章用一个 pipeline runner 作为例子,讲清楚函数命名应该如何暴露设计意图。 ...
标题 请求日志一定要带 RequestId 吗?Python 成熟实践与落地指南 副标题 / 摘要 几乎所有“请求相关”的日志都应该带 requestId,但要通过自动注入而不是手工拼接。 本文给出 Python 成熟做法、工程场景与与 tracing 的关系,帮你真正落地。 目标读者 初学者:第一次处理线上问题,不懂为什么日志要串 requestId。 中级开发者:需要一套可复制的 Python 日志注入方案。 团队负责人:想建立统一的日志与追踪规范。 背景 / 动机 当系统出现错误时,最常见的现场是: “某个时间点报错了,但不知道是哪次请求导致的。” 如果所有“请求相关日志”都有 requestId,你就能一条链串起来: 从入口 → DB → RPC → 异常,一次请求的关键路径一眼可见。 在微服务/多进程环境里,requestId 更是日志协作的最低门槛。 核心概念 requestId:一次请求的唯一编号,用于日志串联与快速定位。 trace_id / span_id:分布式追踪中的链路标识(trace)与步骤标识(span)。 上下文传播:跨线程 / 协程 / 服务传递 requestId 或 trace。 自动注入:通过 middleware + logging filter,在日志里自动带 requestId。 思维推导(从朴素到工程可用) 朴素做法:每条日志手动写 request_id,很快遗漏、重复、维护成本高。 痛点暴露:一次请求会跨多个函数/协程/库层,手写方式不可控。 关键观察:requestId 本质是“请求上下文”,应由框架统一注入。 方法选择:在入口生成 requestId → 传入上下文 → logging 自动注入。 正确性理由:上下文随请求自然传播,日志格式统一且不侵入业务代码。 A — Algorithm(题目与算法) 题目还原 “是不是每一条日志都应该带 requestId?” ...