Przebaczenie w Pythonie vs. pozwolenie i pisanie kaczek

44

W Pythonie często słyszę, że lepiej „błagać o wybaczenie” (wyłapywanie wyjątków) zamiast „pytać o pozwolenie” (sprawdzanie typu / warunku). Jeśli chodzi o wymuszanie pisania kaczek w Pythonie, to jest to

try:
    x = foo.bar
except AttributeError:
    pass
else:
    do(x)

lepszy czy gorszy niż

if hasattr(foo, "bar"):
    do(foo.bar)
else:
    pass

pod względem wydajności, czytelności, „python” lub innego ważnego czynnika?

Darkfeline
źródło
17
jest trzecia opcja, nie rób nic i traktuj dowolne foo bez paska jako błąd
jk.
Pamiętam, że słyszę, które hasattrjest realizowane wewnętrznie z tą dokładną próbą. Nie jestem pewien, czy to prawda ... (działałoby inaczej na nieruchomościach, prawda? Może myślę o getattr...)
Izkata,
@Izkata: Implementacjahasattr używa C-API równoważnego getattr(zwróć, Truejeśli się powiedzie, Falsejeśli nie), ale obsługa wyjątków w C jest znacznie szybsza.
Martijn Pieters
2
Zaakceptowałem odpowiedź Martijna, ale chciałbym dodać, że jeśli próbujesz ustawić atrybut, zdecydowanie powinieneś rozważyć użycie try / catch, ponieważ może to być właściwość bez setera, w którym to przypadku hasattr będzie prawdziwy , ale nadal podniesie AttributeError.
darkfeline

Odpowiedzi:

60

To naprawdę zależy od tego, jak często Twoim zdaniem będzie zgłaszany wyjątek.

Oba podejścia są, moim zdaniem, równie ważne, przynajmniej pod względem czytelności i pytoniczności. Ale jeśli 90% twoich obiektów nie ma atrybutu bar, zauważysz wyraźną różnicę wydajności między tymi dwoma podejściami:

>>> import timeit
>>> def askforgiveness(foo=object()):
...     try:
...         x = foo.bar
...     except AttributeError:
...         pass
... 
>>> def askpermission(foo=object()):
...     if hasattr(foo, 'bar'):
...         x = foo.bar
... 
>>> timeit.timeit('testfunc()', 'from __main__ import askforgiveness as testfunc')
2.9459929466247559
>>> timeit.timeit('testfunc()', 'from __main__ import askpermission as testfunc')
1.0396890640258789

Ale jeśli 90% swoich obiektach nie ma atrybutu, stoły zostały odrzucone:

>>> class Foo(object):
...     bar = None
... 
>>> foo = Foo()
>>> timeit.timeit('testfunc(foo)', 'from __main__ import askforgiveness as testfunc, foo')
0.31336188316345215
>>> timeit.timeit('testfunc(foo)', 'from __main__ import askpermission as testfunc, foo')
0.4864199161529541

Dlatego z punktu widzenia wydajności musisz wybrać podejście, które najlepiej pasuje do twoich okoliczności.

Ostatecznie pewne strategiczne użycie timeitmodułu może być najbardziej pytoniczną rzeczą, jaką możesz zrobić.

Martijn Pieters
źródło
1
Kilka tygodni temu zadałem to pytanie: programmers.stackexchange.com/questions/161798/... Tam zapytałem, czy w luźno pisanych językach trzeba dodatkowo popracować nad sprawdzaniem typów, a ja byłem bombardowany przez ludzi, którzy twierdzili, że nie. Wiem, że cię widzę.
Tulains Córdova,
@ user1598390: Gdy zdefiniujesz interfejs API, który oczekuje jednorodnej kombinacji typów, musisz wykonać kilka testów. Przez większość czasu nie. Obawiam się, że jest to konkretny obszar, z którego nie można wywodzić reguł dotyczących paradygmatów jako całości.
Martijn Pieters,
Cóż, każdy poważny rozwój systemu wymaga zdefiniowania API. Sądzę więc, że najlepiej nadają się do tego języki ścisłe, ponieważ musisz mniej kodować, ponieważ kompilator sprawdza typy w czasie kompilacji.
Tulains Córdova,
1
@GarethRees: ustanawia wzorzec dla drugiej połowy odpowiedzi, w której przekazuję argument do testowanej funkcji.
Martijn Pieters
1
Zauważ hasattr, że w rzeczywistości robi to w C-api odpowiednik try-poza tym pod maską, ponieważ okazuje się, że jedynym ogólnym sposobem ustalenia, czy obiekt ma atrybut w Pythonie, jest próba dostępu do niego.
user2357112,
11

W Pythonie często osiągasz lepszą wydajność, robiąc rzeczy w taki sposób. W przypadku innych języków stosowanie wyjątków do kontroli przepływu jest ogólnie uważane za okropny pomysł, ponieważ wyjątki zwykle nakładają nadzwyczajne koszty ogólne. Ponieważ jednak ta technika jest wyraźnie zatwierdzona w języku Python, interpreter jest zoptymalizowany dla tego typu kodu.

Podobnie jak w przypadku wszystkich pytań dotyczących wydajności, jedynym sposobem na uzyskanie pewności jest profilowanie kodu. Napisz obie wersje i zobacz, która z nich działa szybciej. Chociaż z mojego doświadczenia wynika, że ​​„sposób w języku Python” jest zazwyczaj najszybszym sposobem.

tylerl
źródło
3

Wydaje mi się, że wydajność jest kwestią drugorzędną. Jeśli się pojawi, profiler pomoże ci skupić się na prawdziwych wąskich gardłach, które mogą, ale nie muszą, traktować możliwe nielegalne argumenty.

Z drugiej strony czytelność i prostota zawsze stanowią główny problem. Nie ma tutaj twardych zasad, po prostu skorzystaj z własnego osądu.

Jest to problem uniwersalny, ale istotne są konwencje specyficzne dla środowiska lub języka. Na przykład w Pythonie zazwyczaj wystarczy po prostu użyć oczekiwanego atrybutu i pozwolić potencjalnemu atrybutowi AttributeError dotrzeć do dzwoniącego.

orip
źródło
-1

Jeśli chodzi o poprawność , myślę, że rozwiązaniem jest obsługa wyjątków (czasem jednak sam używam metody hasattr ()). Podstawowym problemem związanym z poleganiem na hasattr () jest to, że zamienia naruszenia kontraktów w cichą awarię (jest to duży problem w JavaScript, który nie rzuca nieistniejących właściwości).

Joe
źródło
3
Wydaje się, że twoja odpowiedź nie wykracza poza to, co już powiedzieli inni. W przeciwieństwie do innych stron, programiści oczekują odpowiedzi, które wyjaśniają, dlaczego za nimi odpowiadają. Poruszasz kwestię cichego niepowodzenia, ale nie zapewniaj sprawiedliwości. Przeczytanie poniższych
Ostatnia odpowiedź, którą udzieliłem, została skrytykowana za zbyt szeroką. Myślałem, że spróbuję krótko i zwięźle.
Joe