检查两个Python函数(或方法)的兼容性

4 投票
3 回答
687 浏览
提问于 2025-04-15 12:23

有没有办法检查两个Python函数是否可以互换?比如说,如果我有

def foo(a, b):
    pass
def bar(x, y):
    pass
def baz(x,y,z):
    pass

我想要一个函数 is_compatible(a,b),当传入foo和bar时返回True,但当传入bar和baz时返回False,这样我就可以在实际调用这两个函数之前检查它们是否可以互换。

3 个回答

1

虽然Python是一种动态类型的语言,但在Python中类型的概念还是很强的(也就是强类型)。而且在引入类型提示后,现在可以检查函数之间是否可以互换。但首先,我们先来说明以下内容:

里氏替换原则

如果类型t2是类型t1的子类型,那么类型t1的对象应该可以被类型t2的对象替代。

Callable类型的协变/逆变:

  • Callable[[], int]Callable[[], float]的子类型(协变)。

    这个比较直观:一个最终返回int的可调用对象可以替代一个返回float的函数(暂时忽略参数列表)。

  • Callable[[float], None]Callable[[int], None]的子类型(逆变)。

    这个有点让人困惑,但请记住,一个处理整数的可调用对象可能会执行一些在浮点数上未定义的操作,比如>><<,而一个处理浮点数的可调用对象肯定不会执行任何在整数上未定义的操作(因为整数是浮点数的子类型)。所以,一个处理浮点数的可调用对象可以替代一个处理整数的可调用对象,但反过来就不行了(忽略返回类型)。

从上面我们可以得出结论:为了让可调用对象c1可以被可调用对象c2替代,以下条件必须满足:

  1. c2的返回类型应该是c1返回类型的子类型。
  2. 对于c1的参数列表:(a1, a2,...an)c2的参数列表:(b1, b2,...bn)a1应该是b1的子类型,a2应该是b2的子类型,依此类推。

实现

一个简单的实现(忽略kwargs和可变长度参数列表):

from inspect import getfullargspec

def issubtype(func1, func2):
  """Check whether func1 is a subtype of func2, i.e func1 could replce func2"""
  spec1, spec2 = getfullargspec(func1), getfullargspec(func2)
  
  if not issubclass(spec1.annotations['return'], spec2.annotations['return']):
    return False
  
  return all((issubclass(spec2.annotations[arg2], spec1.annotations[arg1]) for (arg1, arg2) in zip(spec1.args, spec2.args)))

示例:

from numbers import Integral, Real


def c1(x :Integral) -> Real: 
  pass

def c2(x: Real) -> Integral:
  pass

print(issubtype(c2, c1))
print(issubtype(c1, c2))

class Employee:
  pass

class Manager(Employee):
  pass

def emp_salary(emp :Employee) -> Integral:
  pass

def man_salary(man :Manager) -> Integral:
  pass

print(issubtype(emp_salary, man_salary))
print(issubtype(man_salary, emp_salary))

输出:

True
False
True
False
3

你会根据什么来判断兼容性呢?是参数的数量吗?Python 支持可变数量的参数,所以你永远不知道两个函数在这方面是否兼容。数据类型呢?Python 使用的是鸭子类型(duck typing),这意味着在你使用类似于 isinstance 的测试之前,函数内部对数据类型没有任何限制,所以兼容性测试也无法基于数据类型。

所以简单来说:不可以。

你应该写好文档字符串,这样任何使用你 API 的人都知道他们传给你的函数需要做什么,然后你就可以信任你接收到的函数会正常工作。任何“兼容性”检查要么会排除一些可能有效的函数,要么会让你产生一种“所有东西都正好如你所愿”的错误感觉。

在 Python 中,提供 API 的正确方式是:写好文档,让人们知道他们需要了解的内容,并信任他们会做正确的事情。在关键位置你仍然可以使用 try: except:,但是如果有人因为不想看文档而错误使用你的 API,那就不应该让他们有一种虚假的安全感。而那些确实阅读了文档并想以完全合理的方式使用它的人,也不应该因为他们声明函数的方式而被拒绝使用。

3

看看这个 inspect.getargspec():

inspect.getargspec(func)

这个函数可以获取一个函数的参数名称和默认值。它会返回一个包含四个部分的元组:(args, varargs, varkw, defaults)。其中,args 是一个参数名称的列表(可能还会包含嵌套的列表)。varargs 和 varkw 分别是代表可变参数和关键字参数的名称,或者是 None。defaults 是一个默认参数值的元组,如果没有默认参数,这里会是 None;如果这个元组有 n 个元素,那它们对应的就是 args 列表中最后 n 个参数。

在 2.6 版本中进行了更改:返回一个命名元组 ArgSpec(args, varargs, keywords, defaults)。

撰写回答