Digitação de pato, validação de dados e programação assertiva em Python


10

Sobre a digitação de pato :

A tipagem de pato é auxiliada por habitualmente não testar o tipo de argumentos nos corpos de métodos e funções, baseando-se na documentação, código claro e teste para garantir o uso correto.

Sobre a validação de argumentos (EAFP: Mais fácil pedir perdão do que permissão). Um exemplo adaptado daqui :

... é considerado mais pitônico:

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

Isso significa que qualquer pessoa que use seu código não precisará usar um dicionário ou subclasse real - eles podem usar qualquer objeto que implemente a interface de mapeamento.

Infelizmente, na prática, não é assim tão simples. E se o membro no exemplo acima puder ser um número inteiro? Os números inteiros são imutáveis ​​- portanto, é perfeitamente razoável usá-los como chaves de dicionário. No entanto, eles também são usados ​​para indexar objetos do tipo sequência. Se um membro for um número inteiro, o exemplo dois poderá liberar listas e strings, além de dicionários.

Sobre programação assertiva :

As asserções são uma maneira sistemática de verificar se o estado interno de um programa é o esperado pelo programador, com o objetivo de detectar bugs. Em particular, eles são bons para capturar suposições falsas feitas ao escrever o código ou abusar de uma interface por outro programador. Além disso, eles podem atuar como documentação in-line até certo ponto, tornando óbvias as suposições do programador. ("Explícito é melhor que implícito.")

Os conceitos mencionados às vezes estão em conflito, por isso conto com os seguintes fatores ao escolher se não faço nenhuma validação de dados, faço uma validação forte ou utilizo declarações:

  1. Validação forte. Por forte validação, quero dizer criar uma exceção personalizada ( ApiErrorpor exemplo). Se minha função / método fizer parte de uma API pública, é melhor validar o argumento para mostrar uma boa mensagem de erro sobre tipo inesperado. Ao marcar o tipo, não quero dizer apenas o uso isinstance, mas também se o objeto transmitido suporta a interface necessária (digitação de pato). Enquanto documento a API e especifico o tipo esperado e o usuário pode querer usar minha função de maneira inesperada, me sinto mais seguro quando verifico as suposições. Normalmente, uso isinstancee, se quiser apoiar outros tipos ou patos, alterarei a lógica de validação.

  2. Programação assertiva. Se o meu código é novo, eu uso muito. Quais são os seus conselhos sobre isso? Mais tarde, você remove declarações do código?

  3. Se minha função / método não faz parte de uma API, mas transmite alguns de seus argumentos para outro código não escrito, estudado ou testado por mim, eu faço várias afirmações de acordo com a interface chamada. Minha lógica por trás disso - é melhor falhar no meu código, em algum lugar 10 níveis mais profundo no rastreamento de pilha com erro incompreensível, o que força a depurar muito e depois adicionar a declaração ao meu código de qualquer maneira.

Comentários e conselhos sobre quando usar ou não a validação de tipo / valor, afirma? Desculpe por não ser a melhor formulação da questão.

Por exemplo, considere a seguinte função, onde Customerestá um modelo declarativo 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

Portanto, existem várias maneiras de lidar com a validação:

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

ou

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

Quando e por que você usaria cada uma delas no contexto de digitação de patos, verificação de tipo, validação de dados?


1
Você não deve remover afirma apenas como testes de unidade a menos que por motivo de desempenho
Bryan Chen

Respostas:


4

Deixe-me dar alguns princípios orientadores.

Princípio # 1. Conforme descrito em http://docs.python.org/2/reference/simple_stmts.html, a sobrecarga de desempenho das declarações pode ser removida com uma opção de linha de comando, enquanto ainda existe para depuração. Se o desempenho for um problema, faça isso. Deixe as afirmações. (Mas não faça nada de importante nas afirmações!)

Princípio # 2. Se você estiver afirmando alguma coisa e tiver um erro fatal, use uma afirmação. Não há absolutamente nenhum valor em fazer outra coisa. Se alguém mais tarde quiser mudar isso, poderá alterar seu código ou evitar a chamada do método.

Princípio # 3. Não desaprove algo só porque você acha que é uma coisa estúpida de se fazer. Então, e se o seu método permitir que as strings passem? Se funcionar, funciona.

Princípio # 4. Não permita coisas que sejam sinais de erros prováveis. Por exemplo, considere receber um dicionário de opções. Se esse dicionário contiver itens que não são opções válidas, isso significa que alguém não entendeu sua API ou que ocorreu um erro de digitação. Explodir é mais provável que ocorra um erro de digitação do que impedir alguém de fazer algo razoável.

Com base nos 2 primeiros princípios, sua segunda versão pode ser descartada. Qual dos outros dois você prefere é uma questão de gosto. Qual você acha mais provável? Que alguém passará para quem não é cliente add_customere que as coisas quebrarão (nesse caso, a versão 3 é preferida) ou que alguém em algum momento desejará substituir seu cliente por um objeto proxy de algum tipo que responda a todos os métodos corretos (nesse caso, a versão 1 é preferida).

Pessoalmente, já vi os dois modos de falha. Eu tenderia a ir com a versão 1 do princípio geral de que sou preguiçoso e é menos digitado. (Além disso, esse tipo de falha geralmente tende a aparecer mais cedo ou mais tarde de uma maneira bastante óbvia. E quando eu quero usar um objeto proxy, fico muito irritado com as pessoas que amarraram minhas mãos.) Mas existem programadores que eu respeito quem iria para o outro lado.


Eu prefiro a v.3, especialmente ao projetar a interface - escrevendo novas classes e métodos. Também considero a v.3 útil para métodos de API - porque meu código é novo para outros. Eu acho que a abordagem assertiva é um bom compromisso, porque é removida na produção ao executar no modo otimizado. > Explodir é mais provável que ocorra um erro de digitação do que impedir alguém de fazer algo razoável. <Então, você não se importa de ter essa validação?
warvariuc

Vamos colocar desta forma. Acho que a herança mapeia mal a maneira como eu gosto de evoluir projetos. Eu prefiro composição. Então, evito afirmar que isso deve ser dessa classe. Mas não sou contra as afirmações em que acho que elas me salvam algo.
btilly
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.