上一篇的列表解决了“一个变量装下一串数据”,但它取数据靠数位置:名单[0] 是第一个,名单[2] 是第三个。装一串同类的东西,这没问题;可如果要装的是一行表格记录,位置就会变成麻烦。

列表在这里会咬人一口

假设你要处理工资表里的一个人,顺手用列表装:

1员工 = ["张三", "财务", 8000]
2print(员工[2])   # 8000

今天跑得好好的。下个月表格加了一列“入职日期”,插在部门和工资之间:

1员工 = ["张三", "财务", "2021-03-02", 8000]
2print(员工[2])   # 2021-03-02

代码一个字没改,结果已经错了。问题不在你写得不小心,而在 员工[2] 这句话的意思是“第 3 个东西”——它记住的是位置,不是“这个东西是什么”。表格加列、删列、调整顺序,位置就全变。

列表本来就是给“排成一队、挨个数”的数据用的。可一行表格记录里,各项之间的关系不是“排在第几位”,而是“它叫什么名字”:这一项叫姓名,那一项叫部门,另一项叫工资。

字典:把名字和数据配成对写进去

让数据自己带名字,就要换一种容器,叫 字典(dict)。写法是用大括号把一串“名字和它的数据”配成对装起来:

1员工 = {"姓名": "张三", "部门": "财务", "工资": 8000}

分解着看这一行:

  • 外面的大括号 {} 是字典的标志。
  • 里面每一项都是一“对”,中间用 冒号 分成左右两半:左边那个名字叫 键(key),右边挂着的东西叫 值(value)。
  • 一对和一对之间用 逗号 隔开。

读它的方式:键就是你贴在盒子上的名字牌,值就是这个牌子下面挂的东西。字典存在的意义就是——你报一个名字牌,它把对应的东西交给你,中间隔着多少别的项都不影响。1

对照上一篇的列表看:列表是“一排格子,按顺序数”;字典是“一排带名牌的格子,按名牌找”。

按键取值

取值的写法和列表长得几乎一样,都是方括号,但方括号里放的东西换了:

1print(员工["姓名"])   # 张三
2print(员工["工资"])   # 8000

列表里 名单[0] 的方括号放的是位置编号,是数字;字典里 员工["姓名"] 的方括号放的是键,一般是字符串,所以 要带引号。这是两种容器最容易混的地方:同样一个方括号,配字典时里面写的是“名字”,不是“第几个”。

名字报错了会怎样?字典里没有的键,用方括号硬取会报 KeyError:

1print(员工["电话"])
2# KeyError: '电话'

这和列表取越界时报 IndexError 是同一类事——容器里没这个东西,Python 不会替你猜,直接停下来告诉你。1

键不能重名,这条规矩带来了可靠

字典有一条硬性要求:同一个字典里,键是唯一的。你要是写了两个 "工资",后一个会把前一个覆盖掉。

正是这条规矩,让“按名字取”变得可靠。不管这张表前面加了多少列、顺序怎么调,只要“工资”这个键还在,员工["工资"] 拿到的就一定是工资那一项。你关心的是“哪一项”,而不是“第几项”——数据一旦自带名字,读代码时你也不用一边看一边数位置了。

加一项和改一项,是同一个写法

字典建好之后还能长,也能改:

1员工["电话"] = "13800001111"   # 这个键本来没有 → 加进一对新的
2员工["工资"] = 8500            # 这个键已经有了 → 把值换成 8500
3print(员工)
4# {'姓名': '张三', '部门': '财务', '工资': 8500, '电话': '13800001111'}

请注意这两行的形状完全相同,右边都是 = 赋值。Python 在赋值时先看键在不在:键不在字典里,就新增一对;键已经在里面了,就把旧值替换掉。1 你不用记两套语法,因为“有就改、没有就加”本来就是同一个动作。

删除一对用 del:

1del 员工["电话"]

删完之后再写 员工["电话"],就会报 KeyError——这一对是真的没了。

先问一句“在不在”:in

既然取不存在的键会报错,那有时候你会想先问一声:

1if "工资" in 员工:
2    print(员工["工资"])

把字典放在 in 右边,得到的是 True 或 False:这个键在不在字典里。2 这是上一篇学过的 in 的一种新用法,判断的对象从“某项在不在列表里”换成了“某个键在不在字典里”。

这里有一个必须记住的点:in 只检查键,不检查值。

1"财务" in 员工     # False,虽然“财务”确实作为值存在于字典里
2"姓名" in 员工     # True

想找某个值在不在,得专门写成 "财务" in 员工.values(),那是另一回事。日常用的时候,先默认 in 看的是键。

除了先问再取,还有一种更顺手的读法 get():

1print(员工.get("电话"))             # None
2print(员工.get("电话", "没登记"))   # 没登记

get() 的写法带着点号,和上一篇 append() 一样,是字典自带的方法。它按键取值,但如果键不存在,它不报错:只给一个参数时返回 None,给第二个参数时就返回你指定的那个默认值。13 “电话这一列不是每个人都有”这种场景,用它比先 in 再取要短。

不过 get() 有个坑要提前知道。看这句:

1if 员工.get("工资"):
2    ...

这么写是错的。if 判断的是“这个值算不算数”,而 0、0.0、空字符串 "" 在 Python 里都算“不算数”。所以工资真的是 0 的人,会被这句话当成“没有工资这一项”而跳过。要判断“有没有这个键”,就用 in;要判断“有键而且值不是空”,写 员工.get("工资") is not None。3 处理表格时数值列出现 0 太常见了,这个区别迟早会用到。

用 for 一次取出所有键和值

字典也能交给 for。直接把字典放在 in 右边,每轮交出来的是 键:

1员工 = {"姓名": "张三", "部门": "财务", "工资": 8000}
2
3for 键 in 员工:
4    print(键)

要顺带拿值,就在循环体里用这个键去取:print(键, 员工[键])。

更常用的是一个一步到位的写法:

1for 键, 值 in 员工.items():
2    print(f"{键}:{值}")

输出是:

1姓名:张三
2部门:财务
3工资:8000

这里的循环写法照旧——冒号、缩进、每轮回头,都和之前一样。只有左边多了一个新东西:写了 键, 值 两个变量名,中间用逗号隔开。

原因在 items() 交出来的东西。items() 是字典自带的方法,它每一轮交出的不是单个值,而是 一对(键, 值),例如 ("姓名", "张三")。13 左边写了两个名字,Python 就按顺序把这一对拆开:第一个装进 键,第二个装进 值,然后执行循环体。你可以把它理解成“一对东西,正好放进两个盒子”,这两个盒子只在这一轮里用,下一轮换上新的一对。

如果只关心值,不关心名字,可以写 员工.values();如果只关心键,for 键 in 员工 就够了。

还有一件关于顺序的事:从 Python 3.7 起,字典会记住你放进去的顺序,遍历出来就是插入顺序,不是打乱的。1 所以上面那段代码的输出顺序,和你写字典时的顺序一致。

键和值分别能放什么

键通常用字符串或数字——它就是一个名字牌,越简单越好。值没有这种限制,数字和字符串都能放;以后你还会遇到值本身是列表、甚至又是一个字典的写法,那时候再讲。这一篇里的值都是简单的数字和字符串。

一句话收束

列表按位置找,字典按名字找。表格里的一行记录,每一项之间的关系是“它叫什么”,所以用字典装最贴合:加列、删列、换顺序,只要键还在,取值的代码就不用动。

下一步的问题是:一行记录装进了一个字典,那几百行呢?答案是让很多个字典排成一队,装进一个列表——也就是“列表里装字典”。那正是读取表格、逐行处理的核心结构,我们下一篇来讲。