Может ли 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
Это лучше?
Ответы
Ответ 1
Ваш вкус может отличаться, но стиль Pythonic (tm) - это просто идти вперед и использовать объекты, как вам нужно. Если они не поддерживают операции, которые вы пытаетесь, будет создано исключение. Это называется утиная печать.
Есть несколько причин для предпочтения этого стиля: во-первых, он позволяет полиморфизм, позволяя вам использовать новые виды объектов с существующим кодом, если новые объекты поддерживают правильные операции. Во-вторых, он упрощает успешный путь, избегая многочисленных проверок.
Конечно, сообщение об ошибке, которое вы получаете при использовании неправильных аргументов, будет более ясным с проверкой типов, чем с утиным набором текста, но, как я сказал, ваш вкус может отличаться.
Ответ 2
Одна из причин, по которой 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
Ответ 3
Если это исключение из правила, это нормально. Но если инженерство/дизайн вашего проекта вращается вокруг проверки типов каждой функции (или большинства из них), возможно, вы не хотите использовать Python, а как же С# вместо этого?
С моей точки зрения, вы создаете декоратор для проверки типа, как правило, означает, что вы собираетесь использовать его много. Таким образом, в то время, когда факторинг общего кода в декораторе является питоническим, тот факт, что он для проверки типов не очень питонов.
Ответ 4
Об этом говорили некоторые, поскольку Py3k поддерживает аннотации функций , аннотации типа которых являются приложением. Также было предпринято попытку отсканировать проверку в Python2.
Я думаю, что это никогда не происходило, потому что основная проблема, которую вы пытаетесь решить ( "найти ошибки типа" ), тривиальна для начала (вы видите TypeError
) или довольно сложно (небольшая разница в типах интерфейсов). Плюс, чтобы понять это правильно, вам нужны классные классы и классифицируйте каждый тип в Python. Это много работает, в основном, ничего. Не говоря уже о том, что вы будете выполнять проверки времени выполнения все время.
У Python уже есть сильная и предсказуемая система типов. Если мы когда-нибудь увидим что-то более мощное, я надеюсь, что это произойдет через аннотации типов и умные IDE.
Ответ 5
В дополнение к уже упомянутым идеям вы можете захотеть "принудить" входные данные к типу, который вам нужен. Например, вы можете преобразовать кортеж координат в массив Numpy, чтобы вы могли выполнять на нем операции с линейной алгеброй. Код принуждения довольно общий:
input_data_coerced = numpy.array(input_data) # Works for any input_data that is a sequence (tuple, list, Numpy array…)
Ответ 6
Это.
"Быть 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
, чтобы использовать его). А что, если вы не просмотрели этот конкретный объект, а замарировали его в файл, который был отправлен в другой отдел и загружен несколькими месяцами позже? К тому времени, когда ошибка будет идентифицирована и исправлена, ваша работа может быть уже в беде...
Таким образом, вам не нужно писать декораторы, проверяющие тип. Для этой цели уже существуют модули, например