Может ли Pythonic проверять типы аргументов функции?

Я знаю, что аргументы функции проверки типов обычно не одобряются в Python, но я думаю, что я придумал ситуацию, когда имеет смысл это сделать.

В моем проекте у меня есть абстрактный базовый класс Coord с подклассом Vector, который имеет больше функций, таких как вращение, изменение величины и т.д. Списки и кортежи чисел также вернут True для isinstance(x, Coord). Я также имеют много функций и методов, которые принимают эти типы Координаты в качестве аргументов. Я установил декораторы, чтобы проверить аргументы этих методов. Вот упрощенная версия:

class accepts(object):
 def __init__(self, *types):
 self.types = types
 def __call__(self, func):
 def wrapper(*args):
 for i in len(args):
 if not isinstance(args[i], self.types[i]):
 raise TypeError
 return func(*args)
 return wrapper

Эта версия очень проста, она все еще имеет некоторые ошибки. Это просто для того, чтобы проиллюстрировать суть. И он будет использоваться как:

@accepts(numbers.Number, numbers.Number)
def add(x, y):
 return x + y

Примечание. Я проверяю только типы аргументов в отношении абстрактных базовых классов.

Это хорошая идея? Есть ли лучший способ сделать это без повторения подобного кода в каждом методе?

Edit:

Что делать, если я должен был сделать то же самое, но вместо проверки типов в декораторе я поймаю исключения в декораторе:

class accepts(object):
 def __init__(self, *types):
 self.types = types
 def __call__(self, func):
 def wrapper(*args):
 try:
 return func(*args)
 except TypeError:
 raise TypeError, message
 except AttributeError:
 raise AttributeError, message
 return wrapper

Это лучше?

6 ответов

Ваш вкус может отличаться, но стиль Pythonic (tm) - это просто идти вперед и использовать объекты, как вам нужно. Если они не поддерживают операции, которые вы пытаетесь, будет создано исключение. Это называется утиная печать.

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

Конечно, сообщение об ошибке, которое вы получаете при использовании неправильных аргументов, будет более ясным с проверкой типов, чем с утиным набором текста, но, как я сказал, ваш вкус может отличаться.


Одна из причин, по которой Duck Typing поощряется в Python, заключается в том, что кто-то может обернуть один из ваших объектов, а затем он будет выглядеть не таким, но все же работать.

Вот пример класса, который обертывает объект. A LoggedObject действует всеми способами, такими как объект, который он обертывает, но когда вы вызываете LoggedObject, он регистрирует вызов перед выполнением вызова.

from somewhere import log
from myclass import A
class LoggedObject(object):
 def __init__(self, obj, name=None):
 if name is None:
 self.name = str(id(obj))
 else:
 self.name = name
 self.obj = obj
 def __call__(self, *args, **kwargs):
 log("%s: called with %d args" % (self.name, len(args)))
 return self.obj(*args, **kwargs)
a = LoggedObject(A(), name="a")
a(1, 2, 3) # calls: log("a: called with 3 args")

Если вы явно протестируете для isinstance(a, A), он не сработает, потому что a является экземпляром LoggedObject. Если вы просто позволите утиной печати сделать что-то, это сработает.

Если кто-то ошибочно передает неправильный тип объекта, будет вызвано некоторое исключение, например AttributeError. Исключение может быть более четким, если вы явно проверяете типы, но я думаю, что в целом этот случай является победой для печати уток.

Есть моменты, когда вам действительно нужно проверить тип. Недавно я узнал следующее: когда вы пишете код, который работает с последовательностями, иногда вам действительно нужно знать, есть ли у вас строка или какая-либо другая последовательность. Рассмотрим это:

def llen(arg):
 try:
 return max(len(arg), max(llen(x) for x in arg))
 except TypeError: # catch error when len() fails
 return 0 # not a sequence so length is 0

Предполагается вернуть длинную длину последовательности или любую вложенную внутри нее последовательность. Он работает:

lst = [0, 1, [0, 1, 2], [0, 1, 2, 3, 4, 5, 6]]
llen(lst) # returns 7

Но если вы вызываете llen("foo"), он будет перезаписываться навсегда до.

Проблема в том, что строки имеют особое свойство, что они всегда действуют как последовательность, даже если вы берете наименьший элемент из строки; односимвольная строка по-прежнему является последовательностью. Поэтому мы не можем писать llen() без явного теста для строки.

def llen(arg):
 if isinstance(arg, basestring): # Python 2.x; for 3.x use isinstance(arg, str)
 return len(arg)
 try:
 return max(len(arg), max(llen(x) for x in arg))
 except TypeError: # catch error when len() fails
 return 0 # not a sequence so length is 0


Если это исключение из правила, это нормально. Но если инженерство/дизайн вашего проекта вращается вокруг проверки типов каждой функции (или большинства из них), возможно, вы не хотите использовать Python, а как же С# вместо этого?

С моей точки зрения, вы создаете декоратор для проверки типа, как правило, означает, что вы собираетесь использовать его много. Таким образом, в то время, когда факторинг общего кода в декораторе является питоническим, тот факт, что он для проверки типов не очень питонов.


Это.

"Быть ​​Pythonic" не является четко определенной концепцией, но обычно понимается как написание кода с использованием соответствующих языковых конструкций, а не более подробный, чем необходимо, в соответствии с руководством по стилю Python (PEP 8) и, как правило, стремление иметь код что приятно читать. У нас также есть Zen of Python (import this).

Помогает ли помещать аннотацию @accepts(...) поверх вашей функции или повышает удобочитаемость? Вероятно, помогает, потому что правило №2 говорит "Explicit is better than implicit". Существует также PEP-484, который был специально разработан для точно такой же цели.

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

В частности, правило № 10 "Errors should never pass silently." можно рассматривать как поддерживающее дополнительные проверки типов. В качестве примера рассмотрим следующий простой случай:

class Person:
 def __init__(self, firstname: str, lastname: str = ""):
 self.firstname = firstname
 self.lastname = lastname
 def __repr__(self) -> str:
 return self.firstname + " " + self.lastname

Что происходит, когда вы называете это следующим образом: p = Person("John Smith".split())? Ну, сначала ничего. (Это уже проблематично: был создан недопустимый объект Person, но эта ошибка прошла молча). Затем через некоторое время вы попытаетесь просмотреть человека и получите

>>> print(p)
TypeError: can only concatenate tuple (not "str") to tuple

Если вы просто создали объект, и если вы опытный программист на Python, то вы быстро поймете, что неправильно. Но что, если нет? Сообщение об ошибке является бесполезным (т.е. Вам нужно знать внутренности класса Person, чтобы использовать его). А что, если вы не просмотрели этот конкретный объект, а замарировали его в файл, который был отправлен в другой отдел и загружен несколькими месяцами позже? К тому времени, когда ошибка будет идентифицирована и исправлена, ваша работа может быть уже в беде...

Таким образом, вам не нужно писать декораторы, проверяющие тип. Для этой цели уже существуют модули, например


В дополнение к уже упомянутым идеям вы можете захотеть "принудить" входные данные к типу, который вам нужен. Например, вы можете преобразовать кортеж координат в массив Numpy, чтобы вы могли выполнять на нем операции с линейной алгеброй. Код принуждения довольно общий:

input_data_coerced = numpy.array(input_data) # Works for any input_data that is a sequence (tuple, list, Numpy array…)


Об этом говорили некоторые, поскольку Py3k поддерживает аннотации функций , аннотации типа которых являются приложением. Также было предпринято попытку отсканировать проверку в Python2.

Я думаю, что это никогда не происходило, потому что основная проблема, которую вы пытаетесь решить ( "найти ошибки типа" ), тривиальна для начала (вы видите TypeError) или довольно сложно (небольшая разница в типах интерфейсов). Плюс, чтобы понять это правильно, вам нужны классные классы и классифицируйте каждый тип в Python. Это много работает, в основном, ничего. Не говоря уже о том, что вы будете выполнять проверки времени выполнения все время.

У Python уже есть сильная и предсказуемая система типов. Если мы когда-нибудь увидим что-то более мощное, я надеюсь, что это произойдет через аннотации типов и умные IDE.

licensed under cc by-sa 3.0 with attribution.