python functools
# Python itertools:一个被低估的标准库模块
这些年写了那么多Python代码,回头想想,itertools这个模块就像厨房里的盐——用得好了,菜的味道立刻不一样;但很多人要么不知道它的存在,要么只是偶尔用一下groupby,然后就把它扔回抽屉里了。
它到底是什么
itertools是Python标准库里的一个模块,专门处理迭代相关的操作。说白了,它就是一套对迭代器进行组合、变换、筛选的工具函数集合。和那些需要导入的第三方库不同,itertools从一开始就装在Python环境里,随用随取。
不过要理解itertools的真正价值,得先明白一件事:Python里那些用for循环遍历列表、元组、字典的操作,本质上都是在处理迭代器。itertools提供的函数,能让你不用手动写循环、不用创建中间列表,就把这些迭代操作完成得又快又优雅。
它能做什么
用一个实际的例子来说明。假设我在处理一个电商网站的日志数据,需要找出连续三天购买记录超过100元的用户。如果用常规做法,可能要写几个嵌套循环,一大堆if判断。用itertools的话:
fromitertoolsimportgroupby,teedeffind_loyal_customers(purchases):# 按用户分组,按日期排序sorted_purchases=sorted(purchases,key=lambdax:(x['user_id'],x['date']))foruser_id,user_groupingroupby(sorted_purchases,key=lambdax:x['user_id']):# 检测连续三天的购买金额是否都超过100amounts=[p['amount']forpinuser_group]iflen(amounts)>=3:# 这里用到了itertools的另一个工具foriinrange(len(amounts)-2):ifall(amt>100foramtinamounts[i:i+3]):yielduser_idbreak当然这个例子还可以优化,但重点在于itertools的groupby让我们从按用户分组这个烦人的手动操作中解脱出来。
itertools干的活大致可以分成几类:
- 组合和排列:比如要生成所有可能的密码组合,或者比赛的对阵表
- 无限迭代器:像count、cycle、repeat,适合做轮询、生成ID这些场景
- 迭代器筛选:像takewhile、dropwhile、filterfalse,能按条件快速筛选数据流
- 迭代器组合:像chain、zip_longest,能同时处理多个数据源
怎么使用
用itertools有一点很重要:它的函数返回的都是迭代器,不是列表。这意味着数据是懒加载的——只有你在循环里用它的时候,才真正计算出来。
拿cycle举个例子。以前我写一个轮流分配任务的脚本时,是这样写的:
fromitertoolsimportcycle team_members=['张三','李四','王五']tasks=['任务A','任务B','任务C','任务D','任务E','任务F']# cycle会无限循环这个列表assignments=zip(cycle(team_members),tasks)formember,taskinassignments:print(f'{member}负责{task}')输出:
张三负责任务A 李四负责任务B 王五负责任务C 张三负责任务D 李四负责任务E 王五负责任务F再看一个实际业务场景。某个系统需要每隔5分钟检查一次服务器状态,但又不想在重启系统时立刻检查(可能刚启动不稳定)。这时候可以用islice跳过前几个:
fromitertoolsimportcycle,islice health_checks=cycle(['normal','warning','error','normal'])# 跳过前两次检查forstatusinislice(health_checks,2,None):ifstatus=='error':alert_ops()最佳实践
说几个这些年用itertools攒下的经验。
第一,用chain代替多层循环。有时候需要顺序遍历几个列表,有人会写:
forsublistin[list1,list2,list3]:foriteminsublist:process(item)改成chain之后:
fromitertoolsimportchainforiteminchain(list1,list2,list3):process(item)代码更扁平,可读性也更好。
第二,深刻理解“懒加载”。itertools的威力很大一部分在于它不占内存。如果数据量很大(比如几百万行日志),用itertools处理比先构建列表再处理要高效得多。这是很多人容易忽视的——他们习惯了先装到一个列表里再说,结果内存飙上去还不知道为什么。
第三,善于组合itertools的函数。itertools的函数可以互相嵌套使用。比如要生成斐波那契数列的前10项,可以用accumulate配合一个简单的lambda:
fromitertoolsimportaccumulate fibo=[0,1]fibonacci=accumulate(range(2,10),lambdax,y:x+fibo[-1])forfinfibo:yieldfforfinfibonacci:yieldf这种方式比递归更高效,比显式循环更简洁。
第四,groupby使用前一定要排序。这是个常犯的错误——groupby只对连续相同的键进行分组,不排序的话,相同键的数据分散在不同的位置,就分不成一组了。
和同类技术对比
在Python生态里,处理迭代操作的技术主要有三个层次:itertools、函数式编程(map、filter、reduce)、生成器表达式。
itertools vs 生成器表达式:生成器表达式胜在简单,一行就能搞定一个操作。但当你需要做更复杂的操作时(比如组合、分组、无限迭代),生成器表达式就不够用了。itertools提供了这些场景的现成方案,不需要自己从头写生成器。
itertools vs map/filter:作为函数式编程的老牌工具,map和filter在简单的变换场景下足够好用。但它们有个问题:代码读起来不太直观。特别是map和filter嵌套使用时,可读性直线下降。相比之下,itertools的函数命名更直白,chain就是拼接,compress就是筛选,看名字就知道干什么。
如果硬要说itertools的缺点:它的函数不能直接处理那些自定义的、有状态的复杂迭代逻辑。比如需要维护一个计数器、需要根据历史数据做决策,这种时候还是得老老实实写生成器函数。但话说回来,itertools也没打算覆盖所有场景,它解决的是那些“模式化”的迭代需求。
选型建议:如果是简单的映射或过滤,用生成器表达式或列表推导式。如果涉及组合、排列、无限序列、分组、链式操作这些模式化的需求,先# # Python functools 模块:一个被低估的工具箱
从一次代码重构说起
前段时间接手了一个同事留下的项目,里面有个缓存逻辑写得相当复杂。他用的是字典加时间戳的手动实现,代码差不多有五十行,还涉及到锁的问题。我当时就想,这其实用functools.lru_cache两行就能搞定的事情,为什么非得自己折腾?但转念一想,很多人确实不太了解functools到底能做什么,甚至有人觉得它就是个reduce函数的老家。
它到底是什么
functools是Python标准库里的一个模块,专门为高阶函数(也就是操作或返回函数的函数)提供支持。可以理解成是Python函数式编程的工具包。它不是那种炫技的东西,而是用来解决实际问题的基础工具。就像你工具箱里的螺丝刀,平时可能不觉得它多重要,但真正需要拧螺丝的时候,你会感谢它的存在。
这个模块的核心思想其实很简单:既然Python里函数是一等公民,那为什么不能有专门处理函数的函数?functools就是干这个的。
它能做什么
说几个常见的场景你就明白了:
假设你有个函数要计算斐波那契数列,如果不用任何优化,一个n=40的调用就要等上几秒钟。用上lru_cache之后,计算速度会快到让你怀疑人生。这其实就是最典型的记忆化缓存。
再比如,你写了一个带很多参数的函数,但每次调用的时候,大部分参数都是固定的。反复写那些重复的参数很烦人,而且容易出错。这时候partial就能派上用场,有点像预配置。
还有种情况:你想把一个类的方法变成只读属性,或者想把一个函数的行为包装一下,比如加个日志、算个耗时。这时候就需要用wraps来确保装饰器不会丢失原函数的信息。
你说这些都是小功能?但正是这些小功能,能让代码变得更简洁、更可靠。
怎么用好它们
lru_cache的妙用
fromfunctoolsimportlru_cache@lru_cache(maxsize=128)defget_full_name(first,last):returnf"{first}{last}"这里有个容易被忽略的点:lru_cache的参数必须是可哈希的。如果你传了一个列表进去,它会直接抛异常。所以涉及到可变参数的时候,要么转化成元组,要么自己实现缓存逻辑。
maxsize也很讲究。设成None是不限制大小,但内存可能会被撑爆。一般128或者256就够用了,如果确实需要无限缓存,可以用functools.cache(Python 3.9+)。
partial的使用场景
fromfunctoolsimportpartialdefsend_email(recipient,subject,body,cc=None,bcc=None):...notify_admin=partial(send_email,recipient="admin@example.com",subject="System Alert")partial最容易被滥用的地方是传递self。很多人喜欢在类里用partial来绑定方法,但其实用lambda或者methodcaller更清晰。partial最适合的场景是API的一致性处理,比如你的函数签名有10个参数,但绝大多数调用只需要改其中一两个。
wraps不可或缺
fromfunctoolsimportwrapsdeflog_call(func):@wraps(func)defwrapper(*args,**kwargs):print(f"Calling{func.__name__}")returnfunc(*args,**kwargs)returnwrapper如果你写过装饰器,应该遇到过这种情况:被装饰的函数,它的__name__和__doc__都变成了wrapper的。wraps就是用来解决这个问题的。它是update_wrapper的语法糖,会把原函数的元信息复制过来。
single_dispatch的妙用
这个功能比较冷门,但非常有趣。它让你可以根据第一个参数的类型来分发不同的实现:
fromfunctoolsimportsingledispatch@singledispatchdefprocess_data(data):raiseNotImplementedError("Unsupported type")@process_data.register(list)def_(data):return[x*2forxindata]@process_data.register(dict)def_(data):return{k:v*2fork,vindata.items()}这在处理多态数据时特别好用,比写一堆if isinstance干净得多。
最佳实践
说几条摸爬滚打出来的经验:
缓存不要滥用。
lru_cache确实方便,但要注意两点:一是参数变化频繁的场景不适合,缓存命中率低反而增加开销;二是缓存大的对象要注意内存,Python的垃圾回收是引用计数为主的,缓存会让对象一直活着。partial的参数顺序要小心。partial是从前往后填充参数的,如果你中间有默认参数,可能会导致意想不到的结果。建议在partial前先确认一下函数签名。装饰器层级不要太多。一个函数套五六个装饰器,调试的时候会很痛苦。尽量把装饰器的逻辑拆分到不同的职责上,保持每个装饰器简单明确。
reduce其实不常用。很多人知道functools.reduce,但实际项目中用得很少。Python的for循环可读性更好,除非你在做函数式编程的练习。避免过度抽象。有时候用
partial、wraps能让代码少几行,但如果同事看不懂,那就得不偿失。代码首先是给人读的。
和同类技术的对比
缓存方面:lru_cachevsredis/memcached
lru_cache是进程内的,重启就丢了。如果需要跨进程共享缓存,就需要redis或者memcached。但lru_cache胜在零配置、零延迟,适合单个进程内部的重复计算。
也有第三方的cachetools库,提供了更丰富的缓存策略,比如TTL(过期时间)、LFU(最少使用)等。但如果是简单的需求,标准库的lru_cache足够用。
装饰器工具:wrapsvsdecorator库
wraps足够99%的场景使用。但有个第三方库decorator,它不仅能拷贝元信息,还能让被装饰的函数保持原来的签名(对IDE友好)。如果你写的是对外发布的库,可以考虑用decorator。
部分应用:partialvslambda
partial和lambda都能创建偏函数。区别在于:partial会保留原函数的元信息(经过__name__改造),而且执行效率略高。但lambda更灵活,可以处理复杂的表达式。个人倾向于:固定参数用partial,需要计算的时候用lambda。
覆盖__builtins__的场景
functools里有个cmp_to_key,是用来把老式的比较函数转成key函数的。现在基本用不上了,因为Python 3里已经移除了cmp参数,而且key函数更强大。但如果你维护的是老代码,这个函数能帮你平滑迁移。
写在最后
functools是Python标准库里被低估的模块之一。它的每个函数都不复杂,但组合起来能解决很多实际问题。下次你写重写一个缓存逻辑的时候,或者打算给函数加日志但又怕丢失元信息的时候,不妨先想想functools里是不是有现成的工具。
毕竟,代码越少,出错的机会就越少。用itertools想想有没有现成的函数。如果业务逻辑特别定制化,才考虑写自己的生成器函数。
就像开头说的,itertools就像是厨房里的基础调料,平时不起眼,但真正会用的人,能让代码少写不少废话,性能也上去一截。关键是要多看、多用、多想——遇到某种迭代需求,先想想itertools有没有对应的函数,再去纠结要不要自己造轮子。
