写 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", encoding="utf-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()的稳定排序与key
sorted()是我最常推荐的内置函数,比list.sort()稳:它返回一个新列表,不改原数据,语义上更安全。真正拉开差距的是它的key参数。
students = [ {"name": "Alice", "score": 88}, {"name": "Bob", "score": 72}, {"name": "Carol", "score": 95}, ] rank = sorted(students, key=lambda s: s["score"], reverse=True) 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(key=lambda x: x["chinese"], reverse=True) data.sort(key=lambda x: x["total"], reverse=True)有的同学在这里会问:为什么第二次排序不会破坏第一次的结果?这正是稳定排序的承诺。大多数排序场景用sorted()配合key和reverse就完全够用,并不需要写复杂的比较函数。
4.3 用slice+sorted组合处理实际问题
我印象很深的一个需求是"截取列表中间某部分,并对这部分排序,且不影响原列表"。如果不用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("test@example.com")) # True print(Validator().is_valid_email("bad-email")) # False为什么要用staticmethod而不是直接写模块级函数?主要是归属和组织。如果某个函数和类的概念强相关,放进类里可以避免污染模块命名空间,也让调用方一目了然。
5.2 super()的工作原理与MRO
super()可能是这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原因就在 MRO:D -> 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 f"Report({self.title!r}, {len(self.rows)} rows)" def __repr__(self): return f"Report(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, start=0)的第二个参数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, start=100)可以直接给总和加一个基数;配合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, key=lambda x: x[1], reverse=True) 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 f"Top 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, threshold=60) 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: can't set attribute | 类使用了__slots__或属性只读 | 检查类定义,删除__slots__或换可写属性 |
TypeError: 'slice' object cannot be interpreted as an integer | 把slice对象当索引直接传给某个只接受整数的方法 | 显式用slice.start/stop/step |
AttributeError: 'super' object has no attribute ... | 在错误的类层级里调用方法,或某个协作类没调super() | 检查 MRO,补全super().__init__()链 |
TypeError: unsupported operand type(s) for +: 'int' and 'str' | sum()对字符串列表求和 | 用''.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 日常代码里的地基,地基稳了,上面盖什么楼都不怕。