ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python大括号{}完全解析:字典、集合与f-string的实战指南

Python大括号{}完全解析:字典、集合与f-string的实战指南 1. 大括号在Python里的“身份识别”先问一个问题你第一次写Python的时候看到{}脑子里第一反应是什么如果是从C、Java或者JavaScript转过来的十有八九会觉得这是代码块——就是包住函数体、循环体、条件分支的那层皮。写Python爬虫时我在某公司带过几个新人几乎每个从Java转过来的小伙伴第一天都会在编辑器里敲出类似的代码if x 0 { print(positive) }然后运行报SyntaxError一脸懵。这就是{}在Python里面最让人混淆的地方它在Python里根本不作代码块标记。Python用缩进表达代码层次用冒号:开启一个块。而{}只有一个身份——构造数据结构的字面量语法。说得再直白一点看到{}你应该条件反射地想到两种东西字典dict和集合set。1.1 它不是代码块而是数据结构我整理了一张对照表方便你从其他语言切过来时快速建立心智模型语言{}的含义在Python中对应的替代方案C/Java代码块冒号: 缩进C/Java数组初始化一部分场景列表[]或list()JavaScript对象字面量字典{}近似对应JavaScript代码块冒号: 缩进Python字典字面量就是它自己Python集合字面量就是它自己换句话说在Python里你见到{}应该立刻想到“这里正在创造一只容器”而不是“这里开始了一段逻辑”。举例来说d {name: 张三, age: 30} # 这是一个字典 s {苹果, 香蕉, 橙子} # 这是一个集合 empty {} # 这是一个空字典注意这一点empty {}是无数人踩过的一个坑因为从字面上你很难分辨它到底想表达“空字典”还是“空集合”。答案固定{}创建的是空字典空集合你必须老老实实写set()。后面我单独有一节专门讲这个这里先埋个伏笔。1.2 为什么Python偏偏不拿大括号当代码块要理解这个问题得知道Python在设计哲学上的一个坚持可读性优先。有其他语言的开发者吐槽过Python缩进敏感很烦但反过来想缩进是唯一的层次标识这意味着所有Python代码长出来都带着统一的“骨架”不管是谁写的一眼扫过去就能看懂嵌套深度。如果{}承担了代码块的职责它就得兼职做数据结构的字面量那{a: 1}这种代码就会产生语法上的歧义——你到底是创建字典还是开一个命名不存在的代码块Python选择彻底切断这条路把{}只留给数据结构。所以你如果有“大括号包代码”的习惯到了Python这里先改掉用缩进和你自己的审美来组织逻辑。1.3 从C系语言转过来最容易踩的误区第一个误区就是刚才说的拿{}包逻辑块这个报错还算显眼SyntaxError一出来你基本能定位到。真正隐蔽的是第二个误区写多行字典时最后一个元素后面加了逗号这个在Python里完全合法config { host: 127.0.0.1, port: 8080, }这个尾逗号在Python里是鼓励的方便以后加字段不产生diff噪音。但是在JavaScript或者C里有些编译器会警告。所以你如果带着旧习惯来看这段代码可能觉得“这是不是写错了”其实这是Python的风格偏好。第三个误区嵌套数据结构的读写习惯。在Java里取一个嵌套Map的值得一层层get()在Python里你直接用[]套[]就行data {user: {address: {city: 上海}}} city data[user][address][city]简单粗暴。如果中间某个键不存在会直接抛KeyError不像Java返回null。这个差异也经常让转语言的人蒙圈。怎么优雅地处理这种问题我在字典那节详细说。2. 字典大括号最常见的舞台Python里{}出现频率最高的场景就是字典。字典的基础概念很好理解一种“键-值”映射结构。你要是用过Java的HashMap或者JavaScript的对象Python的字典对他们来说就是换了身衣服。2.1 字典的创建和基本读写创建字典一般有三种方式# 方式一字面量最常用 d {name: 李四, age: 25} # 方式二从键值参数创建 d dict(name李四, age25) # 方式三从可迭代对象创建 pairs [(name, 李四), (age, 25)] d dict(pairs)日常开发里方式一用得最多因为它直观、可读性最好。方式二适合临时组装一小撮数据但键必须是合法的标识符不能带空格或特殊字符。方式三你大概率用不太到但面试里可能会被问到。读操作d[name] # 李四键不存在会抛KeyError d.get(name) # 李四键不存在返回None不报错 d.get(email, 未填写) # 可以指定默认值我个人在写业务代码时get()用得比[]多因为很多场景是“这个字段可能有也可能没有”用get()加默认值能省掉一堆try/except。写操作和删除d[age] 26 # 更新已有键 d[city] 北京 # 新增键 d.pop(age) # 删除并返回该键的值 del d[city] # 删除但不返回 d.update({hobby: coding}) # 批量更新或新增2.2 常用方法和几个容易踩的坑字典的方法不少但实际高频用到的主要是keys()、values()、items()。遍历的时候for key in d: print(key) # 只取键 for value in d.values(): print(value) # 只取值 for k, v in d.items(): print(k, v) # 同时取键和值注意items()返回的是视图对象踩过的坑里我想重点说三个第一个坑是在遍历字典的同时修改字典。比如你想把字典里所有值大于10的键删掉新手最容易写成这样d {a: 5, b: 15, c: 20} for k in d: if d[k] 10: del d[k]这段代码运行时大概率会抛RuntimeError: dictionary changed size during iteration。因为遍历过程中字典的长度变化了Python为了保护迭代器的一致性直接罢工。正确做法是把要删的键先收集到一个列表里遍历完之后再删to_delete [] for k in d: if d[k] 10: to_delete.append(k) for k in to_delete: del d[k]或者用字典推导式一行搞定下一节详细讲。第二个坑是键的类型必须是可哈希的。说白了就是字典要根据键计算哈希值来定位存储位置所以键只能是不可变类型字符串、数字、元组都可以列表、字典这些可变类型不能当键。有人觉得“我把列表转成字符串存进去不就行了吗”可以但那不是同一个问题了。记住一条经验法则你在位运算层面都不能修改的东西才适合当键。元组里如果嵌了列表那这个元组也不能当键。第三个坑是嵌套字典的默认值问题。比如统计一段文本里每个单词在每句话里出现的次数你需要{word: {sentence_id: count}}这种结构。新手容易这么写if word not in stats: stats[word] {} stats[word][sent_id] stats[word].get(sent_id, 0) 1这个写法能用但啰嗦。更Pythonic的方式是用collections.defaultdictfrom collections import defaultdict stats defaultdict(lambda: defaultdict(int)) stats[word][sent_id] 1你看代码量直接砍半而且逻辑更清楚了。defaultdict的好处是访问不存在的键时会自动用你给的工厂函数创建默认值并写进字典不会抛KeyError。2.3 字典推导式的威力列表推导式大家见得多了字典推导式同样好用。基本语法是{键表达式: 值表达式 for 元素 in 可迭代对象}。前面的“删除值大于10的键”用推导式一行搞定d {a: 5, b: 15, c: 20} d {k: v for k, v in d.items() if v 10}再举一个实际场景把两个列表配对成字典。names [张伟, 李娜, 王芳] ages [28, 32, 25] people {name: age for name, age in zip(names, ages)}还有更骚的操作比如用字典推导式翻转键值d {a: 1, b: 2, c: 3} reversed_d {v: k for k, v in d.items()}这个看起来很爽但要小心如果原字典有两个相同的值翻转之后后面的键会覆盖前面的键。比如{a: 1, b: 1}翻转后得到{1: b}a就丢了。所以翻转前最好确认值唯一。字典推导式的本质是构造一个全新的字典所以旧字典不会被修改。如果你需要原地修改还是老老实实用循环加删除或者干脆重新赋值。3. 集合另一个被忽略的大括号使用者如果说字典是大伙都知道的“正宫”集合就是大括号语法里最容易被忽略的第二主角。集合的概念简单到一句话就能讲完一大堆互不相同的元素塞进同一个容器。元素之间没有顺序也没有键值对就是干巴巴的一堆值。3.1 集合的创建与去重创建集合用大括号元素用逗号隔开s {1, 2, 3, 4}最常见的实战场景是去重。我在做数据清洗的时候经常面对一堆可能有重复的ID直接转成集合再转回列表重复项就没了ids [101, 102, 103, 101, 104, 102] unique_ids list(set(ids))这个过程可以理解为集合天生不允许元素重复往里塞元素的时候如果有重复的会被自动忽略。你没有写任何判断逻辑重复数据就被过滤掉了。这个代码跑起来很快因为集合底层是哈希表判断重复的成本极低。你可能会问那我把数据从字典的keys()转成集合行不行完全行而且这也是一种常见的去重思路。比如你需要对比两个字典里有哪些相同的键dict_a {x: 1, y: 2, z: 3} dict_b {y: 4, z: 5, w: 6} same_keys set(dict_a.keys()) set(dict_b.keys())这一段就自然引出了集合的另一个优势——集合运算。3.2 集合运算交集、并集、差集集合的运算逻辑可以直接对应到集合论的概念语法也很直观运算符号方法说明并集|union()出现在任一集合中的元素交集intersection()同时出现在两个集合中的元素差集-difference()属于A但不属于B的元素对称差^symmetric_difference()只出现在其中一个集合的元素实战中我经常用差集来对比配置。比如线上环境的白名单IP和测试环境的白名单IP想知道哪些IP只在测试环境里出现过online {192.168.1.1, 192.168.1.2, 10.0.0.1} test {192.168.1.2, 10.0.0.1, 172.16.0.8} only_test test - online print(only_test) # {172.16.0.8}还有一个场景判断一个列表里有没有重复元素不用写for循环比较集合和原列表的长度就行has_duplicates len(items) ! len(set(items))这个判断看着简单但特别实用。我写过很多数据校验的脚本这句代码是常客。3.3 空集合的黄金陷阱这节我必须单独拿出来讲因为我见过太多人在这里翻车。在Python里你想创建一个“空集合”直觉会写s {}运行没问题但s是空字典不是空集合。检查类型的话print(type(s)) # class dict这就是我前面说的“{}创建的是空字典”的真正含义。创建一个真正的空集合只有一条路s set()这个坑藏得非常深因为代码不报错类型也不会在你眼前直接暴露只有当你后续用s.add(1)往里塞元素时字典和集合的行为看起来还挺像。但如果你做了{1, 2} - s这种运算或者试图把s当成集合传给别人问题才会浮出水面。还有一种更隐蔽的情况如果你想判断一个集合是不是空集if not s: # 无论s是空字典还是空集合都会走到这里但之后行为不同 pass所以如果你在写一个函数参数可以是字典也可以是集合一定要在入口处明确类型。我的习惯是凡是涉及“空”的初始化一律问自己一句“我到底要的是哪个容器”。4. f-string中的大括号格式化输出的秘密到了Python 3.6版本以后大括号在字符串格式化里又有了一个新角色——f-string占位符。很多人学到这里时有个疑惑“我字符串里写个{}为什么有时候能显示数字有时候就报错”这一节把这个问题彻底讲透。4.1 基础用法占位符和表达式求值f-string用f前缀开头普通文本里的{}会被解释成“在这里插入一个变量的值”name 赵敏 age 28 print(f姓名{name}年龄{age}) # 输出姓名赵敏年龄28关键在于{}里面不只是放变量名它可以把任何合法的Python表达式丢进去直接求值。这就比老式的%格式化和format()方法灵活得多price 19.99 quantity 3 print(f总价{price * quantity:.2f} 元) # 输出总价59.97 元甚至可以在里面调用函数def double(x): return x * 2 print(f翻倍结果{double(5)})我当初从format()切到f-string之后最直观的感受是写日志的代码变短了。以前得先算好值再拼接现在直接把表达式甩进去就行。调试时打印变量也很方便可以这样print(f{key }) # 输出key name这个语法是Python 3.8之后支持的调试时特别爽不用自己写前面那串变量名。4.2 嵌套大括号与大括号转义怎么输出“{}”字面量有个场景你写多了就会发现想在字符串里输出一个真的大括号比如你想打印JSON样例{status: 0}或者生成一些模板字符串。如果你直接写print(f结果{status: 0})一定会被语法错误打断。因为{}在f-string里是保留符号它默认你要插值。想输出字面量的大括号必须用双大括号转义print(f结果{{status: 0}}) # 输出结果{status: 0}注意两点第一里面的引号要用单引号否则会和f-string外层的引号冲突第二转义是{{和}}不是反斜杠。有些语言用反斜杠转义Python的f-string里反斜杠在很多版本里有限制双大括号是唯一正道。还有一种嵌套场景——f-string里套字典同时要对字典内容做格式化。比如user_info {name: 钱七, score: 99.5} # 你想输出钱七得了99.5分 print(f用户{user_info[name]}得了{user_info[score]}分)这种写法里{}内部又出现了一对引号很容易写错。我的习惯是如果嵌套超过两层先把要用的值取出来赋给一个临时变量再在f-string里引用。这样代码好读也不容易在引号上翻车name user_info[name] score user_info[score] print(f用户{name}得了{score}分)4.3 格式化控制符对齐、填充、精度f-string最常见的格式化是数字和字符串的展示控制。这里列几个我真心觉得用得上的场景写法示例保留两位小数{value:.2f}{3.14159:.2f}→3.14百分数{value:.1%}{0.856:.1%}→85.6%左对齐占10位{value:10}{abc:10}→abc 右对齐占10位{value:10}{abc:10}→ abc居中占10位{value:^10}{abc:^10}→ abc 千位分隔{value:,}{1234567:,}→1,234,567组合使用也可以比如对齐加宽度加千位分隔print(f{1234567:15,.2f}) # 输出 1,234,567.00有人会问这些格式控制在面试里真的考吗会考但更实际的价值在于生成报表、日志对齐、输出表格这种日常活。你能想象打印一个带表头的统计报告如果没有对齐控制所有数字挤在一起有多难看吗f-string的格式控制是最轻量级的排版工具不需要引入任何第三方库。5. 大括号在代码技巧里的几个野路子除了字典、集合和f-string大括号还有几种存在感比较低的玩法。这些玩法不一定要求你天天用但关键时刻能让你少写很多行代码。5.1 解包与合并{}在函数传参中的特技你可能见过这么一种写法def connect(host, port, timeout30): print(host, port, timeout) params {host: 127.0.0.1, port: 5432, timeout: 60} connect(**params)这里的**params就是把字典解包成关键字参数传给函数。字典里每个键对应参数名值对应参数值。这个技巧在调用那些“参数特别多”的函数时特别好用尤其是当你有一堆配置项需要动态传给某个接口时。反过来函数定义的时候也可以用**kwargs收集多余的关键字参数def log(level, **kwargs): print(level, kwargs)调用log(ERROR, msg出错了, code500)时kwargs会得到一个字典{msg: 出错了, code: 500}。那“合并两个字典”又是怎么回事你可能有这三个选择a {x: 1} b {y: 2} # 方式一update merged a.copy() merged.update(b) # 方式二双星号解包Python 3.5 merged {**a, **b} # 方式三| 运算符Python 3.9 merged a | b{**a, **b}是我写脚本时最喜欢的写法因为它表达式化——它可以出现在任何需要字典的地方而不只是一条独立的语句。比如你要给函数传一个合并后的字典参数方式一得多写三行方式二直接写进参数位置func({**defaults, **user_input})而且注意这种合并如果需要后者覆盖前者顺序就是{**defaults, **user_input}后面的键值覆盖前面同名的。5.2 JSON与Python字典的互转现在做前后端接口对接字典和JSON基本是无缝衔接的。字典的写法{name: 张三}和JSON的写法{name: 张三}几乎是同一套语法唯一的差别是JSON里键必须是字符串Python字典的键可以是数字、元组等。我用json模块做转换的次数多了之后总结出三个高频场景import json # 1. 从JSON字符串解析成Python字典 data json.loads({name: 张三, age: 30}) # 2. 从Python字典序列化成JSON字符串 text json.dumps({name: 张三, age: 30}, ensure_asciiFalse) # 3. 从文件读JSON / 写JSON with open(config.json, r, encodingutf-8) as f: config json.load(f) with open(output.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)这里有个非常容易踩的坑如果不加ensure_asciiFalse中文会被转存成\uXXXX的转义序列。比如张三会变成\u5f20\u4e09文件里看起来非常吓人而且别人打开后没法直接读。第一次遇到这事的人基本都会怀疑自己是不是哪里编码写错了。解决办法就是我上面写的把ensure_ascii显式设成False这样中文就会以原文写进文件。另一个跟大括号直接相关的问题是JSON里的大括号一定要严格配对。有些配置文件是手写的多了一个逗号或者漏了一个}json.load()会直接抛JSONDecodeError。排查这种错误有个土办法把你的JSON字符串复制到编辑器的括号匹配功能里一看就定位到哪一层没配对。5.3 函数默认参数里的“空字典陷阱”这节聊一个面试中老爱问、实际中也老坑人的问题函数的默认参数不能直接用可变对象比如字典或列表。新手写代码时经常想实现一个“累积记录”的效果def add_record(new_item, storage{}): storage[new_item] True return storage第一次调用add_record(apple)返回{apple: True}。第二次调用add_record(banana)你可能以为会得到{banana: True}但实际返回{apple: True, banana: True}。原因在于函数定义时{}只被创建了一次所有对这个函数的调用共享同一个默认字典。这不是你想要的“每次调用都初始化一个空字典”。正确的写法是def add_record(new_item, storageNone): if storage is None: storage {} storage[new_item] True return storage有人觉得“我按这个写法要多写两行很麻烦”但这就是Python中引以为戒的典型陷阱之一。以后凡是默认参数涉及到字典、列表、集合、自定义类对象一律用None做默认值在函数体内再初始化为具体容器。这个习惯能帮你避免大量诡异的“数据串台”问题。6. 常见问题速查与排查心得把我在各种项目里实际碰到过的问题汇总一下给出一张速查表。这份表算是我这么久“趟雷”之后攒下来的一个索引型笔记你可以直接把它贴在键盘边上遇到对应报错先来查一遍。现象可能原因解决办法SyntaxError: invalid syntax且代码块用了{}把{}当代码块用了改成冒号缩进KeyError: xxx访问字典不存在的键用get()或检查inRuntimeError: dictionary changed size during iteration遍历字典时增删元素先收集要改的键再统一修改TypeError: unhashable type: list用列表做字典的键改成元组等不可变类型中文在JSON里变\uXXXX没设置ensure_asciiFalse设置ensure_asciiFalse空集合操作总感觉不对{}创建的是空字典用set()创建空集合函数默认字典被多次调用共享默认参数用了{}改成None函数体内初始化f-string无法输出{}没转义用{{和}}JSONDecodeErrorJSON文本格式不合法用编辑器括号匹配定位问题6.1 排查思路分享报错信息不会骗人很多初学者看到报错第一反应是“出问题了好慌”我的建议恰恰相反——报错信息是Python在帮你指路。先读最关键的一行File xxx.py, line N它会告诉你出错的精确行号和第几个符号。比如SyntaxError: invalid syntax配一个指向第八行末尾的箭头那你第一件事应该是去看那行代码里有没有不匹配的大括号。如果你用了IDE或者编辑器大括号的高亮和括号匹配功能一定要打开。写字典嵌套多的时候我习惯先在{}配对完整的情况下先写外层骨架再往里填内容config { database: { host: ..., port: 3306, options: { charset: utf8mb4 } } }这样一层一层对着括号缩进写基本不会写错。6.2 一段兜底技巧用真实数据验证容器类型有时候你拿到外部传入的数据不确定它到底是字典还是集合还是一些奇怪类型与其靠猜不如直接打印类型。调试代码时我常临时加一行print(type(data), data)看到class dict就知道后面可以安心用data[key]看到class set就知道不能用[]取值。如果你是在写一个对其他人开放的接口担心别人传错类型可以加一层类型判断if not isinstance(data, dict): raise TypeError(f期望dict实际收到{type(data)})这个写法看着啰嗦但在数据处理脚本里能省掉很多“跑一半才发现类型不对”的尴尬。尤其当一段脚本要处理几十个字段时前期一个类型校验能让你少调试半小时。6.3 我自己实战里的三条心得第一能用字典推导式尽量用推导式但别强行用。推导式的核心价值是“把一个容器变换成另一个容器”适合过滤、映射、翻转这种逻辑清晰的操作。如果你的变换逻辑超过三步或者中间需要复杂的条件分支那拆成普通的for循环反而更好读不用为了“秀操作”把代码写成谜。第二f-string虽然好用但别在里面写太长的表达式。我见过有人在一个{}里写了40多字符的嵌套调用加三目运算可读性差到极点。遇到这种情况先在外面把结果算好再放进f-string里格式化。调试着也方便你能单独打印每一步的中间值。第三最容易在大括号上栽跟头的其实是“手写结构化文本”的瞬间——不管是JSON、配置文件模板还是SQL里的IN子句生成。因为你对数据结构的直觉是“大括号就是容器”但输出目标要求的是“字符必须合法”。这种情况下我的建议是拿不准时把生成结果打出来或者写到临时文件里再让程序去解析它。格式化输出配合解析验证是一条能兜住你的安全线。说到底Python的大括号{}本身不复杂复杂的是它被塞进了四种完全不同的语境字典、集合、f-string插值和函数传参解包。你只要心里有这张“身份地址”看到{}先问一句“我现在的场景是哪个”接下来的一切都好办了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进