
我见过太多人在这个问题上翻车。上周 code review,一个实习生写了 shuffled = my_list.shuffle(),然后对着一个 None 报错抓了半小时头发。这不是个例,random.shuffle 的用法看着只有一行,真踩起坑来能让你怀疑人生。
先说结论:shuffle 是原地操作,它不返回任何东西。很多人脑子里想的是"给我一个新列表",实际拿到的是空。这个认知偏差是九成错误的根源。
最典型的写法:
import random
nums = [1, 2, 3, 4, 5]
result = nums.shuffle()
print(result) # Noneshuffle 的返回值是 None,官方文档写得很清楚,但没人看文档。你该写的是:
random.shuffle(nums)
print(nums) # 已经被打乱了如果一定要保留原列表,先 copy 再 shuffle:
shuffled = nums.copy()
random.shuffle(shuffled)这一行顺序不能反。我见过有人先 shuffle 再 copy,等于白忙活一场。还有人用切片 nums[:] 做复制,写法没问题,只是可读性差一点,团队里统一一种就行。
上面那个技巧其实暴露了第二个坑。shuffle 直接改的是你传进去的那个对象。如果你把同一个列表引用传给了两处代码,一处 shuffle,另一处也跟着乱。这种 bug 不会报错,只是数据悄悄变了,最难查。
写单元测试的时候尤其要小心。一个测试里 shuffle 了全局配置列表,另一个测试依赖它的原始顺序,两个测试单独跑都过,合起来就挂。这种间歇性的失败,八成是 shuffle 的副作用。我的习惯是,凡是会被打乱的列表,进测试前先 deep copy 一份隔离掉。
调试随机逻辑最崩溃的瞬间:你明明只改了一行代码,结果每次跑出来的现象都不一样,根本没法定位。问题出在你没固定随机种子。
random.seed(42)
random.shuffle(data)设了 seed,同样的输入永远得到同样的打乱结果。写爬虫、做实验、生成示例数据,只要你想让别人复现你的结果,seed 是标配。不过注意,seed 只对当前这条随机序列生效,中间插了别的随机调用,序列就变了。曾经有个同事在 seed 之后顺手调了一次 random.random(),后面的 shuffle 全偏了,他对着日志看了两天。
这是我最想拍醒大家的一点。很多人一上手就 shuffle,其实他真正想要的是"从里面随机抽 N 个出来"。这种需求用 random.sample 更干净:
picked = random.sample(population, k=3)sample 不修改原列表,也不会因为抽得太多而把整个列表打乱。你要的是子集,就用子集的工具。shuffle 是把整副牌洗一遍,sample 是从牌堆里抽几张,两件事。
还有个容易忽略的细节:k 的值不能大于总体长度,否则直接抛 ValueError。你要是拿一个不确定大小的列表去 sample,记得先 min 一下,或者干脆判断长度。我更倾向于在调用前断言 k <= len(population),这样出错时信息更明确,比看那段 ValueError 舒服。
做数据分析的人容易踩这雷。numpy 有自己的随机体系,np.random.shuffle 和 Python 标准库的 shuffle 是两码事,前者专门伺候 numpy 数组,后者只认 list 这类可变的 Python 序列。
import numpy as np
arr = np.array([1, 2, 3, 4])
np.random.shuffle(arr) # 原地,作用在 ndarray 上你把 numpy 数组丢给标准库 random.shuffle,多数时候能跑,但类型行为和你的预期可能不一致,尤其数组里还套着维度的时候。同一个项目里,定一个规矩:要么全用标准库,要么全用 numpy,别混。我见过有人先拿 numpy 洗了数组,转成 list 后又用标准库洗一遍,问他为什么,他说"这样更随机"。不是的,这样只是更慢。
有人用 asyncio 或者多线程跑一批任务,先 shuffle 再分发给 worker,结果发现 worker 拿到的顺序还是老样子。这往往不是 shuffle 没跑,而是你 shuffle 的对象和 worker 读的对象不是同一个引用,或者你在 shuffle 之后又做了一次排序、又被别处的代码还原了。
并发环境里,最好把"生成顺序"这一步集中在一个地方完成,生成完立刻分发出去,中间不要再去碰那个列表。否则你永远在和竞态条件玩捉迷藏。我一般会把打乱后的列表一次性塞进队列,worker 只从队列里取,谁都不许回写。
顺带聊一句原理,免得你把它当黑盒。Python 的 shuffle 用的是 Fisher-Yates 算法,从最后一个元素往前,每次随机挑一个前面的位置交换。时间复杂度是 O(n),而且保证每个排列出现的概率严格相等。有些新手自己写个"随机取索引再拼新列表"的洗牌,概率分布其实是偏的,跑一百万次你会发现某些顺序出现得明显更多。所以别自己造轮子,标准库那个已经是对的。
讲点真正用得上的。做大规模数据采集时,单一 IP 很快会被对方封掉,所以大家都会备一个代理 IP 池。比如我从亿牛云拿了一批高质量动态住宅代理,亿牛云本身会做 IP 轮换,但你本地也得自己排个序,别每次都从第一个开始用。
import random
# proxies 来自亿牛云代理池,形如 "http://user:pass@ip:port"
proxies = load_from_yiniu()
# 每次请求前打乱,把流量均匀铺开,降低被识别为机器的概率
random.shuffle(proxies)
proxy = proxies[0]
requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=10)这里 shuffle 的价值不在于"随机好看",而在于打散顺序、避免请求特征呈现规律。反爬系统最喜欢抓的就是"永远先连 A 再连 B"这种固定模式。你把 IP 顺序打乱,再配合亿牛云自身的动态切换,对方就比较难给你贴标签。
亿牛云那边的代理质量确实稳,但再稳也架不住你把同一个 IP 连续锤几千次。我一般会做两层:亿牛云负责换出口 IP,本地 shuffle 负责换使用顺序。两层叠起来,单个 IP 的命中频率能压得很低。
不过提醒一句,代理池 shuffle 之后记得做失败重试。某一个 IP 偶尔不可用,别卡死在它身上,try 一下换下一个。我习惯把 shuffle 和重试封装在一起,整体当作一个"取可用代理"的黑盒,业务代码根本不关心顺序。长这样:
def get_proxy():
random.shuffle(proxies)
for p in proxies:
try:
if check_alive(p):
return p
except Exception:
continue
raise RuntimeError("没有可用的代理")写了这么多,最后压缩成几条,你下次写之前扫一眼:
shuffle 没有返回值,别去接它。要保留原列表就先 copy。需要复现就设 seed。要的是子集就上 sample,k 别超总长度。numpy 和标准库分开用。并发里只打乱一次,之后塞队列。代理池记得 shuffle 加重试,亿牛云负责换 IP,本地负责换顺序。
这些坑没有一个是高深问题,但每一个都能让你白白耗掉一个下午。把上面那几行记熟,比背一百个面试题管用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。