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

3

有没有可能检查两个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,这样我就可以在调用它们之前检查它们是否可互换。

1
“兼容”是什么意思?def quux(*args)在哪里适用? - S.Lott
3个回答

4
你将基于什么来确定兼容性?参数数量吗?Python 使用可变长度参数列表,因此在这种情况下您永远不知道两个函数是否兼容。数据类型呢?Python 使用鸭子类型,因此在函数内部使用 isinstance 测试或类似方法之前,不能对数据类型施加任何约束以进行兼容性测试。
简而言之:不需要。
你应该编写良好的文档字符串,以便 API 的任何用户都知道他所提供的函数必须做什么,然后您应该相信您得到的函数的行为是正确的。任何“兼容性”检查都会排除可能有效的函数,或者给您带来“一切都正常”的错误感觉。
暴露 API 的 Pythonic 方式是:编写良好的文档,以便人们知道他们需要知道的内容,并相信他们会做正确的事情。在关键位置上,您仍然可以使用 try except,但是那些滥用您的 API 的人由于他们只是不想阅读文档而被误导的情况不应该被给予虚假的安全感。而那些已经阅读了您的文档并希望以完全可接受的方式使用它的人,不应该因为声明函数的方式不同而被拒绝使用它的可能性。

是的,参数数量。我知道我不能检查更多,但我认为尽早拒绝给定的方法/函数对模块的用户很好(这是一个简单的事件处理机制)。 - Dave Vogt
1
但是这种方式会拒绝一个可能完全有效的函数,只因为它被声明为def func(*args)或def func(a,b,foo="default")。请记住这一点。 - balpha
@balpha:这也可以使用inspect.getargspec进行检查。它会报告是否有*args和**kwargs参数。 - Georg Schölly
是的,但最终你将一无所获。我已经编辑了我的答案,希望能让我的陈述更加清晰明了。 - balpha
1
@adnanmuttaleb 可能吧,但这个答案是从2009年的。也许发表一个考虑到现代Python的新答案会更好。 - balpha
显示剩余3条评论

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)。


哦,非常好……正是我所需要的。谢谢! - Dave Vogt

1
虽然 Python 是一种动态类型的语言,但是它对于类型有一个强烈的概念(强类型)。在引入类型提示后,现在可以检查函数的可互换性。但首先让我们阐述以下内容:
Liskov 替换原则:
如果类型 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接