Утиная печать, проверка данных и напористое программирование на Python

10

О наборе утки :

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

О проверке аргументов (EAFP: проще просить прощения, чем разрешения). Адаптированный пример отсюда :

... это считается более питоническим:

def my_method(self, key):
    try:
        value = self.a_dict[member]
    except TypeError:
        # do something else

Это означает, что любому, кто использует ваш код, не нужно использовать настоящий словарь или подкласс - он может использовать любой объект, который реализует интерфейс отображения.

К сожалению, на практике это не так просто. Что если член в приведенном выше примере может быть целым числом? Целые числа неизменны - поэтому вполне разумно использовать их в качестве ключей словаря. Однако они также используются для индексации объектов типа последовательности. Если member является целым числом, то во втором примере можно пропустить списки и строки, а также словари.

Об ассертивном программировании:

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

Упомянутые концепции иногда противоречат друг другу, поэтому я полагаюсь на следующие факторы при выборе: вообще не проверять данные, проводить строгую проверку или использовать утверждения:

  1. Сильная проверка. Под строгой проверкой я подразумеваю повышение пользовательского исключения ( ApiErrorнапример). Если моя функция / метод является частью общедоступного API, лучше проверить аргумент, чтобы показать хорошее сообщение об ошибке неожиданного типа. Под проверкой типа я подразумеваю не только использование isinstance, но также и то, что передаваемый объект поддерживает необходимый интерфейс (типизирование утки). Хотя я документирую API и указываю ожидаемый тип, а пользователь может захотеть использовать мою функцию неожиданным образом, я чувствую себя безопаснее, когда проверяю предположения. Я обычно использую, isinstanceи если позже я хочу поддержать других типов или уток, я изменяю логику проверки.

  2. Напористое программирование. Если мой код новый, я использую много утверждений. Каковы ваши советы по этому поводу? Вы позже удаляете утверждения из кода?

  3. Если моя функция / метод не является частью API, но передает некоторые его аргументы другому коду, не написанному, не изученному или протестированному мной, я делаю много утверждений в соответствии с вызываемым интерфейсом. Моя логика в этом - лучше провалиться в моем коде, тогда где-то на 10 уровней глубже в трассировке стека с непонятной ошибкой, которая вынуждает много отлаживать, а потом все равно добавлять утверждение в мой код.

Комментарии и советы о том, когда использовать или не использовать проверку типа / значения, утверждает? Извините за не лучшую формулировку вопроса.

Например, рассмотрим следующую функцию, где Customerдекларативная модель SQLAlchemy:

def add_customer(self, customer):
    """Save new customer into the database.
    @param customer: Customer instance, whose id is None
    @return: merged into global session customer
    """
    # no validation here at all
    # let's hope SQLAlchemy session will break if `customer` is not a model instance
    customer = self.session.add(customer)
    self.session.commit()
    return customer

Итак, есть несколько способов обработки валидации:

def add_customer(self, customer):
    # this is an API method, so let's validate the input
    if not isinstance(customer, Customer):
        raise ApiError('Invalid type')
    if customer.id is not None:
        raise ApiError('id should be None')

    customer = self.session.add(customer)
    self.session.commit()
    return customer

или

def add_customer(self, customer):
    # this is an internal method, but i want to be sure
    # that it's a customer model instance
    assert isinstance(customer, Customer), 'Achtung!'
    assert customer.id is None

    customer = self.session.add(customer)
    self.session.commit()
    return customer

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

warvariuc
источник
1
Вы не должны удалять утверждения так же, как модульные тесты, если только не по причинам производительности
Брайан Чен

Ответы:

4

Позвольте мне привести некоторые руководящие принципы.

Принцип № 1. Как указано в http://docs.python.org/2/reference/simple_stmts.html, издержки производительности для утверждений можно удалить с помощью параметра командной строки, оставаясь при этом для отладки. Если производительность является проблемой, сделайте это. Оставь утверждения. (Но не делайте ничего важного в утверждениях!)

Принцип № 2. Если вы что-то утверждаете и будете иметь фатальную ошибку, используйте assert. Совершенно бесполезно заниматься чем-то другим. Если кто-то позже захочет это изменить, он может изменить ваш код или избежать вызова этого метода.

Принцип № 3. Не запрещайте что-либо только потому, что вы думаете, что это глупо. Так что, если ваш метод пропускает строки? Если это работает, это работает.

Принцип № 4. Запрещать вещи, которые являются признаками вероятных ошибок. Например, рассмотрите возможность передачи словаря опций. Если этот словарь содержит вещи, которые не являются допустимыми параметрами, то это признак того, что кто-то не понял ваш API или имел опечатку. Это может привести к опечатке, а не к тому, чтобы кто-то сделал что-то разумное.

Основываясь на первых двух принципах, ваша вторая версия может быть выброшена. Какой из двух других вы предпочитаете - дело вкуса. Что вы считаете более вероятным? То, что кто-то передаст не-клиента, add_customerи что-то сломается (в этом случае предпочтительна версия 3), или что кто-то в какой-то момент захочет заменить вашего клиента прокси-объектом некоторого вида, который отвечает на все правильные методы (в этом случае версия 1 является предпочтительной).

Лично я видел оба режима отказа. Я бы предпочел перейти с версии 1 из общего принципа, что я ленивый, и он меньше печатает. (Также такого рода ошибки обычно имеют тенденцию обнаруживаться рано или поздно довольно очевидным способом. И когда я хочу использовать прокси-объект, я действительно раздражаюсь на людях, которые связали мне руки.) Но есть программисты, которых я уважаю, кто пошел бы в другую сторону.

btilly
источник
Я предпочитаю v.3, особенно при разработке интерфейса - написание новых классов и методов. Также я считаю v.3 полезным для методов API - потому что мой код является новым для других. Я думаю, что настойчивый подход - хороший компромисс, потому что он убирается в работе при работе в оптимизированном режиме. > Это может привести к опечатке, а не к тому, чтобы кто-то сделал что-то разумное. <Итак, ты не против такой проверки?
варварюк
Скажем так. Я считаю, что наследование плохо соответствует тому, как мне нравится развивать дизайн. Я предпочитаю композицию. Поэтому я уклоняюсь от утверждения, что это должно быть того класса. Но я не против утверждений, где я думаю, что они меня что-то спасают.
Btilly