前言

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包装之后,分发算法在模块被导入后就已经缓存起来了(只进行过这一次类型检查,存在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