~ 在 Python 里不会自动展开。
直接写 open('~/a.txt') 会在当前目录建一个名字就叫 ~ 的文件夹。
去重有两条路:list 保序但要一个个比,set 一行搞定但顺序没了。
菜单这种要固定顺序的地方用 list,只是判断「有没有」的地方用 set。
判断「在不在」,set 比 list 快三个数量级——因为它算哈希直接跳,不一个个比。
上一篇学会了装包,这一篇开始规划程序:一个下载器要接受哪些参数、默认值怎么定。 教材第 5 章中间三节是【YouTube 下载器的程序规划】【将影片存入系统的默认路径】 还有一节【使用 set 建立不重复的选项列表】。 这三节是本课里少有的「先想清楚再动手」的部分。前面几章都是边写边跑,这一章得先画出参数表。 演示里那个 3200 倍的差距要看清定语:它只能说明「差三个数量级」,不能说明具体多少微秒。
一、默认存哪:~ 不会自动展开
想存到「下载」文件夹,最直觉的写法是 '~/Downloads'。但 Python 不认这个 ~。
演示第一段验证了:expanduser('~') 给出 C:\Users\admin,
而直接 open('~/a.txt') 会在当前目录建一个名字就叫 ~ 的文件夹。
这不是报错,是悄悄建错地方。 等你回头找文件才发现。
正确写法是 os.path.expanduser('~/Downloads')。它把 ~ 换成当前用户的主目录。
还有个 os.path.expandvars('$HOME'),展开的是环境变量。
| 系统 | 下载目录 | 说明 |
|---|---|---|
| Windows | ~/Downloads |
本机实测就是 C:\Users\admin\Downloads |
| macOS | ~/Downloads |
通用写法,本机没法实测 |
| Linux | ~/Downloads 或 ~/下载 |
取决于桌面环境的语言设置 |
注意最后一行:Linux 上目录名会跟着系统语言变。 中文桌面下可能是「下载」。
所以「拼 ~/Downloads」是一条简化路,够用但不严谨。
真正稳的做法是问系统「你的下载目录在哪」,但那要引入额外的库,本书不展开。
知道自己用的是哪一档,比盲目相信重要。
二、选项列表里有重复
用户可能把同一个格式说两遍:mp4、mp3、mp4、webm、mp3、mp4。菜单上不能重复显示。
去重有两条路。
演示里都跑了:list 去重得到 ['mp4', 'mp3', 'webm'](保序),
set 去重得到 ['mp3', 'mp4', 'webm'](顺序变了)。
list 去重的写法是「见过没有?没见过就加」——一个循环,一个个比。
set 去重一行 list(set(x)) 就完了。
代价是 set 不保序。 演示里那个顺序还是按哈希排出来的,同一台机器同一个进程里固定,
换台机器可能就变。
所以选择标准很清楚:要固定顺序(菜单、下拉框、报告列)用 list 去重;只是判断「有没有」用 set。
顺带说:Python 3.7 以后字典保序,所以「用 dict 去重又保序」也是一条路:list(dict.fromkeys(x))。
这写法比手写循环短,也保序。
三、set 的三种运算
「哪些下过、哪些还差」这类问题,用集合一句话就完了,不用写两层循环。
| 运算 | 写法 | 意思 |
|---|---|---|
| 交集 | a & b |
两边都有 |
| 并集 | a | b |
合起来,去重 |
| 差集 | a - b |
在 a 里、不在 b 里 |
| 对称差 | a ^ b |
只在一边有 |
演示里已下载 ['a.mp4','b.mp4','c.mp4']、待下载 ['c.mp4','d.mp4'],交集是 c.mp4(下过了,
跳过),差集是 d.mp4(还差这个)。
这就是断点续传的全部逻辑。 不用写双层循环一个个比,一个减号搞定。
注意差集有方向:a - b 是「在 a 里不在 b 里」,b - a 是反过来。
写反了结论正好相反,这是集合运算最容易出错的地方。
四、判断「在不在」,set 快多少
演示第四段测了一组:两万个元素,查 200 次。list 花 62.1 毫秒,set 花 0.019 毫秒。 3200 倍。 原因在数据结构:list 是一个个比过去,平均要翻一半元素; set 是先算哈希,直接跳到那个格子,元素多少都差不多。 但这个数字要说清楚定语。 set 这一次已经快到计时器的分辨率边缘了,单次 0.1 微秒这个数只能看个量级,不比它更准。
该记的是「差三个数量级」这件事,不是具体多少微秒。 你换台机器跑,可能是 2000 倍也可能是 5000 倍,但量级不会变。 set 有代价:占内存更多、元素必须能算哈希(列表就放不进去)、顺序不保留。 所以不是「一律用 set」,是「判断存在性用 set,要顺序用 list」。
五、用户没给的参数怎么补
一个下载器有几个参数:格式、输出目录、画质。用户可能一个都不给,也可能给一半。
演示第五段跑的是「什么都没给」:format 落回 mp4、outdir 落回下载目录、quality 落回 best。
这一层叫默认值兜底,是程序规划的必备动作。
写法上有讲究。
演示里用 if not getattr(a, 'format', None),但注释里点出了一个问题:
not x 会把「没给」和「给了空字符串」当成一回事。
'' 是假值,None 也是假值,not 一判两个都进去了。
可这两个意思完全不同:前者是「用户没填」,后者可能是「用户故意留空」。
严格点应该判 is None:if a.format is None: a.format = 'mp4'。
这样空字符串会被当作有效值保留下来。
这条规矩可以推到所有默认值逻辑:「没给」和「给了空」是两件事,别用一个 not 混过去。
六、动手 Demo:默认存哪、哪些选项重复了
先看 ~ 到底展不展开,三个系统的下载目录各长什么样。
再比 list 和 set 两种去重,看顺序差在哪。
然后用集合运算算「哪些下过、哪些还差」。
最后实测一下判断存在性差多少。
import os, platform, time
print('① 各系统的「下载」目录')
print('| 系统 | 下载目录写法 | 说明 |')
print('|---|---|---|')
print('| Windows | ~/Downloads | 本机实测就是 %s |' % os.path.expanduser('~/Downloads'))
print('| macOS | ~/Downloads | 通用写法,本机没法实测 |')
print('| Linux | ~/Downloads 或 ~/下载 | 取决于桌面环境的语言设置 |')
print()
print(' expanduser(\'~\') =', repr(os.path.expanduser('~')))
print(' expandvars(\'$HOME\') =', repr(os.path.expandvars('$HOME')))
print(' 本机平台:', platform.system())
print('② 选项列表里有重复')
raw = ['mp4', 'mp3', 'mp4', 'webm', 'mp3', 'mp4']
seen, uniq = set(), []
for x in raw:
if x not in seen:
seen.add(x)
uniq.append(x)
print(' 原始列表', raw)
print(' 用 list 去重(保序)', uniq)
print(' 用 set 去重 ', list(set(raw)))
print('③ set 的三种运算')
done = {'a.mp4', 'b.mp4', 'c.mp4'}
todo = {'c.mp4', 'd.mp4'}
print(' 已下载', sorted(done))
print(' 待下载', sorted(todo))
print(' 交集(已下过,跳过) ', sorted(done & todo))
print(' 并集(全部) ', sorted(done | todo))
print(' 差集(还差哪些) ', sorted(todo - done))
print('④ 判断「在不在」,set 快多少')
big = ['f%06d.mp4' % i for i in range(20000)]
bset = set(big)
probe = ['f%06d.mp4' % (i * 97 % 20000) for i in range(200)]
t0 = time.perf_counter()
for q in probe:
q in big
t_list = time.perf_counter() - t0
t0 = time.perf_counter()
for q in probe:
q in bset
t_set = time.perf_counter() - t0
print('| 容器 | 元素数 | 查 200 次总耗时 | 单次 |')
print('|---|---:|---:|---:|')
print('| list | 20000 | %.1f 毫秒 | %.1f 微秒 |' % (t_list * 1000, t_list * 1e6 / 200))
print('| set | 20000 | %.3f 毫秒 | %.2f 微秒 |' % (t_set * 1000, t_set * 1e6 / 200))
print(' 这次的差距是 %.0f 倍。' % (t_list / t_set))
print('⑤ 用户没给的参数怎么补')
class Args:
format = None
outdir = None
quality = None
a = Args()
a.format = a.format or 'mp4'
a.outdir = a.outdir or os.path.expanduser('~/Downloads')
a.quality = a.quality or 'best'
print(' 用户什么都没给:')
for k in ('format', 'outdir', 'quality'):
print(' %-8s= %s' % (k, getattr(a, k)))
print(' if not getattr(...) 会把「没给」和「给了空字符串」当成一回事。')
print(' 严格点应该判断 is None,因为空字符串有时是用户故意给的。')
实跑输出:
--------------------------------------------------------------------
默认存哪、哪些选项重复了
--------------------------------------------------------------------
① 各系统的「下载」目录
--------------------------------------------------------------------
| 系统 | 下载目录写法 | 说明 |
|---|---|---|
| Windows | ~/Downloads | 本机实测就是 C:\Users\admin\Downloads |
| macOS | ~/Downloads | 通用写法,本机没法实测 |
| Linux | ~/Downloads 或 ~/下载 | 取决于桌面环境的语言设置 |
expanduser('~') = C:\Users\admin
expandvars('$HOME') = C:\Users\admin
~ 在 Python 里不会自动展开,得调 expanduser。
直接写 open('~/a.txt') 会在当前目录建一个名字就叫 ~ 的文件夹。
更稳的做法是拿系统认定的下载目录,各平台给的答案才一致。
本书用的是「拼 ~/Downloads」这条简化路,够用但不算严谨。
② 选项列表里有重复
--------------------------------------------------------------------
原始列表 ['mp4', 'mp3', 'mp4', 'webm', 'mp3', 'mp4']
用 list 去重(保序)['mp4', 'mp3', 'webm']
用 set 去重 ['mp3', 'mp4', 'webm']
list 去重保序但要一个个比,set 一行搞定但顺序没了。
菜单这种要固定顺序的地方用 list,只是判断「有没有」的地方用 set。
③ set 的三种运算
--------------------------------------------------------------------
已下载 ['a.mp4', 'b.mp4', 'c.mp4']
待下载 ['c.mp4', 'd.mp4']
交集(已下过,跳过) ['c.mp4']
并集(全部) ['a.mp4', 'b.mp4', 'c.mp4', 'd.mp4']
差集(还差哪些) ['d.mp4']
「哪些下过、哪些还差」这类问题,用集合一句话就完了,不用写两层循环。这就是 set 值得学的地方。
④ 判断「在不在」,set 快多少
--------------------------------------------------------------------
| 容器 | 元素数 | 查 200 次总耗时 | 单次 |
|---|---:|---:|---:|
| list | 20000 | 62.1 毫秒 | 310.4 微秒 |
| set | 20000 | 0.019 毫秒 | 0.10 微秒 |
list 是一个个比过去,平均要翻一半元素;set 是先算哈希直接跳到那个格子,元素多少都差不多。
这次的差距是 3200 倍。
set 这一次快到计时器的分辨率边缘了,单次 0.1 微秒这个数只能看个量级,不比它更准。
比的是「差三个数量级」这件事,不是具体多少微秒。
但 set 有代价:占内存更多,元素必须能算哈希(列表就不能放进去),顺序也不保留。
⑤ 用户没给的参数怎么补
--------------------------------------------------------------------
用户什么都没给:
format = mp4
outdir = C:\Users\admin\Downloads
quality = best
if not getattr(...) 会把「没给」和「给了空字符串」当成一回事。
严格点应该判断 is None,因为空字符串有时是用户故意给的。
第一段那个 expanduser 是本篇第一个实用技巧。 ~ 不会被自动展开,直接写 open('~/a.txt') 会在当前目录建一个名叫 ~ 的文件夹,不报错,只是建错地方。 第二段两种去重的顺序不一样:list 去重保住原来的先后,set 去重按哈希排。 菜单要用前者,判断存在性用后者。 第三段那个差集就是断点续传的全部逻辑:待下载减去已下载,剩下的才是要下的。 注意差集有方向,写反了结论正好相反。 第四段那个倍数是数出来的,但要加定语:set 已经快到计时器边缘,单次那个微秒数只能看量级。 该记的是「差三个数量级」。 第五段末尾说的是一个通用毛病:not 会把 None 和空字符串一起当成假。 「没给」和「给了空」意思不同,得分开判。
30 秒实验
- 把演示第一段的
expanduser('~/Downloads')换成'~/Downloads',然后真的open一下, 看文件建到哪去了——建完记得删。 - 把演示第三段的
todo - done改成done - todo,看结果变成什么。 - 把演示第四段的
big长度从 20000 改成 200,再跑一次,看倍数怎么变(提示:元素少了, list 的平均查找次数也少了)。
常见误区
- 「
'~/Downloads'会自动展开成用户目录。」不会。 要写os.path.expanduser()。 不写的话会在当前目录建一个叫~的文件夹,不报错, 只是建错地方。 - 「set 去重比 list 好,一行就够。」它不保序。 菜单、下拉框这种要固定顺序的地方必须用 list 去重。
- 「
if not x就是判断「没给值」。」它把None、''、0、[]全算成没给。 「没给」和「给了空」是两件事,该用is None。
自测三问(点开对答案)
1. `open('~/a.txt', 'w')` 会发生什么? —— 它不会展开 `~`,会在**当前目录**建一个名字就叫 `~` 的文件夹,然后在里面建 a.txt。 不报错,所以很难发现。正确写法是 `os.path.expanduser('~/a.txt')`。 2. 去重的时候什么时候该用 list,什么时候该用 set? —— 要固定顺序(菜单、报告列、下拉框)用 list 去重,一个循环保住原顺序; 只是判断「有没有」用 set,一行 `list(set(x))` 就完了。 另外 `list(dict.fromkeys(x))` 是第三种:一行、又保序。 3. 演示里 set 比 list 快 3200 倍,这个数字能直接拿去说明「set 快 3200 倍」吗? —— 不能。set 那一次已经快到计时器的分辨率边缘,单次 0.1 微秒这个数只能看量级。 能说的是「差三个数量级」。换台机器、换个元素数,倍数会变,但量级不会变。下一篇收尾:按什么排序、出了错怎么接、怎么叫外部程序干活。
- ☐ (本篇完,下一篇 5.3 排序、try except 与调用 FFmpeg)