前言

functools 模块里面的函数是非常常用和有用的,凡是这个模块新增的内容都是值得了解的。这篇文章将介绍 Python 3.8 新增的 singledispatchmethod。

复习 singledispatch

singledispatch 在我的书里面也提过。Python 3.4 时为 functools 模块引入了将普通函数转换为泛型函数的工具 singledispatch。先铺垫点知识:

  1. 泛型函数:泛型函数是指由多个函数组成的函数,可以对不同类型实现相同的操作,调用时应该使用哪个实现由分派算法决定
  2. Single dispatch:一种泛型函数分派形式,基于单个参数的类型来决定

我们通过 json 序列化的例子理解一下,下面这个报错相信很多同学见过:

In : from datetime import datetime, date

In : now = datetime.now()

In : d = {'now': now, 'name': 'XiaoMing'}

In : import json

In : json.dumps(d)
...
/usr/local/Cellar/python/3.7.2_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py in default(self, o)
    177
    178         """
--> 179         raise TypeError(f'Object of type {o.__class__.__name__} '
    180                         f'is not JSON serializable')
    181

TypeError: Object of type datetime is not JSON serializable

datetime (date) 类型不能直接 json 序列化。常见的解决方案是指定 default 参数:

In : def json_encoder(obj):
...:     if isinstance(obj, (date, datetime)):
...:         return obj.isoformat()
...:     raise TypeError(f'{repr(obj)} is not JSON serializable')
...:
In : json.dumps(d, default=json_encoder)
Out: '{"now": "2019-05-29T21:20:08.439376", "name": "XiaoMing"}'

方案:如果一个对象 obj 是 datetime 和 date 类型,序列化时值直接用obj.isoformat()转成了字符串。

如果用 singledispatch 可以这样写:

In : from functools import singledispatch

In : @singledispatch
...: def json_encoder(obj):
...:     raise TypeError(f'{repr(obj)} is not JSON serializable')
...:

In : @json_encoder.register(date)
...: @json_encoder.register(datetime)
...: def encode_date_time(obj):
...:     return obj.isoformat()
...:

In : json.dumps(d, default=json_encoder)
Out: '{"now": "2019-05-29T21:20:08.439376", "name": "XiaoMing"}'

可以看到,通过 singledispatch 装饰器把 json_encoder 函数转化成泛型函数。

singledispatch 优势和应用场景?

我工作到现在基本没有在代码里面使用过 singledispatch 这种代码设计风格。就如上面的 json 序列化问题,我会选择写一个json_encoder函数,在里面用 if/elif/else 处理不同的类型问题:我会觉得这样逻辑更紧凑可读性好。

写这篇文章前我还特意搜了一些知名 / 主流项目、开发者、组织,绝绝大多数都没有用它。那把它放在标准库且是在一个很重要的模块里面的重要意义是什么呢?

延伸阅读链接 2 是关于 singledispatch 的 PEP 443 作者,也是 Python 核心开发、Python3.8 的发布经理 (Release Manager)Łukasz Langa 的相关博客文章,这里面的内容比较有价值。Łukasz 介绍了为什么你应该使用 singledispatch,看完之后我是这么总结的:

  1. 作者对这类代码设计风格的喜好。这个有点智者见智仁者见仁了,我觉得使用 singledispatch 之后一方面由于整体逻辑分散增加了代码行数,另外一方面我认为使用模式会不必要的增加代码阅读的难度。
  2. 更好的性能。这个需要阅读它的源码才可以理解,如果是常规的 if/elif/else,每一次判断都要做一次一次类型检查,如果某次判断恰好符合 else 部分的,前面的那些 if/elif 都不能少,这是一种性能浪费;而由 singledispatch 包装之后,分发算法在模块被导入后就已经缓存起来了 (只进行过这一次类型检查,存在 < code>dispatch_cache 里),选择分发是一次 Hash 查找 (key in dict),很快。

讲道理,我们应该选性能最好的这个。但事实上业务里面用 if/elif/else 还是 singledispatch 的性能差别是很小的:绝大部分情况下小到用户感受不到,所以我个人的倾向是不使用 singledispatch

如果你有自己的理解,欢迎留言~

singledispatchmethod

singledispatch 主要针对的是函数,但对于方法不友好,举个例子:

In : class Dispatch:
...:     @singledispatch
...:     def foo(self, a):
...:         return a
...:
...:     @foo.register(int)
...:     def _(self, a):
...:         return 'int'
...:
...:     @foo.register(str)
...:     def _(self, a):
...:         return 'str'
...:

In : cls = Dispatch()

In : cls.foo(1)
Out: 1  # 没有返回 'int'

In : cls.foo('s')
Out: 's'  # 没有返回 'str'

也就是 singledispatch 在方法上失效了。现在可以用 singledispatchmethod 来做了:

>>> from functools import singledispatchmethod
>>> class Dispatch:
...     @singledispatchmethod
...     def foo(self, a):
...         return a
...
...     @foo.register(int)
...     def _(self, a):
...         return 'int'
...
...     @foo.register(str)
...     def _(self, a):
...         return 'str'
...
>>> cls = Dispatch()
>>> cls.foo(1)
'int'
>>> cls.foo('s')
'str'

这种模式还可以用在 classmethod、staticmethod、abstractmethod 等装饰器上,如官网的例子:

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

以上就是 Python3.8 带来的 singledispatchmethod 的用途了。

延伸阅读

  1. https://www.python.org/dev/peps/pep-0443/
  2. http://lukasz.langa.pl/8/single-dispatch-generic-functions/
  3. https://hynek.me/articles/serialization/
  4. https://github.com/python/cpython/pull/6306