《流畅的python》单分派泛函数

发布时间 2023-04-25 20:31:16作者: limalove

假设我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象。我们可能会编写这样的函数:

import html
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

这个函数适用于任何Python类型,但是现在我们想做个扩展,让它使用特别的方式显示某些类型。

str:把内部的换行符替换为'<br>\n';不使用<pre>,而是使用<p>。

int:以十进制和十六进制显示数字。

list:输出一个HTML列表,根据各个元素的类型进行格式化。

因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize的变体,也无法使用不同的方式处理不同的数据类型。在Python中,一种常见的做法是把htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int,等等。这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数htmlize会变得很大,而且它与各个专门函数之间的耦合也很紧密。Python 3.4新增的functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。

示例7-21 singledispatch创建一个自定义的htmlize.register装饰器,把多个函数绑在一起组成一个泛函数

from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch  ➊
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)
@htmlize.register(str)  ➋
def _(text):            ➌
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)  ➍
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple)  ➎
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>'+inner+'</li>\n</ul>'

 

❶ @singledispatch标记处理object类型的基函数。❷ 各个专门函数使用@«base_function».register(«type»)装饰。❸ 专门函数的名称无关紧要;_是个不错的选择,简单明了。❹ 为每个需要特殊处理的类型注册一个函数。numbers.Integral是int的虚拟超类。❺ 可以叠放多个register装饰器,让同一个函数支持不同类型。

只要可能,注册的专门函数应该处理抽象基类(如numbers.Integral和abc.MutableSequence),不要处理具体实现(如int和list)。这样,代码支持的兼容类型更广泛。例如,Python扩展可以子类化numbers.Integral,使用固定的位数实现int类型。

[插图]使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类。

singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。singledispatch是经过深思熟虑之后才添加到标准库中的,它提供的特性很多,这里无法一一说明。这个机制最好的文档是“PEP 443—Single-dispatch generic functions”。[插图]

@singledispatch不是为了把Java的那种方法重载带入Python。在一个类中为同一个方法定义多个重载变体,比在一个函数中使用一长串if/elif/elif/elif块要更好。但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多。@singledispath的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。

装饰器是函数,因此可以组合起来使用(即,可以在已经被装饰的函数上应用装饰器,如示例7-21所示)。

def escape(s, quote=True):
"""
Replace special characters "&", "<" and ">" to HTML-safe sequences.
If the optional flag quote is true (the default), the quotation mark
characters, both double quote (") and single quote (') characters are also
translated.
"""
s = s.replace("&", "&amp;") # Must be done first!
s = s.replace("<", "&lt;")
s = s.replace(">", "&gt;")
if quote:
s = s.replace('"', "&quot;")
s = s.replace('\'', "&#x27;")
return s