Używanie try vs if w pythonie

145

Czy istnieje uzasadnienie do zadecydować, który z trylub ifkonstrukcje do wykorzystania, podczas testowania zmienna ma wartość?

Na przykład istnieje funkcja, która zwraca listę lub nie zwraca wartości. Chcę sprawdzić wynik przed przetworzeniem. Która z poniższych byłaby lepsza i dlaczego?

result = function();
if (result):
    for r in result:
        #process items

lub

result = function();
try:
    for r in result:
        #process items
except TypeError:
    pass;

Powiązana dyskusja:

Sprawdzanie istnienia elementu członkowskiego w Pythonie

artdanil
źródło
Pamiętaj, że jeśli twoja sekcja #process items może rzucić TypeError, musisz zawinąć inną próbę: z wyjątkiem: block. W tym konkretnym przykładzie
użyłbym

Odpowiedzi:

237

Często słyszysz, że Python zachęca stylu EAFP („łatwiej jest prosić o przebaczenie niż o pozwolenie”) zamiast stylu LBYL („spójrz, zanim skoczysz ”). Dla mnie to kwestia wydajności i czytelności.

W twoim przykładzie (powiedz, że zamiast zwracać listę lub pusty ciąg, funkcja miała zwrócić listę lub None), jeśli spodziewasz się, że 99% czasu resultfaktycznie będzie zawierało coś iterowalnego, zastosowałbym try/exceptpodejście. Będzie szybciej, jeśli wyjątki są naprawdę wyjątkowe. Jeśli resultjest to Nonewięcej niż 50% czasu, użycie ifjest prawdopodobnie lepsze.

Aby wesprzeć to kilkoma pomiarami:

>>> import timeit
>>> timeit.timeit(setup="a=1;b=1", stmt="a/b") # no error checking
0.06379691968322732
>>> timeit.timeit(setup="a=1;b=1", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.0829463709378615
>>> timeit.timeit(setup="a=1;b=0", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.5070195056614466
>>> timeit.timeit(setup="a=1;b=1", stmt="if b!=0:\n a/b")
0.11940114974277094
>>> timeit.timeit(setup="a=1;b=0", stmt="if b!=0:\n a/b")
0.051202772912802175

Tak więc, podczas gdy ifwyciąg zawsze kosztuje, skonfigurowanie try/exceptbloku jest prawie darmowe . Ale kiedy Exceptionfaktycznie nastąpi, koszt jest znacznie wyższy.

Morał:

  • Jest całkowicie OK (i „pythonowy”) w użyciu try/exceptdo sterowania przepływem,
  • ale ma to sens najbardziej, gdy Exceptionsą naprawdę wyjątkowe.

Z dokumentacji Pythona:

EAFP

Łatwiej prosić o przebaczenie niż o pozwolenie. Ten wspólny styl kodowania w Pythonie zakłada istnienie prawidłowych kluczy lub atrybutów i wyłapuje wyjątki, jeśli założenie okaże się fałszywe. Ten czysty i szybki styl charakteryzuje się obecnością wielu tryi exceptwypowiedzi. Technika kontrastuje ze stylem LBYL wspólnym dla wielu innych języków, takich jak C.

Tim Pietzcker
źródło
1
Dziękuję Ci. Widzę, że w tym przypadku uzasadnieniem może być oczekiwanie wyniku.
artdanil
6
.... i to jest jeden (z wielu) powodów, dla których tak trudno jest zrobić prawdziwy optymalizujący JIT dla Pythona. Jak niedawno pokazał LuaJIT 2 w wersji beta, języki dynamiczne mogą być naprawdę, bardzo szybkie; ale w dużej mierze zależy to od początkowego projektu języka i stylu, który promuje. (w związku z tym projekt języka jest powodem, dla którego nawet najlepsze JIT JavaScirpt nie mogą się równać z LuaJIT 1, a znacznie mniej 2)
Javier
1
@ 2rs2ts: Po prostu sam zrobiłem podobne czasy. W Pythonie 3 try/exceptbył o 25% szybszy niż if key in d:w przypadkach, gdy klucz był w słowniku. Było znacznie wolniej, gdy klucza nie było w słowniku, zgodnie z oczekiwaniami i zgodnie z tą odpowiedzią.
Tim Pietzcker
5
Wiem, że to stara odpowiedź, jednak: używanie instrukcji jak w 1/1przypadku czasu nie jest dobrym wyborem, ponieważ zostaną one zoptymalizowane (zobacz dis.dis('1/1')i zauważ, że nie następuje podział).
Andrea Corbellini
1
Zależy to również od operacji, którą chcesz przeprowadzić. Pojedynczy podział jest szybki, wyłapanie błędu jest dość kosztowne. Ale nawet w przypadkach, gdy koszt obsługi wyjątków jest akceptowalny, dokładnie zastanów się, o co prosisz komputer. Jeśli sama operacja jest bardzo droga, niezależnie od jej wyników, a istnieje tani sposób na stwierdzenie, czy sukces jest możliwy: użyj go. Przykład: nie kopiuj połowy pliku tylko po to, aby zdać sobie sprawę, że jest za mało miejsca.
Bachsau
14

Twoja funkcja nie powinna zwracać typów mieszanych (tj. Listy lub pustego łańcucha). Powinien zwrócić listę wartości lub tylko pustą listę. Wtedy nie musiałbyś niczego testować, tj. Twój kod zwija się do:

for r in function():
    # process items
Brandon
źródło
2
Całkowicie się z tobą zgadzam w tej kwestii; Jednak funkcja nie jest moja i po prostu jej używam.
artdanil
2
@artdanil: Więc możesz zapakować tę funkcję w jedną, którą napiszesz, która działa tak, jak ta, o której myśli Brandon Corfman.
quamrana
24
co nasuwa pytanie: czy opakowanie powinno używać warunku „if”, czy „próby” obsługi przypadku, którego nie można powtórzyć?
jcdyer
@quamrana, czyli dokładnie to, co próbuję zrobić. Tak więc, jak zauważył @jcd, pytanie dotyczy tego, jak mam radzić sobie z sytuacją w funkcji opakowania.
artdanil
12

Proszę zignorować moje rozwiązanie, jeśli dostarczony przeze mnie kod nie jest na pierwszy rzut oka oczywisty i musisz przeczytać wyjaśnienie po przykładzie kodu.

Czy mogę założyć, że „brak zwróconej wartości” oznacza, że ​​zwracana wartość to Brak? Jeśli tak, lub jeśli „brak wartości” jest wartością logiczną False, możesz wykonać następujące czynności, ponieważ Twój kod zasadniczo traktuje „brak wartości” jako „nie iteruj”:

for r in function() or ():
    # process items

Jeśli function()zwraca coś, co nie jest prawdziwe, iterujesz po pustej krotce, tj. Nie uruchamiasz żadnych iteracji. Zasadniczo jest to LBYL.

tzot
źródło
4

Twój drugi przykład jest zepsuty - kod nigdy nie zgłosi wyjątku TypeError, ponieważ możesz iterować zarówno przez łańcuchy, jak i listy. Iterowanie po pustym łańcuchu lub liście jest również prawidłowe - spowoduje to zerowe wykonanie treści pętli.

Dave Kirby
źródło
4

Która z poniższych byłaby lepsza i dlaczego?

W tym przypadku preferowana jest opcja Look Before You Leap. Z wyjątkiem podejścia TypeError może wystąpić w dowolnym miejscu w treści pętli i zostałby przechwycony i wyrzucony, co nie jest tym, czego chcesz i utrudnia debugowanie.

(Zgadzam się jednak z Brandonem Corfmanem: zwracanie Brak dla „brak elementów” zamiast pustej listy jest zepsute. Jest to nieprzyjemny zwyczaj programistów Java, którego nie powinno się widzieć w Pythonie. Lub w Javie).

bobince
źródło
4

Ogólnie odnoszę wrażenie, że wyjątki powinny być zarezerwowane dla wyjątkowych okoliczności. Jeśli resultoczekuje się, że nigdy nie będzie pusty (ale może być, na przykład, gdy dysk się zawiesił itp.), Drugie podejście ma sens. Z drugiej strony, jeśli pusty resultjest całkowicie rozsądny w normalnych warunkach, przetestuj go za pomocąif instrukcji ma większy sens.

Miałem na myśli (bardziej powszechny) scenariusz:

# keep access counts for different files
file_counts={}
...
# got a filename somehow
if filename not in file_counts:
    file_counts[filename]=0
file_counts[filename]+=1

zamiast odpowiednika:

...
try:
    file_counts[filename]+=1
except KeyError:
    file_counts[filename]=1
Managu
źródło
Oto przykład różnicy między podejściami, o których wspomina Tim Pietzcker: Pierwsze to LBYL, drugie to
EAFP
Tylko krótka notatka, ++która nie działa w Pythonie, użyj += 1zamiast tego.
tgray
3

bobince mądrze wskazuje, że zawijanie drugiej sprawy może również wychwycić TypeErrors w pętli, co nie jest tym, czego chcesz. Jeśli jednak naprawdę chcesz użyć try, możesz przetestować, czy jest iterowalny przed pętlą

result = function();
try:
    it = iter(result)
except TypeError:
    pass
else:
    for r in it:
        #process items

Jak widać, jest raczej brzydki. Nie sugeruję tego, ale należy to wspomnieć dla kompletności.

AFoglia
źródło
Moim zdaniem celowe wpadanie na błędy typu jest zawsze złym stylem kodowania. Programista powinien wiedzieć, czego się spodziewać i być przygotowany do obsługi tych wartości bez wyjątków. Ilekroć operacja zrobiła to, co miała zrobić (z punktu widzenia programisty), a oczekiwane wyniki są listą lub Nonezawsze sprawdzaj wynik za pomocą is Nonelub is not None. Z drugiej strony, zgłaszanie wyjątków dla uzasadnionych wyników jest również złym stylem. Wyjątki dotyczą nieoczekiwanych rzeczy. Przykład: str.find()zwraca wartość -1, jeśli nic nie zostanie znalezione, ponieważ samo wyszukiwanie zakończyło się bez błędów.
Bachsau
1

Jeśli chodzi o wydajność, użycie bloku try dla kodu, który normalnie nie generuje wyjątków, jest szybsze niż użycie instrukcji if za każdym razem. Tak więc decyzja zależy od prawdopodobieństwa wyjątkowych przypadków.

grayger
źródło
-5

Zgodnie z ogólną zasadą, nigdy nie powinieneś używać try / catch ani żadnych rzeczy obsługujących wyjątki do kontrolowania przepływu. Mimo że iteracja za kulisami jest kontrolowana przez wywoływanie StopIterationwyjątków, nadal powinieneś preferować pierwszy fragment kodu od drugiego.

ilowe
źródło
7
To prawda w Javie. Python dość mocno obejmuje EAFP.
fengb
3
To wciąż dziwna praktyka. Z mojego doświadczenia wynika, że ​​mózgi większości programistów są zaprogramowane tak, aby pomijać EAFP. W końcu zastanawiają się, dlaczego wybrano określoną ścieżkę. To powiedziawszy, jest czas i miejsce, aby złamać każdą zasadę.
ilowe