Wpisywanie kaczek, sprawdzanie poprawności danych i asertywne programowanie w języku Python

10

O pisaniu kaczek :

Wpisywaniu kaczek pomaga zwykle nie testować typów argumentów w metodach i funkcjach, polegając na dokumentacji, czytelnym kodzie i testowaniu w celu zapewnienia poprawnego użycia.

Informacje na temat sprawdzania poprawności argumentów (EAFP: Łatwiej prosić o wybaczenie niż pozwolenie). Dostosowany przykład stąd :

... uważa się za bardziej pytoniczne:

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

Oznacza to, że nikt inny używający twojego kodu nie musi używać prawdziwego słownika lub podklasy - może użyć dowolnego obiektu, który implementuje interfejs mapowania.

Niestety w praktyce nie jest to takie proste. Co jeśli członek w powyższym przykładzie może być liczbą całkowitą? Liczby całkowite są niezmienne - więc używanie ich jako kluczy słownikowych jest całkowicie rozsądne. Są one jednak również używane do indeksowania obiektów typu sekwencji. Jeśli element członkowski jest liczbą całkowitą, wówczas przykład drugi może przepuścić listy i ciągi znaków, a także słowniki.

O programowaniu asertywnym :

Asercje to systematyczny sposób sprawdzania, czy wewnętrzny stan programu jest zgodny z oczekiwaniami programisty, w celu wykrycia błędów. W szczególności są one przydatne do wychwytywania fałszywych założeń poczynionych podczas pisania kodu lub nadużywania interfejsu przez innego programistę. Ponadto mogą do pewnego stopnia działać jako dokumentacja online, czyniąc założenia programisty oczywistymi. („Jawne jest lepsze niż niejawne.”)

Wspomniane pojęcia są czasami w konflikcie, więc liczę na następujące czynniki przy podejmowaniu decyzji, czy w ogóle nie przeprowadzam weryfikacji danych, nie przeprowadzam silnej weryfikacji lub używam stwierdzeń:

  1. Silna walidacja. Przez silną walidację mam na myśli wprowadzenie niestandardowego wyjątku ( ApiErrorna przykład). Jeśli moja funkcja / metoda jest częścią publicznego interfejsu API, lepiej zweryfikować argument, aby wyświetlić dobry komunikat o błędzie o nieoczekiwanym typie. Przez sprawdzenie typu nie mam na myśli tylko używania isinstance, ale także tego, czy przekazywany obiekt obsługuje wymagany interfejs (pisanie kaczką). Chociaż dokumentuję interfejs API i określam oczekiwany typ, a użytkownik może nieoczekiwanie skorzystać z mojej funkcji, czuję się bezpieczniej, gdy sprawdzam założenia. Zwykle używam isinstancei jeśli później chcę obsługiwać inne typy lub kaczki, zmieniam logikę sprawdzania poprawności.

  2. Programowanie asertywne. Jeśli mój kod jest nowy, często używam twierdzeń. Jakie są na to twoje porady? Czy później usuwasz twierdzenia z kodu?

  3. Jeśli moja funkcja / metoda nie jest częścią API, ale przekazuje niektóre z jej argumentów do innego kodu, który nie został napisany, przestudiowany lub przetestowany przeze mnie, robię wiele twierdzeń zgodnie z wywoływanym interfejsem. Moja logika za tym - lepiej zawieść w moim kodzie, a następnie gdzieś o 10 poziomów głębiej w stosie śledzenia z niezrozumiałym błędem, który zmusza do częstego debugowania, a następnie i tak dodawania aser do mojego kodu.

Komentarze i porady dotyczące tego, kiedy używać sprawdzania poprawności typu / wartości, czy nie? Przepraszamy za najlepsze sformułowanie pytania.

Rozważmy na przykład następującą funkcję, gdzie Customerjest model deklaratywny 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

Istnieje kilka sposobów obsługi sprawdzania poprawności:

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

lub

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

Kiedy i dlaczego miałbyś używać każdego z nich w kontekście pisania kaczek, sprawdzania typów, sprawdzania poprawności danych?

warvariuc
źródło
1
nie powinieneś usuwać twierdzeń podobnie jak testów jednostkowych, chyba że z powodów wydajnościowych
Bryan Chen

Odpowiedzi:

4

Pozwól, że podam kilka zasad przewodnich.

Zasada nr 1. Jak opisano w http://docs.python.org/2/reference/simple_stmts.html narzuty związane z wydajnością asertów można usunąć za pomocą opcji wiersza poleceń, pozostając przy tym do debugowania. Jeśli wydajność stanowi problem, zrób to. Pozostaw twierdzenia. (Ale nie rób nic ważnego w twierdzeniach!)

Zasada nr 2. Jeśli coś twierdzisz i wystąpi błąd krytyczny, użyj potwierdzenia. Robienie czegoś innego nie ma absolutnie żadnej wartości. Jeśli ktoś później chce to zmienić, może zmienić kod lub uniknąć wywołania tej metody.

Zasada nr 3. Nie zakazuj czegoś tylko dlatego, że uważasz, że jest to głupota. Co jeśli twoja metoda pozwala na ciągi znaków? Jeśli to działa, to działa.

Zasada nr 4. Odrzuć rzeczy, które są oznakami prawdopodobnych błędów. Na przykład rozważ przesłanie słownika opcji. Jeśli ten słownik zawiera rzeczy, które nie są prawidłowymi opcjami, oznacza to, że ktoś nie zrozumiał Twojego interfejsu API lub miał literówkę. Wysadzenie w to bardziej prawdopodobne jest złapanie literówki niż powstrzymanie kogoś przed zrobieniem czegoś rozsądnego.

W oparciu o pierwsze 2 zasady twoja druga wersja może zostać wyrzucona. Który z pozostałych dwóch wolisz, to kwestia gustu. Który według Ciebie jest bardziej prawdopodobny? Że ktoś przekaże osobę niebędącą klientem add_customeri wszystko się zepsuje (w takim przypadku preferowana jest wersja 3), lub że ktoś w pewnym momencie będzie chciał zastąpić klienta jakimś obiektem proxy, który odpowiada na wszystkie właściwe metody (w takim przypadku preferowana jest wersja 1).

Osobiście widziałem oba tryby awarii. Zwykle wybieram wersję 1 z ogólnej zasady, że jestem leniwy i że mniej piszę. (Również tego rodzaju porażka zwykle pojawia się prędzej czy później w dość oczywisty sposób. A kiedy chcę użyć obiektu proxy, denerwuję się bardzo na ludzi, którzy związali mi ręce.) Ale są programiści, których szanuję poszedłby w drugą stronę.

btilly
źródło
Wolę v.3, szczególnie przy projektowaniu interfejsu - pisaniu nowych klas i metod. Uważam również, że v.3 jest użyteczny dla metod API - ponieważ mój kod jest nowy dla innych. Myślę, że asertywne podejście jest dobrym kompromisem, ponieważ zostaje usunięte podczas produkcji w trybie zoptymalizowanym. > Wysadzenie tego częściej zapala literówkę niż powstrzymuje kogoś od zrobienia czegoś sensownego. <Więc nie masz nic przeciwko takiej weryfikacji?
warvariuc
Ujmijmy to w ten sposób. Uważam, że dziedziczenie słabo odwzorowuje sposób, w jaki lubię ewoluować projekty. Wolę kompozycję. Dlatego unikam twierdzenia, że ​​to musi być ta klasa. Ale nie jestem przeciwny twierdzeniom, w których myślę, że coś mi uratują.
btilly,