Python 17:命令行输出怎样供人和程序使用
把 "One two one" 交给命令行,成功时标准输出只有一行 JSON,result 是 {"one": 2, "two": 1};把输入换成不存在的文件,标准输出为空,错误写入标准错误,退出码是 2。这两个观察分别回答程序调用方和操作人的问题:哪里取可解析的结果,哪里找失败原因。 本章沿用16 的资源作用域和原来的 submit → run → query,只在外面加一层入口。tasklab 还没有 SQLite;每次调用创建的是独立的进程内任务,返回 task-0001 不意味着下次启动还能查询它。跨进程查询留到 21,不用“CLI 已经能打印 JSON”冒充持久化。 参数何时变成任务 入口 examples/python-language/src/tasklab/cli.py 用 ArgumentParser 把 --text 和 --file 放入 required=True 的互斥组。没有输入和同时传两种输入都会在解析阶段被拒绝。--kind 目前只有 word_count 能进入已...
Python 16:with 退出以后资源归谁
词元生成器只读出第一个 "one" 就停止。随后文件已经关闭;同样的文件在完整读取和消费者抛错两条路径上也关闭。这里的保证并非“生成器迟早会被回收”,而是打开文件的作用域也负责在退出时关闭它,生成器停止时的处理则写在同一个作用域的 finally 中。 09 对生成器提前终止只验证了不再读取后续块,当时根本没有打开文件,不能把那项断言当作资源关闭证据。16 在相同的词频管道外增加真实 UTF-8 文件句柄,并保留原 tasklab 的结果合同。 谁打开,谁确保退出 12345678@contextmanagerdef open_word_file(path): with Path(path).open("r", encoding="utf-8", errors="strict") as source: words = iter_words(source) try: yield words, source finally: ...
Python 15:异常在哪一层转成失败结果
空任务类型在提交时被拒绝,任务还没有 ID;另一项合法任务在计算中故障,已经拥有 ID,最后状态应是 failed 而不是 succeeded。只用一层 except Exception 给两种情况返回“任务失败”,会抹掉有无任务记录、错误发生位置和诊断上下文之间的差异。 tasklab 保持 14 已确定的任务状态入口:输入校验发生在提交前,执行失败才在该任务中记录错误。这里的故障由受信任的实验参数触发,不向任务文本开放任意代码执行。 提交失败与执行失败不共用一个终态 submit(store, "", "invalid") 返回的是 ValueError,任务表仍只有前面已经提交的合法任务,它仍是 queued。在此时写一个 failed 任务,等于给未经接受的输入发了任务 ID,还会破坏先前的“错误输入不推进编号”不变量。 另一个合法任务 ID task-0002 被送进 run(..., fail_at="compute")。受控失败先抛出由 ValueError 引起的 RuntimeError("...
Python 14:鸭子类型依赖怎样的协议
词频结果 {"one": 2, "two": 1} 可以回答两个不同的长度问题:有多少种词,和总共出现多少次词。让 len(result) 返回 2、result.total 返回 3,就必须把各自的协议含义说清楚;仅因为都叫“长度”,不能把计数之和塞进 __len__ 而不告知调用方。 13 的数据类现在代表了词频结果,业务状态仍由 Task 持有。本章只给已有结果加最小 __len__ 和受控的只读属性;没有通过一套神奇运算符隐藏任务提交、取消和查询。 len 会找哪个方法 12345678910@dataclassclass TaskResult: counts: dict[str, int] = field(default_factory=dict) def __len__(self): return len(self.counts) @property def total(self): return sum(self.counts.values()) 实测 ...
Python 13:dataclass 能保证哪些不变性
@dataclass(frozen=True) 拒绝给字段重新赋值,却不阻止修改字段里保存的列表。一个测试先得到 FrozenInstanceError,紧接着又成功把字符串追加进同一个对象的 labels。这两项输出不矛盾:冻结约束的是字段赋值入口,不是递归冻结对象图。 在 12 的任务实例基础上,tasklab 把任务定义与词频结果提炼为两种 dataclass,状态迁移仍由原有 Task 和 run 负责。外部 query 继续返回同一形状的快照,旧实验无需改调用方式。 自动生成方法并不自动定义所有权 123456789@dataclass(frozen=True)class TaskDefinition: kind: str text: str labels: tuple[str, ...] = ()@dataclassclass TaskResult: counts: dict[str, int] = field(default_factory=dict) 两个内容相同的 TaskDefinition 值比较相等,但可以是两个不同的对象;自动生...
Python 12:方法调用为何会多出 self
同一进程里,第一个任务已经成功,第二个还在队列中。若把任务状态放在类级别,两项就可能意外共用;若每项拥有自己的实例,执行第一项不应改动第二项。self 不是调用方手动输入的额外任务 ID,而是经实例访问函数时被绑定的那个实例。 前面几章的业务任务暂存在字典中,10 的分批词频没有改变状态管理。本章将任务记录提炼为内部 Task 类,公开的 submit/run/query/cancel 及查询快照仍维持原来的业务接口。 数据从公共字典进入每个实例 Task.__init__(self, kind, text, metadata) 给各实例建立独立的 state、result 和标签列表;Task.snapshot(self) 返回给调用方仍是字典。实际创建两项后,第一项 run 到 succeeded,第二项还是 queued;第一项标签为 ["first"],第二项是 ["second"]。这既检查类的实现,也检查替换内部表示后旧调用约定没有变。 Python 3.12 教程的方法对象一节区分了类里的函数与经实例访问得到的绑定方法。对...
Python 11:装饰器改变了哪个调用对象
给任务执行入口加计时后,成功结果没有变,未知任务 ID 仍抛 KeyError,却多出两条记录:一次成功、一次失败。这比“看到耗时数字”更重要。包装层若吞掉异常、重复执行失败函数,或者令调用方再也看不到原有签名,计时功能就改变了任务合同。 在 06 的闭包基础上,tasklab.metrics.timed 接收一个由应用持有的记录列表,返回装饰函数;真正处理词频的仍是同一个 run,装饰器不重试也不替业务选择终态。 外部调用拿到的是包装后的函数 123456789101112131415def timed(records): def decorate(function): @wraps(function) def wrapper(*args, **kwargs): started = perf_counter_ns() succeeded = False try: result = function(*args, **kwargs) ...
Python 10:惰性管道在哪里积累成本
词元改成生成器后,还要过滤停用词并分批消费。把数据分成两块,第一块里正好有两个词;取出第一批时如果第二块也被读取,管道就有了不必要的提前工作。只看最终词频相同,无法发现上游是否被额外消费。 09 用读取块记录验证过第一次 next 的范围;本章继续在 tasklab 上组合过滤与批处理,同时保持未过滤任务的词频业务合同。 先知道过滤针对谁 word_batches(chunks, skip=(), size=64) 将 iter_words(chunks) 产生的词元交给生成器表达式筛选,再用 Python 3.12 标准库 itertools.batched 组成固定容量的元组批次。核心路径是: 123skipped = frozenset(skip)words = (word for word in iter_words(chunks) if word not in skipped)return batched(words, size) 过滤条件作用于单个词元,不是整份任务结果;skip={"skip"} 时得到 "one&...
Python 09:yield 如何保存未完成的计算
词频任务收到三段合成文本,调用 iter_words(chunks()) 后,上游读入记录仍为空;取出第一个词后,记录里只有第一段 "One "。代码不是跳过了后两段,生成器只是停在第一个 yield,直到下一次请求才继续执行剩下的计算。 08 的任务集合已经区分可迭代对象与一次遍历。09 用同一个 tasklab 的 word_count 负载替换一次性 text.lower().split():词元从一个按需产出的生成器进入计数循环,结果合同保持不变。 建立对象时不等于读取输入 123456789101112def iter_words(chunks): letters = [] for chunk in chunks: for character in chunk: if character.isspace(): if letters: yield "".join(letters).lower() ...
Python 08:for 循环向对象请求了什么
同一份任务 ID 集合第一次遍历得到三个 ID,第二次遍历仍能得到三个;把第一次生成的迭代器留在手里,再遍历一次却得到空列表。数据没有被删除。两次操作只是取迭代器的方式不同:可迭代对象能提供新遍历,已经耗尽的那个迭代器不会自动倒回起点。 前一章把四种业务操作统一分派。这里仍使用同一进程的 tasklab,为任务列表增加读取 ID 的接口;不把遍历后的列表当成自动更新的数据库视图。 集合与一次遍历不是同一个东西 tasklab.task_ids(store) 返回当时任务 ID 组成的元组。元组可以多次交给 iter;每次获得独立的迭代器,逐次向它调用 next,直到 StopIteration。Python 3.12 的迭代器教程展示了 for 如何向对象请求迭代器,以及迭代器怎样逐项提供值。 123456collection = task_ids(store)iterator = iter(collection)first = next(iterator)rest = list(iterator)exhausted = list(iterator)again = list(...










