dict.fromkeys(old)
python
list(dict.fromkeys(列表)) 是 Python 中实现 "去重且保序" 最快、最优雅的写法,
dict 是 Python 的字典。
dict.fromkeys() 是一个类方法,它的作用是:拿着你给的一串东西(old 列表),把它们变成字典的"键(Key)"。因为没有给值,所以值默认全都是 None。
执行 dict.fromkeys(['a', 'c', 'a', 'f', 'g', 'd', 'c']) 后,得到的结果是:
{'a': None, 'c': None, 'f': None, 'g': None, 'd': None}
在 Python 3.7 及之后的版本中,字典天生是保证插入顺序的(谁先存进去,谁就在前面)。所以原列表里的顺序被完美保留了下来。
list(...)
上一步得到了一个字典 {'a': None, ...}。
直接用 list() 套住这个字典,它的作用是把字典的所有键(Key)提取出来,放入一个列表中。所以最终变成了:
['a', 'c', 'f', 'g', 'd']
字典推导式
python
bag1 = {
'apple': 3,
'banana': 1,
}
bag2 = {
'apple': 1,
'mango': 2
}
new = {
k: bag1.get(k, 0) + bag2.get(k, 0)
for k in bag1.keys() | bag2.keys()
}
print(new)
1. for k in bag1.keys() | bag2.keys()(提取所有的商品名)
bag1.keys() 拿到的就是 ['apple', 'banana']。
bag2.keys() 拿到的就是 ['apple', 'mango']。
中间的 | 是重点! 这是集合(set)的并集操作符。它把两个字典的键合在一起,并且自动去重。
所以,这里遍历的 k 分别是:'apple'、'banana'、'mango'。
2. k: bag1.get(k, 0) + bag2.get(k, 0)(计算商品的数量)
这里是字典推导式的格式(键: 值)。
键是 k。
值是:bag1.get(k, 0) 加上 bag2.get(k, 0)。
.get(k, 0) 是极其关键的防错技巧。意思是:去字典里找 k 的值,如果找不到,就默认返回 0,而不是报错崩溃。
等价的普通的 for 循环,逻辑更一目了然
new = {}
# 先把 bag1 的内容全抄过去
for k, v in bag1.items():
new[k] = v
# 再把 bag2 的内容加进去
for k, v in bag2.items():
if k in new:
new[k] += v # 如果有相同的键,数量相加
else:
new[k] = v # 如果没有,直接新增
print(new)
get
python
在 Python 的底层设计中,字典(dict)是一个类(Class),而 get 就是写在这个类里面的一个函数(方法)。
get 是 Python 字典出厂自带的"原装功能"。只要你用 {} 创建了一个字典,它就自动拥有 get 方法,不需要你写任何代码去单独定义。
get() 是 Python 字典(dict)专属的内置方法。
它专门用来安全地获取字典里的值。
我们可以用一个简单的对比,来看看它和普通的取值方式(中括号 [])有什么不同:
1. 普通取值方式 dict[key]
如果你要取的键不在字典里,Python 会直接报错(引发 KeyError),导致程序崩溃。
bag1 = {'apple': 3}
print(bag1['apple']) # 输出: 3
print(bag1['banana']) # 报错!KeyError: 'banana',程序直接崩溃
2. 安全取值方式 dict.get(key, 默认值)
用 get() 取不到值时,它不会报错,而是安静地返回你指定的"默认值"。如果不填默认值,它就返回 None。
bag1 = {'apple': 3}
print(bag1.get('apple')) # 输出: 3
print(bag1.get('banana')) # 输出: None (不报错)
print(bag1.get('banana', 0)) # 输出: 0 (找不到时,返回你指定的默认值 0)
除了 get,字典一出生就自带的常用方法还有一大堆,你随时都可以"点"出来用:
常用方法 作用 例子
dict.keys() 获取所有的键 my_dict.keys()
dict.values() 获取所有的值 my_dict.values()
dict.items() 获取所有的键值对 my_dict.items()
dict.pop(key) 删除并返回某个键的值 my_dict.pop('apple')
dict.clear() 清空整个字典 my_dict.clear()
list
python
第一类:往列表里"加"东西 (增)
方法 大白话解释 代码例子 结果
append(x) 把 x 塞到列表最末尾(一次只能塞一个)。 a = [1, 2]; a.append(3) [1, 2, 3]
extend(x) 把另一个列表 x 拆开,把里面的元素一个个塞到末尾。 a = [1, 2]; a.extend([3, 4]) [1, 2, 3, 4]
insert(i, x) 在指定的索引 i 位置,插队塞入元素 x。 a = [1, 3]; a.insert(1, 2) [1, 2, 3]
(💡 易错点:append([3, 4]) 会让列表变成 [1, 2, [3, 4]],里面套了一个列表;而 extend([3, 4]) 才会变成 [1, 2, 3, 4]。)
第二类:把东西"扔"掉 (删)
方法 大白话解释 代码例子 结果
pop(i) 按索引拔。把位置 i 的元素拔出来,并返回给你。如果不填 i,默认拔最后一个。 a = [10, 20, 30]; x = a.pop(1) a 变成 [10, 30],x 变成 20
remove(x) 按内容删。找到第一个等于 x 的元素并删掉(不返回任何东西)。 a = [10, 20, 20]; a.remove(20) [10, 20]
clear() 清空列表里的所有东西。 a = [1, 2]; a.clear() []
del a[i] 这不是方法,是 Python 的删除关键字,也是按索引删。 a = [1, 2]; del a[0] [2]
第三类:找东西和数数 (查)
方法 大白话解释 代码例子 结果
index(x) 找位置。返回元素 x 第一次出现的索引(找不到会报错)。 a = ['苹果', '橘子']; a.index('橘子') 返回 1
count(x) 数个数。数一数元素 x 在列表里出现了几次。 a = [1, 1, 2]; a.count(1) 返回 2
(💡 小技巧:如何判断一个东西在不在列表里?用关键字 in:if '橘子' in a:,不用调用方法。)
第四类:重新排队 (排)
方法 大白话解释 代码例子 结果
sort() 原地排序。把列表按从小到大(或字母顺序)排好,直接改变原列表。 a = [3, 1, 2]; a.sort() a 变成 [1, 2, 3]
reverse() 原地反转。把列表倒过来排。 a = [1, 2, 3]; a.reverse() a 变成 [3, 2, 1]
(💡 进阶:sort(reverse=True) 可以从大到小排。)
第五类:复制自己 (其他)
方法 大白话解释 代码例子 结果
copy() 浅拷贝。复制出一个一模一样的新列表。 b = a.copy() b 是 a 的复制品
(💡 避坑:如果直接写 b = a,那不是复制,那是给原列表起了个外号,改 b 会连累 a!必须用 .copy() 才是真复制。)
补充:虽然不是列表的方法,但极常用的内置函数
这些不需要用点 . 调用,直接套在列表外面就能用:
len(a):算列表有多长(几个元素)。
max(a) / min(a):找列表里的最大值/最小值。
sum(a):把列表里所有数字加起来。
sorted(a):也是排序,但它不改变原列表,而是返回一个排好序的新列表。
bag.sort(key=lambda x: x == '橘子')
python
一步:先看懂 lambda x: x == '橘子' 是什么
lambda 就是"临时定义的一个小函数"。
lambda x: x == '橘子' 等价于下面这个普通函数:
def 判断是不是橘子(x):
return (x == '橘子')
它的作用是:你给我一个元素 x,我判断它是不是 '橘子'。
如果是橘子,返回 True。
如果不是橘子,返回 False。
第二步:Python 里的"潜规则"(True 就是 1,False 就是 0)
在 Python 底层,布尔值其实是整数:
True 等于 1。
False 等于 0。
所以,那个小函数相当于:
如果元素是橘子,打分 = 1。
如果元素不是橘子,打分 = 0。
第三步:sort(key=...) 是怎么用这个打分的?
bag.sort(key=...) 的意思是:"不要按元素原本的样子排序,而是按我给你的函数算出来的'打分'来从小到大排序。"
我们把列表 ['苹果', '橘子', '香蕉', '芒果'] 交给 sort,sort 会在内部这样操作:
苹果 拿去打分 ➡️ 不是橘子 ➡️ 0
橘子 拿去打分 ➡️ 是橘子 ➡️ 1
香蕉 拿去打分 ➡️ 不是橘子 ➡️ 0
芒果 拿去打分 ➡️ 不是橘子 ➡️ 0
此时,sort 看到的数据其实变成了这样:
苹果: 0
橘子: 1
香蕉: 0
芒果: 0
然后 sort 按照 0 和 1 从小到大的顺序重新排队:
得分为 0 的排前面:苹果、香蕉、芒果(因为它们得分相同,所以保持原来的相对顺序,这叫"稳定排序")。
得分为 1 的排后面:橘子。
最后排完序,橘子就被"挤"到了最末尾。这就是 sort(key=lambda x: x == '橘子') 的全部逻辑!
不用 lambda,用普通函数
bag = ['苹果', '橘子', '香蕉', '芒果']
# 定义一个专门用来打分的函数
def 获取排序分数(x):
if x == '橘子':
return 1
else:
return 0
# 把函数名传给 key
bag.sort(key=获取排序分数)
print(bag)
# 输出: ['苹果', '香蕉', '芒果', '橘子']
元组解包(Tuple Unpacking)
等号右边 a, b 其实是一个元组 (a, b),等号左边 x, y 也是一个元组 (x, y)。
Python 允许你把右边的一筐东西,直接倒进左边对应位置的筐里。
python
a = 10
b = 20
a, b = b, a # 瞬间交换!不用写 temp
print(a) # 输出 20
print(b) # 输出 10
元组(以及所有的 Python 容器,包括列表、字典)里存的,全都是"引用(地址)",而不是数据本身。
一、什么是"存引用"?
在 Python 里,当你写 a = 1, 2 时,其实发生了两件事:
内存里真正开辟了一块空间,建了一个列表对象 1, 2。
变量 a 本身并不装这个列表,a 只是一个标签(或者叫快递单),上面写着一个内存地址,指向那个列表。
当写到 my_tuple = (1, 2, 3, 4) 时,元组这个"大箱子"里装的,其实是两张快递单。一张写着列表 1, 2 的地址,另一张写着列表 3, 4 的地址。
当你执行 my_tuple1.append(5) 时,Python 找到第二张快递单,顺着地址找过去,把那个列表改了。但快递单本身(元组里装的地址)依然没变。
二、为什么 Python 不直接存"数据本身"?
如果元组直接存数据本身(像 C 语言的数组那样),会引发两个极其严重的问题:
- 元组的大小就"固定死"了
如果元组里直接存放数据,Python 必须在创建元组时,就分配好一块连续固定大小的内存。
比如你存一个整数,占 4 个字节。
如果你存一个超级大的列表(里面有 100 万个数),它需要的空间是巨大的。
问题来了:元组里既可能装小整数,又可能装大列表。如果直接存数据本身,元组的内存大小根本无法确定,这会极大增加内存管理的难度。
存引用(存地址)的好处:无论你装的是 1 个字节的布尔值,还是 100MB 的大列表,在 64 位电脑上,一个地址永远只占 8 个字节。元组的大小在设计时就可以完美固定下来。 - 动态类型的需求
Python 是动态类型语言。列表里可以随时把 1 换成 'hello'(从整数变成字符串)。
如果直接存数据本身,类型一变,内存大小就变了,底层就要重新分配内存。存地址的话,地址(指针)永远只有 8 个字节,换了什么东西,改一下地址就行。 - 避免"数据大搬家"
假设你有一个列表 a = 1, 2, 3, ... (100万个元素),你把它放进一个元组 t = (a, 100)。
如果存数据本身:元组必须把 100 万个元素完整复制一遍,极其消耗时间和内存。
如果存引用:元组只需存一个内存地址(8 个字节),瞬间完成,极其高效。
三、为什么元组不可变,但能改里面的列表?(底层真相)
因为"不可变(Immutable)"这个词,在 Python 里指的是"内存地址不变"。
元组是"不可变"的:意味着元组里面的那几张"快递单"一旦贴好,就永远不能撕下来换成新快递单(不能 my_tuple1 = xxx)。
但快递单指向的"房子"(列表)是"可变"的:你可以进房子里随便砸墙、加家具(append、pop)。房子翻新了,但快递单上的地址没变,元组就会觉得"一切正常"。
(注:如果你想彻底冻结,可以在元组里只放 int、str、tuple 这些本身"不可变"的对象。)
元组 my_tuple (在内存中)
┌───────────────────────────────────┐
│ 元素0: 地址0x100 ────────────┼──▶ 内存地址 0x100: 列表 1, 2
├───────────────────────────────────┤
│ 元素1: 地址0x200 ────────────┼──▶ 内存地址 0x200: 列表 3, 4
└───────────────────────────────────┘
执行 my_tuple1.append(5):
你顺着地址 0x200 找到了列表。
你把 0x200 里面的内容改成了 3, 4, 5。
元组里的地址 0x200 依然没变。
match ... case
python
1. 定义两个功能函数
def do_this():
print("do this")
def do_that():
print("do that")
这里提前定义了两个"工具"。如果程序决定要做"这个",就会调用 do_this() 打印出 do this。
2. 接收用户输入并匹配
match input("what you want to do:"):
input(...):会在屏幕上打印提示语,并停下来等待用户打字,按下回车后,把用户打的字(字符串)拿过来。
match ...:把这个拿过来的结果,送去跟下面的 case 挨个比对。(注意:这是 Python 3.10 及以后版本才有的语法!)
3. 进行判断分支
case "this":
do_this()
如果用户输入的是 this,就执行 do_this()。
case "that":
do_that()
如果用户输入的是 that,就执行 do_that()。
case _:
print("invalid input")
case _: 是这里的重点! 下划线 _ 是一个通配符,代表"其他所有情况"。
意思是:如果用户输入的不是 this 也不是 that,不管输入了什么乱七八糟的东西,都会掉进这个兜底的分支里,打印 invalid input(无效输入)。
⚠️ 避坑指南:
版本限制:如果你在电脑上运行这段代码报错 SyntaxError,大概率是因为你的 Python 版本低于 3.10。你可以去终端输入 python --version 查一下。
输入类型:input() 拿到的永远是字符串。所以 case "this": 里面必须带双引号。如果你写 case 1:,让用户输入数字 1,是匹配不上的,因为用户输入的是字符串 "1"。
在 Python 里,如果你要循环遍历一个列表,并且到了末尾回到开头,有一个专门的"神器":itertools.cycle。
python
import time
from itertools import cycle # 导入循环神器
lights = [
('Green', 2),
('Yellow', 0.5),
('Red', 2),
]
# cycle 会自动把列表变成一个无限循环的迭代器
for color, second in cycle(lights):
print(color)
time.sleep(second)
原理:它是一个"发电机(迭代器)"
cycle 属于 Python 的 itertools 工具库。当你调用 cycle(lights) 时,它并没有立刻把列表复制无数遍,而是造了一台无限运转的传送带:
传送带先按顺序吐出 ('Green', 2)。
吐出 ('Yellow', 0.5)。
吐出 ('Red', 2)。
重点来了:吐完最后一个,它会在内存里悄悄把前面的元素记录下来,然后重新从 ('Green', 2) 开始吐,永不停歇。
所以,for 循环只要一运转,它就一直能拿到数据,永远不会报错"索引越界(IndexError)"。
for 循环的底层逻辑是"迭代器协议(Iterator Protocol)":
你不需要知道当前是第几个。
你只需要对传送带说:"下一个(next)是什么?给我。"
传送带每次把东西扔给你,至于传送带内部转到哪里了,那是传送带自己的事,你完全不用管。
这就是现代编程语言极其推崇的 "面向接口编程":把"怎么取数据(下标管理)"的脏活累活封装在底层,让写代码的人专注于"怎么处理数据"。