函数 pattern/predicate 匹配 Python

Function pattern/predicate matching in Python

我希望能够分派函数的不同实现,不仅基于第一个参数的类型,而且基于任意谓词。目前我必须这样做:

def f(param):
    try:
        if param > 0:
            # do something
    except TypeError:
        pass
    try:
        if all(isinstance(item, str) for item in param):
            # do something else
    except TypeError:
        raise TypeError('Illegal input.')

以下是我希望能够做的事情的精神:

@generic
def f(param):
    raise TypeError('Illegal input.')  # default

@f.when(lambda param: param > 0)
def f_when_param_positive(param):
    # do something

@f.when(lambda param: all(isinstance(item, str) for item in param))
def f_when_param_iterable_of_strings(param):
    # do something else

它类似于 Python 3 的 singledispatch,但是 singledispatch 只支持类型分派,不支持任意谓词。

TL;DR:是否有一个库允许基于任意谓词(不仅是参数的类型)进行基于谓词的函数分派?

您可以使用 isintance 并将其与 ABC 组合来检查输入的特征,如下所示:

from collections.abc import Iterable

def foo(param):
    if isinstance(param,int) and param > 0:
        #do something 
    elif isinstance(param,Iterable) and all(isinstance(item, str) for item in param):
        # do something else
    else:
        raise TypeError('Illegal input.')

ABC 告诉你参数有什么样的接口,所以如果你不关心它是否是特定类型,你可以根据你的操作使用适当的接口,所以像那个参数一样定义可能是字符串的 setlisttuple,并且总是会通过第二次检查,因此您可以相应地对其进行处理。还有 numbers 的 ABC 是你想在那种情况下也通用。

我不知道有什么库,但这里有一个基本的实现框架。在我看来,阻止这成为一个实用解决方案的真正问题是,我不知道如何在此处进行专门的解析1。那样的话,估计维护起来会很麻烦。

#!/usr/bin/python3  

class when(object):
  funcs = {}

  def __init__(self, pred):
    self.pred = pred

  def __call__(self, func):
    if func.__qualname__ not in when.funcs:
        when.funcs[func.__qualname__] = {}

    when.funcs[func.__qualname__][self.pred] = func

    return lambda *args, **kwargs: when.__match(func, *args, **kwargs)

  @staticmethod
  def __match(f, *args, **kwargs):
    for pred, func in when.funcs[f.__qualname__].items():
      if pred(*args, **kwargs):
          return func(*args, **kwargs)
    raise NotImplementedError()


@when(lambda x: x < 0)
def my_func(x):
  return "smaller!"

@when(lambda x: x > 0)
def my_func(x):
  return "greater!"


print(my_func(-123))
print(my_func(123))

[1]:分辨率的问题是不容易对。这里有一些可供考虑的替代方案,所有这些方案都严重缺乏实施和使用的充分理由。

  1. apply 的特化谓词可能很复杂,最好交由用户手动定义 ranks/weights。这很笨拙,而且通常 maintenance/boilerplate 令人头疼,不值得这种机制的初始魅力。
  2. 用户总是可以在程序 运行(Python 被解释)时添加更多重载,这可能会导致令人惊讶的时间行为。在你的代码库中散布它是自满的。如果它没有散布开来,为什么不直接 if/else 就结束呢?
  3. 您可以以某种方式限制使用,并强制执行它,以便对于给定调用只有一个谓词必须 return 为真。这在很多方面都是奇怪的、低效的和无用的,例如如果你想捕获 A 或其子类的所有实例,但以特殊方式处理子类 C 怎么办?或者,如果您想进一步特化具有额外条件的谓词。你打算如何对这种模型进行分类?

这是根据@Yam 调整的解决方案以适合您的语法,并用作库。决定(这是一个常见的决定)是第一个谓词获胜:

class guarded:
    def __init__(self, default):
        self.funcs = []
        self.default = default

    def when(self, pred):
        def add(func):
            self.funcs.append( (pred, func) )
            return func
        return add

    def __call__(self, *args, **kwargs):
        for pred, func in self.funcs:
            try:  
                match = pred(*args, **kwargs)
            except Exception:
                match = False
            if match:
                return func(*args, **kwargs)
        return self.default(*args, **kwargs)

用户代码:

@guarded
def f(param):
    raise TypeError('Illegal input')

@f.when(lambda param: param > 0)
def f_when_param_positive(param):
    return 'param_positive'

@f.when(lambda param: all(isinstance(item, str) for item in param))
def f_when_param_iterable_of_strings(param):
    return 'param_iterable_of_strings'

尝试一下,我们得到类似的东西:

>>> print(f(123))
param_positive
>>> print(f(['a', 'b']))
param_iterable_of_strings
>>> print(f(-123))
Traceback (most recent call last):
...
TypeError: Illegal input

感谢回复者。在问了这个问题之后,似乎没有现成的模块可以做到这一点。所以我自己写了 :) 它受到@Elazar 的建议的启发。

请随时查看。 It's on PyPI 您可以使用以下方式安装它:

pip install genericfuncs

它也是 hosted on GitHub,我计划在保持 API 简单的同时继续开发和添加功能。欢迎投稿。