Проверка совместимости двух функций (или методов) Python

Есть ли возможность проверить, взаимозаменяемы ли две функции Python? Например, если у меня есть

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

Мне нужна функция is_compatible(a,b), которая возвращает True при передаче foo и bar, но False при передаче bar и baz, чтобы я мог проверить, взаимозаменяемы ли они, прежде чем вызывать любой из них.


person Dave Vogt    schedule 20.06.2009    source источник
comment
Что значит совместимый? Куда подходит def quux(*args)?   -  person S.Lott    schedule 20.06.2009


Ответы (3)


Взгляните на inspect.getargspec():

inspect.getargspec(func)

Получить имена и значения по умолчанию для аргументов функции. Возвращается кортеж из четырех элементов: (args, varargs, varkw, defaults). args — список имен аргументов (может содержать вложенные списки). varargs и varkw — имена аргументов * и ** или None. defaults — кортеж значений аргументов по умолчанию или None, если аргументов по умолчанию нет; если этот кортеж имеет n элементов, они соответствуют последним n элементам, перечисленным в args.

Изменено в версии 2.6: возвращает именованный кортеж ArgSpec(аргументы, переменные аргументы, ключевые слова, значения по умолчанию).

person Georg Schölly    schedule 20.06.2009
comment
о, очень красиво... именно то, что мне было нужно. Спасибо! - person Dave Vogt; 20.06.2009

На чем бы вы основывали совместимость? Количество аргументов? Python имеет списки аргументов переменной длины, поэтому вы никогда не знаете, могут ли две функции быть совместимыми в этом смысле. Типы данных? Python использует утиную типизацию, поэтому, пока вы не используете тест isinstance или аналогичный внутри функции, нет никаких ограничений на типы данных, на которых может быть основан тест на совместимость.

Итак, вкратце: нет.

Вам лучше написать хорошие строки документации, чтобы любой пользователь вашего API знал, что должна делать функция, которую он вам дает, и тогда вы должны быть уверены, что функция, которую вы получаете, ведет себя правильно. Любая проверка «совместимости» либо исключит возможные допустимые функции, либо даст вам ложное ощущение, что «все именно так, как должно быть».

Питонический способ представления API: напишите хорошую документацию, чтобы люди знали, что им нужно знать, и верили, что они поступают правильно. В критических ситуациях вы по-прежнему можете использовать try: except:, но любой, кто злоупотребляет вашим API, потому что ему просто неинтересно читать документ, не должен испытывать ложного чувства безопасности. И кто-то, кто прочитал ваш документ и хочет использовать его полностью приемлемым способом, не должен быть лишен возможности использовать его на основании того, как он объявил функцию.

person balpha    schedule 20.06.2009
comment
Да, количество аргументов. Я знаю, что не могу проверить гораздо больше, но я думаю, что пользователю модуля будет приятно отклонить данный метод/функцию как можно раньше (это простой механизм обработки событий) - person Dave Vogt; 20.06.2009
comment
Но таким образом вы отклоните вполне допустимую функцию только потому, что она объявлена ​​как def func(*args) или def func(a,b,foo=default). Просто имейте это в виду. - person balpha; 20.06.2009
comment
@balpha: это тоже можно проверить с помощью inspect.getargspec. Он сообщает, есть ли аргументы *args и **kwargs. - person Georg Schölly; 20.06.2009
comment
Да, но вы бы остались без информации вообще. Я отредактировал свой ответ, чтобы, надеюсь, сделать мое заявление немного ясным. - person balpha; 20.06.2009
comment
+1, у вас есть очень важное замечание, о котором следует хотя бы подумать, прежде чем внедрять такую ​​​​функцию. - person Georg Schölly; 20.06.2009
comment
Я думаю, что этот ответ должен учитывать аннотации типов. - person adnanmuttaleb; 28.07.2020
comment
@adnanmuttaleb Возможно, но этот ответ датирован 2009 годом ???? Может быть, опубликовать новый ответ, учитывающий современный Python. - person balpha; 28.07.2020
comment
@balpha извините, но я этого не заметил, я попробую. - person adnanmuttaleb; 28.07.2020

Хотя 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 должно быть 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
person adnanmuttaleb    schedule 28.07.2020