真实对比defaultdict的.get()和[]两种读取方式的真实差异,验证Counter减法对非正数结果的处理,以及ChainMap真实作为活视图而非快照的行为。
代码calls = [0] def factory(): calls[0] += 1 return [] d = defaultdict(factory) d.get("missing") # 看起来是在"读" x = d["missing"] # 也看起来是在"读",会有区别吗 c1 = Counter(a=3, b=1); c2 = Counter(a=1, b=5) print(c1 - c2) # b: 1-5 = -4,会显示负数吗 base = {"x": 1} cm = ChainMap({"y": 2}, base) base["x"] = 999 # 创建ChainMap之后再改底层字典
d.get("missing") → None
calls after .get(): 0 ← 真实factory一次都没被调用
len(d) after .get(): 0 ← 真实字典里还是空的
x = d["missing"] ← 同样"看起来"是在读
calls after d["missing"] READ access: 1 ← 真实factory被调用了一次
len(d) after that read: 1 ← 真实这个key被插进了字典!c1 = Counter(a=3, b=1); c2 = Counter(a=1, b=5)
c1 - c2 → Counter({'a': 2}) ← a: 3-1=2真实保留,b: 1-5=-4真实被完全丢弃,不是b: -4
c2 - c1 → Counter({'b': 4}) ← b: 5-1=4真实保留,a: 1-3=-2真实被丢弃真实验证了Counter的-运算符跟普通字典逐项相减不是一回事——它真实的语义更接近"多重集合的差集":结果只保留真实为正数的计数,任何计算出来非正数(0或负数)的key,真实都不会出现在结果里,而不是像本文一开始可能猜测的那样把-4这种负值也真实保留下来展示。这个设计跟Counter本身建模的"计数只能是非负整数"这个直觉是一致的——如果业务逻辑真的需要知道"哪些key在c2里比c1多多少"这种带符号的差异,直接用Counter的减法拿到的真实是"单向"的信息,需要反过来再算一次c2 - c1才能看到另一半,本文这次的c1-c2和c2-c1刚好互补,各自只暴露了自己那个方向真实为正的部分。
base = {"x": 1}
cm = ChainMap({"y": 2}, base)
cm["x"] → 1
base["x"] = 999 ← 创建cm之后,直接修改底层的base字典
cm["x"] → 999 ← 真实立刻反映了新值,不是还停留在创建时的1这组结果真实证明ChainMap内部真实只是持有传入的那几个字典的引用,不会在构造的时候把它们的内容复制一份存起来——cm查找"x"这个key时,真实是实时沿着传入的字典列表顺序去查,本文这次base字典在cm创建之后被真实修改了,下一次通过cm去查同一个key,看到的真实是base当前最新的值。这个特性真实是ChainMap常被用来实现"层级配置"(比如命令行参数覆盖环境变量覆盖默认配置)的关键:某一层的配置字典如果在程序运行过程中真的被更新了,所有基于这个ChainMap去读配置的代码,不需要重新构造ChainMap,下一次读取就能自动看到最新值——这跟本系列前面验证过的"模块是单例、共享同一份对象"是同一类"引用共享带来的联动效果",用的时候需要清楚这不是一份独立的快照。
每一步都是本文上面用真实的collections模块跑出来的原始结果。
collections.defaultdict / Counter / ChainMap