0 字典特殊场景例子:

  • 我们要写一个程序,在文件系统里管理社交网络账号中的图片。
  • 这个程序应该用字典把这些图片的路径名跟相关的文件句柄(file handle)关联起来,
    • 这样我们就能方便地读取并写入图像了。

      1 使用 get 方法与赋值表达式解决

      ```python pictures = {} path = ‘profile_1234.png’

在图片文件中写一些二进制信息

with open(path, ‘wb’) as f: f.write(b’image data here 1234’)

if (handle := pictures.get(path)) is None: # get + 赋值表达式 try: handle = open(path, ‘a+b’) # 二进制 追加形式 except OSError: print(f’Failed to open path {path}’) raise else: pictures[path] = handle

handle.seek(0) # 0 代表起始位置,1 代表当前位置,2 代表末位 image_data = handle.read()

print(pictures) print(image_data)

  1. - 如果字典里已经有这个 handle 了,那么这种写法只需要进行一次字典访问。
  2. - 如果没有,那么它会通过 get 方法访问一次字典,然后在 try/except/else 结构的 else 分支中做一次赋值(带 finally 块的结构也可以这样做,参见第 65 条)。
  3. - 读取数据的代码与打开文件并处理异常的代码可以分开写。
  4. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22011425/1648104986587-363ca18e-7212-4322-9de2-8f27b86e46d8.png#clientId=ud395136d-e9f8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=330&id=u1cf7dc2e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=462&originWidth=560&originalType=binary&ratio=1&rotation=0&showTitle=false&size=101617&status=done&style=none&taskId=ue10f81af-fe73-4c01-a566-af481853626&title=&width=400)
  5. <a name="als1t"></a>
  6. # 2 用 setdefault 实现(有问题)
  7. ```python
  8. pictures = {}
  9. path = 'profile_1234.png'
  10. # 在图片文件中写一些二进制信息
  11. with open(path, 'wb') as f:
  12. f.write(b'image data here 1234')
  13. try:
  14. handle = pictures.setdefault(path, open(path, 'a+b'))
  15. except OSError:
  16. print(f'Failed to open path {path}')
  17. raise
  18. else:
  19. handle.seek(0)
  20. image_data = handle.read()
  21. print(pictures)
  22. print(image_data)
  • 这样写有很多问题。
  • 首先,即便图片的路径名已经在字典里了,程序也还是得调用内置的 open 函数创建文件句柄,于是导致这个程序要给已经创建过 handle 的那份文件再度创建 handle(两者可能相互冲突)。
  • 另外,如果 try 块抛出异常,那我们可能无法判断这个异常是 open 函数导致的,还是 setdefault 方法导致的,因为这两次调用全都写在了同一行代码里(其他一些类似字典的实现方案或许可以做到,参见第 43 条)。

    3 用 defaultdict 实现(有问题)

  • 下面用 defaultdict 类实现相同的逻辑,只不过这次得专门写一个辅助函数。 ```python from collections import defaultdict

pictures = {} path = ‘profile_1234.png’

在图片文件中写一些二进制信息

with open(path, ‘wb’) as f: f.write(b’image data here 1234’)

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()

print(pictures) print(image_data)

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22011425/1648105307007-5cf33d6a-0286-4fa4-912a-574399df3589.png#clientId=ud395136d-e9f8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=241&id=udcdf3684&margin=%5Bobject%20Object%5D&name=image.png&originHeight=241&originWidth=559&originalType=binary&ratio=1&rotation=0&showTitle=false&size=70422&status=done&style=none&taskId=uce45f6e6-5674-4ed0-8ec1-2d2e3f2cdee&title=&width=559)
  2. - 程序出错的原因在于,传给 defaultdict 的那个函数只能是不需要参数的函数,而**我们写的辅助函数却要求调用方传入一个参数**。defaultdict 并不知道当前要访问的这个键叫什么名字,所以没办法给辅助函数传递这个参数,这也意味着我们没办法用这个参数去调用 open
  3. <a name="X20vs"></a>
  4. # 4 使用 "__**missing__" **方法
  5. ```python
  6. path = 'profile_1234.png'
  7. # 在图片文件中写一些二进制信息
  8. with open(path, 'wb') as f:
  9. f.write(b'image data here 1234')
  10. def open_picture(profile_path):
  11. try:
  12. return open(profile_path, 'a+b')
  13. except OSError:
  14. print(f'Failed to open path {profile_path}')
  15. raise
  16. class Pictures(dict): # 注意继承了 dict
  17. def __missing__(self, key):
  18. value = open_picture(key)
  19. self[key] = value
  20. return value
  21. pictures = Pictures()
  22. handle = pictures[path]
  23. handle.seek(0)
  24. image_data = handle.read()
  25. print(pictures)
  26. print(image_data)

image.png

  • 访问 pictures[path] 时,如果 pictures 字典里没有 path 这个键,那就会调用 __missing__ 方法。
  • 这个方法必须根据 key 参数创建一份新的默认值,系统会把这个默认值插入字典并返回给调用方。
  • 以后再访问 pictures[path],就不会调用 __missing__了,因为字典里已经有了对应的键与值(类似的机制还体现在 __getattr__ 中,参见第 47 条)