Item 18: Know How to Construct Key-Dependent Default Values with __missing__

The built-in dict type’s setdefault method results in shorter code when handling missing keys in some specific circumstances (see Item 16: “Prefer get Over in and KeyError to Handle Missing Dictionary Keys” for examples). For many of those situations, the better tool for the job is the defaultdict type from the collections built-in module (see Item 17: “Prefer defaultdict Over setdefault to Handle Missing Items in Internal State” for why). However, there are times when neither setdefault nor defaultdict is the right fit.

内置的dict类型的setdefault方法在某些特定的情况下处理丢失的键时会使代码更短(参见Item 16:“Prefer get Over in and KeyError to Handle Missing Dictionary Keys”的例子)。对于许多这样的情况,更好的工具是来自collections内置模块的defaultdict类型(参见Item 17:“Prefer defaultdict Over setdefault to Handle Missing Items in Internal State”)。但是,有时setdefault和defaultdict都不合适。

For example, say that I’m writing a program to manage social network profile pictures on the filesystem. I need a dictionary to map profile picture pathnames to open file handles so I can read and write those images as needed. Here, I do this by using a normal dict instance and checking for the presence of keys using the get method and an assignment expression (introduced in Python 3.8; see Item 10: “Prevent Repetition with Assignment Expressions”) :

例如,假设我正在编写一个程序来管理文件系统上的社交网络头像。我需要一个字典来映射图片路径名和已打开的文件句柄,这样我就可以根据需要读取和写入这些图像。在这里,我使用一个普通的dict实例,并使用get方法和赋值表达式(在Python 3.8中引入;参见项目10:“Prevent Repetition with Assignment Expressions”):

pictures = {}  
path = 'profile_1234.png '  
if (handle := pictures.get (path)) is None:  
    try:  
        handle = open (path, 'a+b ')  
    except OSError:  
        print (f 'Failed to open path {path} ')  
        raise  
    else:  
        pictures [path] = handle  

handle.seek (0)  
image_data = handle.read ()  

When the file handle already exists in the dictionary, this code makes only a single dictionary access. In the case that the file handle doesn’t exist, the dictionary is accessed once by get, and then it is assigned in the else clause of the try/except block. (This approach also works with finally; see Item 65: “Take Advantage of Each Block in try/except/else/finally.”) The call to the read method stands clearly separate from the code that calls open and handles exceptions.

当文件句柄已经存在于该字典中时,这段代码只进行一次字典访问。在文件句柄不存在的情况下,该字典被get访问一次,然后在try/except块的else子句中被赋值一次。(这个方法也适用于finally; 参见Item 65:“Take Advantage of Each Block in try/except/else/finally.”)。调用read方法,与调用open和处理异常的代码是明显分开。

Although it’s possible to use the in expression or KeyError approaches to implement this same logic, those options require more dictionary accesses and levels of nesting. Given that these other options work, you might also assume that the setdefault method would work, too:

尽管可以使用in表达式或KeyError方法来实现相同的逻辑,但这些选项需要更多的字典访问和嵌套级别。既然这些选项都能工作,你也可以假设setdefault方法也能工作:

try:  
    handle = pictures.setdefault (path, open (path, 'a+b '))  
except OSError:  
    print (f 'Failed to open path {path} ')  
    raise  
else:  
    handle.seek (0)  
    image_data = handle.read ()  

This code has many problems. The open built-in function to create the file handle is always called, even when the path is already present in the dictionary. This results in an additional file handle that may conflict with existing open handles in the same program. Exceptions may be raised by the open call and need to be handled, but it may not be possible to differentiate them from exceptions that may be raised by the setdefault call on the same line (which is possible for other dictionary-like implementations; see Item 43: “Inherit from collections.abc for Custom Container Types”).

这段代码有很多问题。创建文件句柄的open内置函数总是被调用,即使该路径已经存在于字典中。这会产生一个额外的文件句柄,该句柄可能与同一程序中现有的打开句柄冲突。open调用可能会引发异常并需要处理,但我们无法将它们与setdefault调用在同一行上引发的异常区分开来(这可能适用于其他类似字典的实现;参见Item 43:“Inherit from collections.abc for Custom Container Types”)。

If you’re trying to manage internal state, another assumption you might make is that a defaultdict could be used for keeping track of these profile pictures. Here, I attempt to implement the same logic as before but now using a helper function and the defaultdict class:

如果您试图管理内部状态,您可能会做出的另一个假设是,使用defaultdict来跟踪这些头像图片。在这里,我尝试实现与以前相同的逻辑,但现在使用一个助手函数和defaultdict类:

from collections import defaultdict  

def open_picture (profile_path):  
    try:  
        return open(profile_path, 'a+b ')  
    except OSError:  
        print (f 'Failed to open path {profile_path} ')  
        raise  

pictures = defaultdict (open_picture)  
handle = pictures[path]  
handle.seek (0)  
image_data = handle.read ()  

>>>  
Traceback ...  
TypeError: open_picture () missing 1 required positional  
argument: 'profile_path '  

The problem is that defaultdict expects that the function passed to its constructor doesn’t require any arguments. This means that the helper function that defaultdict calls doesn’t know which specific key is being accessed, which eliminates my ability to call open. In this situation, both setdefault and defaultdict fall short of what I need.

问题是defaultdict期望传递给它的构造器的函数是不带任何参数的。这意味着defaultdict调用的helper函数不知道被访问的是哪个特定键,这就消除了调用open的能力。在这种情况下,setdefault和defaultdict都不能满足我的需要。

Fortunately, this situation is common enough that Python has another built-in solution. You can subclass the dict type and implement the __missing__ special method to add custom logic for handling missing keys. Here, I do this by defining a new class that takes advantage of the same open_picture helper method defined above:

幸运的是,这种情况非常常见,因此Python有另一个内置的解决方案。你可以继承dict类型并实现__missing__魔术方法来添加处理丢失键的自定义逻辑。在这里,我通过定义一个新类来实现这一点,它利用了上面定义的open_picture helper方法:

class Pictures (dict):  
    def __missing__ (self, key):  
        value = open_picture(key) 
        self [key] = value  
        return value  

pictures = Pictures ()  
handle = pictures[path]  
handle.seek (0)  
image_data = handle.read()  

When the pictures [path] dictionary access finds that the path key isn’t present in the dictionary, the __missing__ method is called. This method must create the new default value for the key, insert it into the dictionary, and return it to the caller. Subsequent accesses of the same path will not call __missing__ since the corresponding item is already present (similar to the behavior of __getattr__; see Item 47: “Use __getattr__, __getattribute__, and __setattr__ for Lazy Attributes”).

当pictures[path]字典访问发现path键不在字典中时,__missing__方法被调用。此方法必须为键创建新的默认值,将其插入字典并返回给调用者。后续对相同的path访问不会再调用__missing__,因为相应的项已经存在了(类似于__getattr__;参见第47项:“Use __getattr__, __getattribute__, and __setattr__ for Lazy Attributes”)。

Things to Remember
要记住的事

✦ The setdefault method of dict is a bad fit when creating the default value has high computational cost or may raise exceptions.
✦ The function passed to defaultdict must not require any arguments, which makes it impossible to have the default value depend on the key being accessed.
✦ You can define your own dict subclass with a __missing__ method in order to construct default values that must know which key was being accessed.

✦ 当创建默认值有很高的计算成本或可能引发异常时,dict的setdefault方法是不适合的。
✦ 传递给defaultdict的函数不能有任何参数,这使得它不可能有依赖于被访问的键的默认值。
✦ 你可以用__missing__方法定义你自己的dict子类,以便依赖被访问的键构造默认值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容