Iterable:一个容易被忽视的Python编码细节
Type hints
近年来,越来越多的 Python 开发者愿意为变量声明类型了,变化非常明显。
def add(left, right):
return left + right
from typing import TypeVar, Union
T = TypeVar('T', int, float)
def add_typed(left: T, right: T) -> T:
return left + right
虽然 type hints 并不会在运行时进行类型检查,但它们足以让 IDE 在运行前就报出不少类型风险,也让阅读代码的人有了更多思考空间,不至于迷失在“这到底是什么类型”的疑惑中。
Iterator 和 Iterable 的问题
Python 对 Iterator 的要求只有一条:
An iterator object implements
__next__, which is expected to return the next element of the iterable object that returned it, and to raise aStopIterationexception when no more elements are available.一个迭代器对象需要实现
__next__方法,该方法应当返回其所属可迭代对象的下一个元素;当没有更多元素可返回时,应抛出StopIteration异常。-- Python Wiki
Python 对 Iterable 的要求也很简单:
An iterable object is an object that implements
__iter__, which is expected to return an iterator object.一个可迭代对象是实现了
__iter__方法的对象,该方法应当返回一个迭代器对象。-- Python Wiki
这两年我还写了不少 Rust。在 Rust 里,Python 的 Iterator 对应 std::iter::Iterator,Iterable 则对应 std::iter::IntoIterator。正是有了写 Rust 的经验,我才能一眼看出下面代码的问题:
def stream(
self,
*,
# ...
tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
# ...
) -> AsyncChatCompletionStreamManager[ResponseFormatT]:
# ...
_validate_input_tools(tools)
# ...
api_request = self.create(
...
tools=tools,
...
)
return AsyncChatCompletionStreamManager(
...
input_tools=tools,
)
这段代码节选自 OpenAI 的 Python 接口 completions.py。问题出现在 #1129 这个 PR。
作为 Rust 选手的直觉告诉我,一个 Iterable 在这里被用到了三次,这就要求这个 Iterable 在多次迭代时本身状态不可变,否则无法保证三次迭代的结果一致。
通俗点说:这个 Iterable 必须保证多次调用 __iter__ 返回的迭代器,其迭代结果是一致的。
验证一下。对于普通容器当然没问题,但对于生成器就大有问题。生成器作为迭代器,其状态是会变的,所以三次调用的结果无法保证一致。
from typing import Iterable, List
def generate_from_list(lst: List[int]):
for item in lst:
yield item
# 容器可以多次迭代,产生相同结果
iterable0 = [1 ,2 ,3]
assert list(iterable0) == list(iterable0) == [1, 2, 3]
# 生成器迭代器只能迭代一次
iterable1 = generate_from_list([1, 2, 3])
assert list(iterable1) == [1, 2, 3]
assert list(iterable1) == []
# 容器外套一层map并不能保持容器可以多次迭代的特征,只能迭代一次
iterable2 = map(lambda x: x + 1, [1, 2, 3])
assert list(iterable2) == [2, 3, 4]
assert list(iterable2) == []
# 生成器额迭代器外套一层map自不必多说,只能迭代一次
iterable3 = map(lambda x: x + 1, generate_from_list([1, 2, 3]))
assert list(iterable3) == [2, 3, 4]
assert list(iterable3) == []
运行结果:
反思
我不反思,我一眼就看出这个问题了,我为啥要反思。但需要反思的人还真不少。
写代码时一定要清楚,自己进行的每一个操作对前置操作有什么依赖,是否修改了某个状态,这个状态是否应该由这个操作修改。如果不确定,就必须按照最保守的策略来。
还是以 Iterable 为例,拿到一个可迭代对象后,如果不确定它是不是容器、能否多次迭代且结果一致,那就只允许自己对它做一次迭代。如果因为种种原因不得不多次迭代,那就把它转成 list,再在这个 list 上反复迭代。
#1129 的错误略有不同,它不是在写新代码时出的问题,而是在放宽已有代码的限制时,只考虑了容器的情况,没考虑到其他典型可迭代对象(生成器、map 等)。这个 PR 合并一年多了,似乎还没人踩坑,这恰恰说明大多数场景下传入的就是容器(List),原 PR 放宽约束意义不大,反而引入了潜在风险。
#1606 的错误则更直接。一个 validate_input_tools 函数本不该对输入数据做任何更改,但根据刚才的讨论,这里有潜在的对 Iterable 状态的修改,所以这个函数的类型标注就应该只接受容器,或者其他能表示不可变可迭代对象的类型。
总结
总之,Iterable 虽然是 Python 类型标注中常见的一个概念,但它的“可多次迭代且结果一致”这一点很容易被忽视。写代码时,尤其是在类型标注、接口设计和数据处理时,一定要明确区分“容器型可迭代对象”和“一次性可迭代对象”,避免踩坑。遇到不确定的情况,最保险的做法就是把它转成 list,这样既保证了多次迭代的一致性,也让代码的行为更加可控和易于理解。
(感觉这个还是不要做面试题了)
Iterable:一个容易被忽视的Python编码细节的更多相关文章
- 探索 Python、机器学习和 NLTK 库 开发一个应用程序,使用 Python、NLTK 和机器学习对 RSS 提要进行分类
挑战:使用机器学习对 RSS 提要进行分类 最近,我接到一项任务,要求为客户创建一个 RSS 提要分类子系统.目标是读取几十个甚至几百个 RSS 提要,将它们的许多文章自动分类到几十个预定义的主题领域 ...
- (转)PEP 8——Python编码风格指南
PEP 8——Python编码风格指南标签(空格分隔): Python PEP8 编码规范原文:https://lizhe2004.gitbooks.io/code-style-guideline-c ...
- 小学生都能学会的python(编码 and 字符串)
小学生都能学会的python(编码 and 字符串) 一,编码 最早的计算机编码是ASCII. 有英文+数字+特殊字符 8bit => 1byte 没有中文, 后面的编码必须兼容ASCII ...
- 一篇夯实一个知识点系列--python生成
写在前面 本系列目的:一篇文章,不求鞭辟入里,但使得心应手. 迭代是数据处理的基石,在扫描内存无法装载的数据集时,我们需要一种惰性获取数据的能力(即一次获取一部分数据到内存).在Python中,具有这 ...
- (转载) 浅谈python编码处理
最近业务中需要用 Python 写一些脚本.尽管脚本的交互只是命令行 + 日志输出,但是为了让界面友好些,我还是决定用中文输出日志信息. 很快,我就遇到了异常: UnicodeEncodeError: ...
- Python 编码简单说
先说说什么是编码. 编码(encoding)就是把一个字符映射到计算机底层使用的二进制码.编码方案(encoding scheme)规定了字符串是如何编码的. python编码,其实就是对python ...
- Python之路3【知识点】白话Python编码和文件操作
Python文件头部模板 先说个小知识点:如何在创建文件的时候自动添加文件的头部信息! 通过:file--settings 每次都通过file--setings打开设置页面太麻烦了!可以通过:View ...
- python编码规范
python编码规范 文件及目录规范 文件保存为 utf-8 格式. 程序首行必须为编码声明:# -*- coding:utf-8 -*- 文件名全部小写. 代码风格 空格 设置用空格符替换TAB符. ...
- Python编码问题整理
认识常见编码 GB2312是中国规定的汉字编码,也可以说是简体中文的字符集编码 GBK 是 GB2312的扩展 ,除了兼容GB2312外,它还能显示繁体中文,还有日文的假名 cp936:中文本地系统是 ...
- 【转】python编码规范
http://blog.csdn.net/willhuo/article/details/49300441 决定开始Python之路了,利用业余时间,争取更深入学习Python.编程语言不是艺术,而是 ...
随机推荐
- [笔记]PHP里类的申明和对象的实例化(笔记)
1.申明类 class 类名{ //属性---------- public 属性 = 值: public 属性: //方法----------- function 方法名($n1,$n2){ ...
- 【记录】SMB|Windows下修改SMB端口并挂载
环境:Window11 使用背景:勒索病毒导致445端口不安全,故而该端口在服务器端被全面禁用了,如需使用SMB服务需要换个SMB服务端口. 方法1:端口转发 win+x点开管理员权限的终端: 输入如 ...
- ASP.NET Core 之路由相关
ASP.NET Core中路由的过程:routing middleware把传入的url与一系列模板进行比对,选择相应的endpoint handler,并将其记录在HttpContext上的requ ...
- C# 控制台程序验证await立即返回
class Program{ public static volatile bool flag = true; public static void Main() { Action a = null; ...
- 由 Array.includes 函数引发对引用数据类型的思考
`` 数组的includes方法在日常的编程中比较常用到,其作用就是判断某一数据是否在数组中,通常来说,数组中的数据如果是数字,布尔值,或者字符串的话,都是能够进行判断的 例如: [1,2,3,4]. ...
- Spring Boot 整合 JMS(Active MQ 实现)
我们使用jms一般是使用spring-jms和activemq相结合,通过spring Boot为我们配置好的JmsTemplate发送消息到指定的目的地Destination.本文以点到 ...
- Spring Boot 整合 ActiveMQ 实现手动确认和重发消息
消息队列中间件是分布式系统中重要的组件,已经逐渐成为企业系统内部通信的核心手段.主要功能包括松耦合.异步消息.流量削锋.可靠投递.广播.流量控制.最终一致性等.实现高性能,高可用,可伸缩和最终一致性架 ...
- Spring扩展接口-ApplicationContextInitializer
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...
- IPMI新建BMC管理用户
# 查看ipmi的ip [root@HOST-10-198-2-62 ~]# ipmitool lan print # 首先确认非admin用户的id,选择一个ID创建root用户 [root@HOS ...
- window10本地搭建DeepSeek R1(三) 'NoneType' object has no attribute 'encode'
上面两章介绍了在本地安装DeepSeek+OpenWebUI.这里介绍一下几个需要注意的地方. 1:文件上传失败,上传文件是报错:python "'NoneType' object has ...