Это Pythonic, чтобы проверить типы аргументов функции? - PullRequest
19 голосов
/ 23 декабря 2009

Я знаю, что аргументы функции проверки типов обычно не одобряются в 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 ]

28 голосов
/ 23 декабря 2009

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

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

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

9 голосов
/ 23 декабря 2009

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

Вот пример класса, который оборачивает объект. 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
2 голосов
/ 23 декабря 2009

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

По моему мнению, создание декоратора для проверки типов обычно означает, что вы собираетесь его использовать много . Так что в этом случае, хотя включение общего кода в декоратор является питоническим, тот факт, что он предназначен для проверки типов, не очень питоничен.

1 голос
/ 11 сентября 2017

Это.

«Быть ​​Pythonic» не является четко определенной концепцией, но обычно понимается как написание кода с использованием соответствующих языковых конструкций, не будучи более многословным, чем необходимо, следуя руководству по стилю Python (PEP 8), и в целом стремится иметь код это приятно читать. У нас также есть дзен 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, чтобы использовать его). А что, если вы не просматривали этот конкретный объект, а собрали его в файл, который был отправлен в другой отдел и загружен несколько месяцев спустя? К тому времени, когда ошибка будет обнаружена и исправлена, ваша работа уже может быть в беде ...

При этом вам не нужно самим писать декораторы для проверки типов. Специально для этого уже существуют модули, например

1 голос
/ 23 декабря 2009

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

input_data_coerced = numpy.array(input_data)  # Works for any input_data that is a sequence (tuple, list, Numpy array…) 
1 голос
/ 23 декабря 2009

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

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

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

...