ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python内置函数精髓:set/sorted/setattr等8个函数实战详解

Python内置函数精髓:set/sorted/setattr等8个函数实战详解 写 Python 这些年我越来越发现一个很反直觉的现象很多人能把各种第三方框架玩得飞起但回到语言本身的set()、setattr()、slice()、sorted()、staticmethod、str()、sum()、super()这些内置函数反而讲不清楚。这几个函数看着毫无关联实际在真实项目里经常结伴出现。比如你写一个数据清洗脚本大概率同时用到set()去重和sorted()排序写一个 ORM 框架或者动态配置模块肯定绕不开setattr()设计多继承的类体系时super()跑不掉对外输出结果时str()整天在做隐性转换。所以我一直觉得与其零散地查文档不如把这几个函数放一起系统过一遍。这篇文章适合刚入门想打牢基础的人也适合写了两三年代码但总在某些边角细节上卡壳的朋友。我会结合真实踩过的坑来讲尽量把每个函数背后的设计意图说透最后再给一个融合所有函数的实战案例。1. 为什么把这8个内置函数放在一起讲1.1 从真实工作场景说起先聊个我经常在代码评审里看到的场景。有个同事做运营数据处理要对一批用户标签做清洗他写出来的代码是这样的tags [vip, new, vip, old, new, black] unique_tags [] for tag in tags: if tag not in unique_tags: unique_tags.append(tag) unique_tags.sort() print(unique_tags)这段代码功能没错但如果你知道set()和sorted()三行就能搞定tags [vip, new, vip, old, new, black] unique_tags sorted(set(tags)) print(unique_tags)这就是我为什么把这几个函数放在一起讲。它们看起来功能各不相同却都是支撑 Python 日常编码高频率操作的基础件。一个内置函数掌握得越扎实你写的代码就越短、越稳、越不容易带 bug。很多所谓Pythonic的写法其实就是对内置函数的理解和运用到位了。1.2 这些函数的共同点与区别这8个函数/关键字可以粗分成四类分类函数典型用途数据容器类set()、slice()构造集合、定义切片对象操作与计算类sorted()、sum()、str()排序、求和、字符串化对象与属性类setattr()动态设置对象属性类与继承类staticmethod、super()定义静态方法、协调继承调用很多人会把set()理解成去重工具把sum()理解成加法工具这没错但太浅了。我后面会逐个展开说明每个函数在真实项目中的纵深场景。先说一个通用概念内置函数是 C 语言实现的性能比纯 Python 手写循环高一个量级。在数据量上万之后你在 Python 层写的for循环和内置函数的差距会非常明显。所以能拍板用内置函数的地方就别自己造轮子。2. set()不只是去重而已2.1 集合的基本玩法set()的核心是构建一个无序、不重复的元素集合。我见过不少教程只会讲set([1, 2, 2, 3])得到{1, 2, 3}但实际工作中集合最值钱的能力是集合运算。我有一次做用户画像项目需要筛出在A活动参与过、且在B活动也参与过的用户同时排除C活动里的黑名单用户。SQL 写起来一串 JOIN但如果你已经把三个名单拉到了 Python 里用集合运算就是几条语句a_users {u1, u2, u3, u4} b_users {u3, u4, u5} c_blacklist {u2, u5} intersection a_users b_users # 交集同时在A和B valid intersection - c_blacklist # 差集排除黑名单 all_participated a_users | b_users # 并集参与过任一活动这套运算在数据处理、日志分析、权限判断等场景里非常实用。对应的运算符分别是交集、|并集、-差集、^对称差集。还有一组等价的方法intersection()、union()、difference()、symmetric_difference()用运算符还是方法看可读性需求。2.2 集合运算在数据清洗中的应用我处理 CSV 数据去重时特别爱用set()做链式去重 交叉比对。举个例子你要把 Excel 里两列数据做对比找出只在第一列出现的值col1 [apple, banana, cherry, apple] col2 [banana, durian] only_in_col1 set(col1) - set(col2) print(only_in_col1) # {cherry, apple}这里有个细节值得注意set(col1)本身就把col1里的重复值的apple去掉了所以only_in_col1里的apple不会重复出现。如果你希望保留原始的条数信息那就要换成collections.Counter但这是后话。2.3 set()的坑与注意事项用set()有几个人尽皆知、但新手必踩的坑我列成清单集合元素必须可哈希数字、字符串、元组可以放进去列表和字典不行。如果你确实需要去重列表的列表得先把内层列表转成元组比如set(tuple(x) for x in list_of_lists)。迭代顺序不固定虽然 CPython 底层实现里字符串集合经常表现为看起来有顺序但这是实现细节不要依赖集合的迭代顺序。需要稳定顺序就必须sorted(set(...))。空集合要用set()而不是{}{}创建的是空字典这是出镜率最高的初始化错误。性能判断元素是否在集合中平均时间复杂度 O(1)判断是否在列表中最坏 O(n)。几万条数据的时候差异就非常明显了所以去重 成员判断优先用集合。实测下来用set做百万级成员判断比list快几十倍内存代价也值得。3. setattr()与动态属性从对象到元编程3.1 setattr()的基本用法setattr(object, name, value)就是给对象动态设置属性。很多人第一反应是我直接obj.attr value不就行了为什么要用它关键在于当你连属性名都是变量时点语法就无能为力了。class Config: pass config Config() key database_url setattr(config, key, mysql://localhost:3306/app) print(config.database_url) # mysql://localhost:3306/app如果属性名来自外部输入、配置文件或者循环遍历setattr()就是你唯一的通用入口。与之对应的读取函数是getattr()删除属性用delattr()判断有没有用hasattr()。这四个函数组合起来就是 Python 动态属性操作的四件套。3.2 动态配置与ORM映射的实际场景我写过一个小型配置加载器需求是从 YAML 文件读取若干配置项并把它们挂到配置对象上作为属性。如果手写一堆硬编码赋值配置项一多就没法维护所以直接做通用映射import yaml class Settings: pass def load_settings(yaml_path): settings Settings() with open(yaml_path, r, encodingutf-8) as f: data yaml.safe_load(f) for key, value in data.items(): setattr(settings, key, value) return settings settings load_settings(config.yaml) print(settings.timeout) # 假设YAML里有 timeout 字段这种写法在第三方库中也是标配。比如很多 ORM 在把数据库行映射成对象时就是用setattr(obj, column_name, row_value)实现的。你手动定义一个 Model然后框架自动帮你往里塞属性背后全是setattr()的功劳。理解了这个读源码时就不会一头雾水。3.3 使用 setattr的安全边界setattr()很强大但正因为太灵活有几条经验值得记住不要无脑接受外部输入做属性名。如果允许用户传__class__、__dict__这种特殊属性名可能引发意外行为。我在做 Web 接口时会先对属性名做白名单校验或者用getattr(obj, name, None)去读取时也主动过滤。配合__slots__时要注意。如果一个类定义了__slots__ (name, age)那么setattr(obj, other, 1)会直接抛AttributeError。这不是 bug是设计__slots__本来就是要限制属性范围。面向对象设计上能显式定义就不用动态设置。setattr()适合做框架级、通用型的逻辑但业务代码里到处动态造属性会让代码的意图变得模糊IDE 也无法补全和类型检查。我个人的原则是动态属性适合架构层业务层尽量少用。读写一段代码5 秒内看不出属性从哪来的那多半就是动态属性用过头了。4. slice()与sorted()数据切片的进阶组合4.1 slice对象到底解决了什么Python 的切片表达式大家都会写lst[1:5:2]。但slice()这个内置函数却经常被人忽略。它的作用就是把切片参数打包成一个可复用的对象。s slice(1, 5, 2) data [10, 20, 30, 40, 50, 60, 70] print(data[s]) # [20, 40]为什么需要这种对象我在处理批量数据时经常要对多个数组或列表执行同一段切片逻辑。如果每次写裸切片参数散落在各处很容易改漏定义一个slice对象语义清晰改动也集中middle slice(2, -2) # 去掉首尾两个元素 all_rows [get_row(i) for i in range(100)] middle_rows all_rows[middle]还有一个容易忽略的用法slice对象可以用在自定义类的索引逻辑里。当你写一个支持切片的容器类时__getitem__里收到的参数就是一个slice对象需要解析它的start、stop、step。class MyList: def __init__(self, items): self.items items def __getitem__(self, index): if isinstance(index, slice): return self.items[index.start:index.stop:index.step] return self.items[index]4.2 sorted()的稳定排序与keysorted()是我最常推荐的内置函数比list.sort()稳它返回一个新列表不改原数据语义上更安全。真正拉开差距的是它的key参数。students [ {name: Alice, score: 88}, {name: Bob, score: 72}, {name: Carol, score: 95}, ] rank sorted(students, keylambda s: s[score], reverseTrue) print(rank)这里有两个关键点key函数是在比较之前先执行一次映射实际上用的是schwartzian_transform装饰-排序-取消装饰的思路。数据量大时key函数只被调用 n 次不是 n log n 次所以不会成为性能瓶颈。sorted()是稳定排序。也就是说当key相同时元素保持原来的相对顺序。这非常重要因为你可以借此实现多级排序先按次要条件排再按主要条件排结果依然是主要条件优先、次要条件保持相对顺序。比如按总分排序总分相同再看语文成绩data [ {name: A, total: 200, chinese: 90}, {name: B, total: 200, chinese: 85}, {name: C, total: 195, chinese: 98}, ] # 先按语文排再按总分排稳定排序保证总分相同的时候语文相对顺序保留 data.sort(keylambda x: x[chinese], reverseTrue) data.sort(keylambda x: x[total], reverseTrue)有的同学在这里会问为什么第二次排序不会破坏第一次的结果?这正是稳定排序的承诺。大多数排序场景用sorted()配合key和reverse就完全够用并不需要写复杂的比较函数。4.3 用slicesorted组合处理实际问题我印象很深的一个需求是截取列表中间某部分并对这部分排序且不影响原列表。如果不用slice对象代码会很啰嗦original [5, 3, 8, 1, 9, 2, 7] # 取索引2到5的一段排序后再组装回去 segment slice(2, 5) middle sorted(original[segment]) result original[:segment.start] middle original[segment.stop:] print(result) # [5, 3, 1, 2, 8, 9, 7]这段代码虽然不复杂但把slice的start和stop与索引拼接结合起来就是一个很典型的参数集中管理思维。当你切片逻辑要从配置里读取时slice对象的价值就体现出来了。5. staticmethod与super()面向对象设计中的两个特殊角色5.1 staticmethod到底是不是函数装饰器确切说staticmethod是 Python 内置的一个类不是普通函数但最常见的用法是作为装饰器使用。它把一个普通函数包装成静态方法不接收隐式的 self 或 cls 参数调用时用类名或实例都无所谓。class Validator: staticmethod def is_valid_email(email: str) - bool: return in email and . in email.split()[-1] print(Validator.is_valid_email(testexample.com)) # True print(Validator().is_valid_email(bad-email)) # False为什么要用staticmethod而不是直接写模块级函数主要是归属和组织。如果某个函数和类的概念强相关放进类里可以避免污染模块命名空间也让调用方一目了然。5.2 super()的工作原理与MROsuper()可能是这8个对象里最容易被误解的。它不叫父类调用它的真实语义是沿着 MRO方法解析顺序向后找下一个匹配的方法。class A: def greet(self): print(A) class B(A): def greet(self): print(B) super().greet() class C(A): def greet(self): print(C) super().greet() class D(B, C): def greet(self): print(D) super().greet() D().greet()这段代码的输出顺序很多人猜不对D B C A原因就在 MROD - B - C - A - object。super()在 B 里调用时找的不是 B 的父类 A而是 MRO 里的下一个类 C。这就是协作多继承的关键机制。如果 B 里写A.greet()就会打破 MRO 链C 永远不会执行。5.3 在继承体系中正确使用super我踩过的坑是在__init__里用super().__init__()时所有类必须保持一致的参数签名。尤其是在多继承下某一层的__init__不调用super().__init__()整条链就断了。一个可靠的做法是让所有协作类的__init__都接受通用*args, **kwargs并传给super()class Base: def __init__(self, *args, **kwargs): print(Base init) class MixinA(Base): def __init__(self, *args, **kwargs): print(MixinA init) super().__init__(*args, **kwargs) class MixinB(Base): def __init__(self, *args, **kwargs): print(MixinB init) super().__init__(*args, **kwargs) class Concrete(MixinA, MixinB): def __init__(self): print(Concrete init) super().__init__() Concrete()这种协作模式在 Django、Flask 扩展、日志中间件里很常见。记住一句话只要涉及多继承就优先用super()不要手写类名调用。这样 MRO 才能正确调度。6. str()与sum()最常用却也最容易被忽略的细节6.1 str()不只是字符串转换str()看起来简单实际上它定义了对象面向普通用户的展示方式。这里要和repr()做区分repr()是面向开发者/调试的展示str()是面向用户的展示。我做过一个报告导出的小工具自定义类的__str__和__repr__各写各的效果差很远class Report: def __init__(self, title, rows): self.title title self.rows rows def __str__(self): return fReport({self.title!r}, {len(self.rows)} rows) def __repr__(self): return fReport(title{self.title!r}, rows{self.rows!r}) r Report(销售月报, [1, 2, 3]) print(str(r)) # Report(销售月报, 3 rows) print(repr(r)) # Report(title销售月报, rows[1, 2, 3])在用f-string输出、拼接字符串、print()时Python 会自动调用对象的__str__。及时定义它能显著提升日志和报错信息的可读性。另一个值得注意的点str()遇到不可转换的对象时会调用__repr__作为兜底所以如果__str__没定义也不会炸只是显示效果偏开发向。6.2 sum()的start参数与生成器陷阱sum(iterable, start0)的第二个参数start很多人不理解它是干嘛的。它表示累加的初始值默认从 0 开始。对于数字求和这没问题但如果你想对列表求和就会遇到类型错误try: sum([[1, 2], [3, 4]], []) except TypeError as e: print(e) # can only concatenate list (not int) to list这是因为列表不能和整数相加。很多人以为sum([[1,2],[3,4]], [])能展平列表执行后才会发现报错。正确写法是sum([[1, 2], [3, 4]], [])其实在 Python 3 也不行因为列表的只支持列表不支持整数正确思路是使用itertools.chain或列表推导。sum()还有两个值得注意的小细节对生成器求和时它是懒加载的内存占用很友好。sum(x * x for x in range(10))这种写法简洁且不产生中间列表。start参数的类型决定了结果的初始值。用sum(values, start100)可以直接给总和加一个基数配合Decimal或Fraction做精确计算时也很方便。实操中我尽量不让sum()去拼接序列。拼接序列这种操作的时间复杂度是 O(n^2)数据量大时非常慢标准做法是用.join()拼字符串、itertools.chain.from_iterable()展平列表。7. 综合实战把这8个内置函数串起来7.1 需求说明与设计理论知识讲再多都不如一个落地案例让人印象深刻。我设计了一个小场景从多个数据源模拟为列表读取成绩数据去重、清洗、排序、展示汇总同时用类组织逻辑最终输出一个可读的报告字符串。这个案例不复杂但能自然地把set()、setattr()、slice()、sorted()、staticmethod、str()、sum()、super()全部融入进去。看代码前先说明设计思路用staticmethod定义数据清洗入口体现归属感。用set()去重合并。用sorted()按分数排序。用slice()截取排名区间。用sum()计算总分。用super()做子类扩展。用setattr()在运行时给对象追加字段。用str()控制对象展示。7.2 代码实现与逐步拆解class ScoreProcessor: def __init__(self, data_sources): self.data_sources data_sources self.records [] self._threshold 0 staticmethod def clean(raw_items): 静态方法清洗原始数据去除非法记录并去重。 seen set() for item in raw_items: name, score item if not isinstance(score, (int, float)) or score 0: continue key (name, score) if key in seen: continue seen.add(key) yield name, score def load(self): merged [] for source in self.data_sources: merged.extend(self.clean(source)) self.records list(merged) return self def top_n(self, n): 取排序后的前n条这里用到slice对象。 self.records sorted(self.records, keylambda x: x[1], reverseTrue) s slice(0, n) return self.records[s] def total_score(self): return sum(score for _, score in self.records) def __str__(self): if not self.records: return No records top self.top_n(3) lines [f{name}: {score} for name, score in top] return fTop records ; .join(lines) f | total{self.total_score()} class ScoreProcessorWithThreshold(ScoreProcessor): def __init__(self, data_sources, threshold): super().__init__(data_sources) self.threshold threshold def filter_low_score(self): self.records [r for r in self.records if r[1] self.threshold] return self data_sources [ [(Alice, 88), (Bob, 72), (Alice, 88)], [(Carol, 95), (Bob, 72), (David, 66)], [(Eve, 58), (Alice, 88)], ] processor ScoreProcessorWithThreshold(data_sources, threshold60) processor.load().filter_low_score() # 动态追加一个字段平均分 setattr(processor, average_score, processor.total_score() / len(processor.records)) print(processor) print(avg:, processor.average_score)在这个案例里每个函数的角色非常清晰ScoreProcessor.clean是staticmethod不需要 self天然适合做纯函数式的数据清洗。set()在clean里配合seen集合完成去重与非法数据过滤。sorted()在top_n里按分数降序排序。slice(0, n)负责取排名区间。sum()在total_score里对生成器求和简洁且省内存。super().__init__(data_sources)保证多继承链不断。setattr()在运行时给对象动态关联平均分。__str__让print(processor)输出友好信息。7.3 还能怎么扩展这个案例可以继续延展的方向很多。比如把data_sources换成数据库游标clean里改成set()对重复主键去重或者把ScoreProcessor里的排序字段做成可配置的运行时通过setattr()挂不同的排序策略函数再复杂点可以用super()配合staticmethod做一个职责链模式的校验器。想让这8个函数形成肌肉记忆最好的办法就是在自己手头的小工具里刻意用一用。8. 常见问题与避坑实录8.1 高频报错与排查平时我回答同事问题遇到的内置函数报错基本集中在这几类报错信息产生原因解决方案TypeError: unhashable type: list尝试把列表放进set()内层转元组或使用不可变容器AttributeError: cant set attribute类使用了__slots__或属性只读检查类定义删除__slots__或换可写属性TypeError: slice object cannot be interpreted as an integer把slice对象当索引直接传给某个只接受整数的方法显式用slice.start/stop/stepAttributeError: super object has no attribute ...在错误的类层级里调用方法或某个协作类没调super()检查 MRO补全super().__init__()链TypeError: unsupported operand type(s) for : int and strsum()对字符串列表求和用.join()拼接字符串8.2 经验总结速查表集合判断比列表判断快得多优先用set()做去重和成员测试但要记得它无序。sorted()不会改原列表list.sort()会。选择标准很简单要不要原地排序要不要保留原数据super()在单继承里等于父类方法调用但真正的价值在多继承 MRO。多继承场景永远用super()别图省事写类名。str()和repr()方向不同自定义类时两者都建议实现日志和用户界面展示都会受益。sum()别用来展平嵌套列表嵌套列表展平用itertools.chain.from_iterable()。setattr()很灵活但别在业务代码里滥用尤其是属性名来自外部输入时先做校验。slice()不只是语法糖它能帮你把切片参数收敛成可复用的对象在写自定义容器类时必不可少。8.3 我给新手的一个建议我个人的体会是内置函数的强大不在于某个单独函数有多复杂而在于它们之间可以像乐高积木一样组合。你单独背set()、sorted()感觉没什么了不起但当你写出sorted(set(data), key...)这种一行式的时候代码的可读性和性能都会上一个台阶。还有一个很实用的学习技巧凡是看到别人代码里用了内置函数而你不知道为什么的就去查它的 C 源码实现和官方文档的Note部分。Python 文档里那些 Note 不是废话每一个几乎都对应一个历史坑或者性能约定。比如sorted()的稳定性说明、set()的元素可哈希要求都是官方文档明确写过的约定理解了才知道为什么代码要那么写。最后唠叨一句花半小时把这8个内置函数逐个用一遍比刷十道算法题更值。它们是 Python 日常代码里的地基地基稳了上面盖什么楼都不怕。
返回列表