Błędny kod Pythona: 10 najczęstszych błędów popełnianych przez programistów Pythona
Opublikowany: 2022-03-11O Pythonie
Python jest interpretowanym, zorientowanym obiektowo, wysokopoziomowym językiem programowania z dynamiczną semantyką. Jego wysokopoziomowe wbudowane struktury danych, w połączeniu z dynamicznym typowaniem i dynamicznym wiązaniem, czynią go bardzo atrakcyjnym dla szybkiego tworzenia aplikacji, a także jako język skryptowy lub klej do łączenia istniejących komponentów lub usług. Python obsługuje moduły i pakiety, zachęcając w ten sposób do modułowości programu i ponownego wykorzystania kodu.
O tym artykule
Prosta, łatwa do nauczenia składnia Pythona może wprowadzić w błąd programistów Pythona – zwłaszcza tych, którzy są nowsi w tym języku – do pominięcia niektórych jego subtelności i niedoceniania potęgi zróżnicowanego języka Python.
Mając to na uwadze, ten artykuł przedstawia listę „top 10” nieco subtelnych, trudniejszych do wyłapania błędów, które mogą ugryźć nawet bardziej zaawansowanych programistów Pythona z tyłu.
(Uwaga: ten artykuł jest przeznaczony dla bardziej zaawansowanych odbiorców niż Typowe błędy programistów Pythona, który jest skierowany bardziej do tych, którzy są nowsi w języku.)
Powszechny błąd nr 1: niewłaściwe używanie wyrażeń jako domyślnych argumentów funkcji
Python pozwala określić, że argument funkcji jest opcjonalny , podając dla niego wartość domyślną . Chociaż jest to świetna funkcja języka, może prowadzić do zamieszania, gdy domyślną wartością jest mutable . Rozważmy na przykład poniższą definicję funkcji Pythona:
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
Częstym błędem jest myślenie, że opcjonalny argument zostanie ustawiony na określone domyślne wyrażenie za każdym razem , gdy funkcja zostanie wywołana bez podania wartości opcjonalnego argumentu. Na przykład w powyższym kodzie można by oczekiwać, że wielokrotne wywoływanie foo()
(tj. bez podania argumentu bar
) zawsze zwróci 'baz'
, ponieważ założeniem byłoby, że za każdym razem , gdy wywoływane jest foo()
(bez bar
podany argument) bar
jest ustawiony na []
(tj. nowa pusta lista).
Ale spójrzmy, co tak naprawdę się dzieje, gdy to zrobisz:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
Co? Dlaczego przy każdym wywołaniu foo()
dodawał domyślną wartość "baz"
do istniejącej listy, zamiast tworzyć za każdym razem nową listę?
Bardziej zaawansowana odpowiedź programowania w Pythonie jest taka, że domyślna wartość argumentu funkcji jest oceniana tylko raz, w momencie definiowania funkcji. Tak więc argument bar
jest inicjowany do wartości domyślnej (tj. pustej listy) tylko wtedy, gdy foo()
jest zdefiniowane po raz pierwszy, ale potem wywołania foo()
(tj. bez określonego argumentu bar
) będą nadal używać tej samej listy do który bar
został pierwotnie zainicjowany.
FYI, typowe obejście tego jest następujące:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
Powszechny błąd nr 2: nieprawidłowe używanie zmiennych klas
Rozważmy następujący przykład:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
Ma sens.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
Tak, znowu zgodnie z oczekiwaniami.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
Co za $%#!& ?? Zmieniliśmy tylko Ax
. Dlaczego Cx
też się zmienił?
W Pythonie zmienne klas są wewnętrznie obsługiwane jako słowniki i są zgodne z tym, co często określa się jako Method Resolution Order (MRO). Tak więc w powyższym kodzie, ponieważ atrybut x
nie znajduje się w klasie C
, zostanie on wyszukany w jego klasach bazowych (w powyższym przykładzie tylko A
, chociaż Python obsługuje wielokrotne dziedziczenie). Innymi słowy, C
nie ma własnej własności x
, niezależnej od A
. Zatem odniesienia do Cx
są w rzeczywistości odniesieniami do Ax
. Powoduje to problem w Pythonie, chyba że jest prawidłowo obsłużony. Dowiedz się więcej o atrybutach klas w Pythonie.
Powszechny błąd nr 3: Niepoprawne określenie parametrów dla bloku wyjątków
Załóżmy, że masz następujący kod:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
Problem polega na tym, except
instrukcjaexcept nie przyjmuje listy wyjątków określonych w ten sposób. Zamiast tego w Pythonie 2.x składnia except Exception, e
jest używana do powiązania wyjątku z opcjonalnym drugim określonym parametrem (w tym przypadku e
), aby udostępnić go do dalszej kontroli. W rezultacie w powyższym kodzie except
IndexError
nie jest przechwytywany przez instrukcjęexcept; zamiast tego wyjątek jest powiązany z parametrem o nazwie IndexError
.
Właściwym sposobem przechwytywania wielu except
w instrukcjiexcept jest określenie pierwszego parametru jako krotki zawierającej wszystkie wyjątki do przechwycenia. Ponadto, aby uzyskać maksymalną przenośność, użyj słowa kluczowego as
, ponieważ ta składnia jest obsługiwana zarówno przez Python 2, jak i Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Powszechny błąd nr 4: niezrozumienie reguł zakresu Pythona
Rozdzielczość zakresu Pythona opiera się na tak zwanej regule LEGB , która jest skrótem od lokalnych, załączając, globalnych , wbudowanych. Wydaje się dość proste, prawda? Cóż, w rzeczywistości istnieje kilka subtelności w sposobie, w jaki działa to w Pythonie, co prowadzi nas do wspólnego, bardziej zaawansowanego problemu programowania w Pythonie poniżej. Rozważ następujące:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
Jaki jest problem?
Powyższy błąd występuje, ponieważ kiedy przypisujesz do zmiennej w zakresie, zmienna ta jest automatycznie uznawana przez Pythona za lokalną w tym zakresie i zasłania każdą zmienną o podobnej nazwie w dowolnym zakresie zewnętrznym.
Wielu jest w związku z tym zaskoczonych, że w poprzednio działającym kodzie pojawia się UnboundLocalError
, gdy jest on modyfikowany przez dodanie instrukcji przypisania gdzieś w ciele funkcji. (Możesz przeczytać więcej na ten temat tutaj.)
Szczególnie często zdarza się to potykać się programistów podczas korzystania z list. Rozważmy następujący przykład:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Co? Dlaczego foo2
bombardowało, podczas gdy foo1
działało dobrze?
Odpowiedź jest taka sama jak w poprzednim przykładzie, ale jest wprawdzie bardziej subtelna. foo1
nie tworzy przypisania do lst
, podczas gdy foo2
tak. Pamiętając, że lst += [5]
jest tak naprawdę tylko skrótem dla lst = lst + [5]
, widzimy, że próbujemy przypisać wartość do lst
(dlatego Python zakłada, że znajduje się w zasięgu lokalnym). Jednak wartość, którą chcemy przypisać do lst
, jest oparta na samym lst
(ponownie, teraz przypuszczalnie znajduje się w zakresie lokalnym), który nie został jeszcze zdefiniowany. Bum.
Powszechny błąd nr 5: Modyfikowanie listy podczas jej iteracji
Problem z następującym kodem powinien być dość oczywisty:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range
Usuwanie elementu z listy lub tablicy podczas iteracji to problem Pythona dobrze znany każdemu doświadczonemu programiście. Ale chociaż powyższy przykład może być dość oczywisty, nawet zaawansowani programiści mogą zostać przypadkowo ukąszeni przez to w znacznie bardziej złożonym kodzie.
Na szczęście Python zawiera szereg eleganckich paradygmatów programowania, które, jeśli są właściwie używane, mogą skutkować znacznie uproszczonym i usprawnionym kodem. Dodatkową korzyścią z tego jest to, że prostszy kod jest mniej podatny na przypadkowe usunięcie-elementu-listy-podczas-iterowania-na- nim. Jednym z takich paradygmatów jest rozumienie list. Co więcej, listy składane są szczególnie przydatne do unikania tego konkretnego problemu, co pokazuje ta alternatywna implementacja powyższego kodu, która działa doskonale:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
Powszechny błąd nr 6: mylenie sposobu, w jaki Python wiąże zmienne w domknięciach
Rozważmy następujący przykład:

>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
Możesz spodziewać się następujących danych wyjściowych:
0 2 4 6 8
Ale w rzeczywistości otrzymujesz:
8 8 8 8 8
Niespodzianka!
Dzieje się tak z powodu późnego wiązania Pythona, który mówi, że wartości zmiennych używanych w domknięciach są sprawdzane w momencie wywołania funkcji wewnętrznej. Tak więc w powyższym kodzie, za każdym razem, gdy wywoływana jest jakakolwiek z zwracanych funkcji, wartość i
jest sprawdzana w otaczającym zakresie w momencie jej wywołania (i do tego czasu pętla została zakończona, więc i
został już przypisany jej końcowy wartość 4).
Rozwiązanie tego powszechnego problemu w Pythonie to trochę hack:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8
Voila! Korzystamy tutaj z domyślnych argumentów, aby wygenerować anonimowe funkcje w celu osiągnięcia pożądanego zachowania. Niektórzy nazwaliby to eleganckim. Niektórzy nazwaliby to subtelnym. Niektórzy go nienawidzą. Ale jeśli jesteś programistą Pythona, w każdym przypadku ważne jest, aby to zrozumieć.
Częsty błąd nr 7: Tworzenie cyklicznych zależności modułów
Załóżmy, że masz dwa pliki, a.py
i b.py
, z których każdy importuje drugi w następujący sposób:
W a.py
:
import b def f(): return bx print f()
A w b.py
:
import a x = 1 def g(): print af()
Najpierw spróbujmy zaimportować a.py
:
>>> import a 1
Działało dobrze. Być może to cię zaskoczy. W końcu mamy tu import kołowy, który prawdopodobnie powinien być problemem, prawda?
Odpowiedź jest taka, że sama obecność cyklicznego importu nie jest sama w sobie problemem w Pythonie. Jeśli moduł został już zaimportowany, Python jest na tyle sprytny, że nie próbuje go ponownie zaimportować. Jednak w zależności od momentu, w którym każdy moduł próbuje uzyskać dostęp do funkcji lub zmiennych zdefiniowanych w drugim, możesz rzeczywiście napotkać problemy.
Wracając więc do naszego przykładu, kiedy importowaliśmy a.py
, nie było problemu z importowaniem b.py
, ponieważ b.py
nie wymaga niczego z a.py
zdefiniowanego w momencie importowania . Jedynym odniesieniem w b.py
do a
jest wywołanie af()
. Ale to wywołanie jest w g()
i nic w a.py
lub b.py
wywołuje g()
. Więc życie jest dobre.
Ale co się stanie, jeśli spróbujemy zaimportować b.py
(bez wcześniejszego zaimportowania a.py
, czyli):
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'
O o. To nie jest dobrze! Problem polega na tym, że w procesie importowania b.py
, próbuje zaimportować a.py
, co z kolei wywołuje f()
, które próbuje uzyskać dostęp do bx
. Ale bx
nie zostało jeszcze zdefiniowane. Stąd wyjątek AttributeError
.
Przynajmniej jedno rozwiązanie tego problemu jest dość trywialne. Po prostu zmodyfikuj b.py
, aby zaimportować a.py
w g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print af()
Nie, kiedy go importujemy, wszystko jest w porządku:
>>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
Powszechny błąd nr 8: zderzenie nazw z modułami biblioteki standardowej Pythona
Jedną z zalet Pythona jest bogactwo modułów bibliotecznych, które są dostarczane „po wyjęciu z pudełka”. Ale w rezultacie, jeśli nie unikasz tego świadomie, nie jest tak trudno napotkać konflikt nazw między nazwą jednego z twoich modułów a modułem o tej samej nazwie w standardowej bibliotece dostarczanej z Pythonem (na przykład , możesz mieć w kodzie moduł o nazwie email.py
, który byłby w konflikcie ze standardowym modułem biblioteki o tej samej nazwie).
Może to prowadzić do poważnych problemów, takich jak importowanie innej biblioteki, która z kolei próbuje zaimportować wersję modułu z biblioteki standardowej Pythona, ale ponieważ masz moduł o tej samej nazwie, inny pakiet błędnie importuje twoją wersję zamiast tej w standardowa biblioteka Pythona. W tym miejscu zdarzają się złe błędy Pythona.
Dlatego należy uważać, aby nie używać tych samych nazw, co w modułach biblioteki standardowej Pythona. Łatwiej jest zmienić nazwę modułu w pakiecie niż złożyć wniosek o ulepszenie Pythona (PEP), aby zażądać zmiany nazwy i spróbować uzyskać zatwierdzenie.
Powszechny błąd nr 9: brak odpowiedzi na różnice między Pythonem 2 a Pythonem 3
Rozważmy następujący plik foo.py
:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
W Pythonie 2 działa to dobrze:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Ale teraz przejdźmy do Pythona 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
Co się tu właśnie wydarzyło? „Problem” polega na tym, że w Pythonie 3 obiekt wyjątku nie jest dostępny poza zakresem bloku except
. (Powodem tego jest to, że w przeciwnym razie zachowałby cykl referencyjny z ramką stosu w pamięci, dopóki moduł odśmiecania pamięci nie uruchomi i nie usunie referencji z pamięci. Więcej szczegółów technicznych na ten temat jest dostępnych tutaj).
Jednym ze sposobów uniknięcia tego problemu jest utrzymanie odwołania do obiektu wyjątku poza zakresem except
, aby pozostał dostępny. Oto wersja poprzedniego przykładu, w której wykorzystano tę technikę, dzięki czemu otrzymujemy kod, który jest przyjazny zarówno dla Pythona 2, jak i Pythona 3:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
Uruchamiam to na Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Jezu!
(Nawiasem mówiąc, nasz Python Hiring Guide omawia szereg innych ważnych różnic, o których należy pamiętać podczas migracji kodu z Pythona 2 do Pythona 3.)
Powszechny błąd nr 10: Niewłaściwe użycie metody __del__
Załóżmy, że masz to w pliku o nazwie mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
A potem próbowałeś to zrobić z another_mod.py
:
import mod mybar = mod.Bar()
Otrzymasz brzydki wyjątek AttributeError
.
Czemu? Ponieważ, jak opisano tutaj, po wyłączeniu interpretera wszystkie zmienne globalne modułu są ustawione na None
. W rezultacie w powyższym przykładzie, w momencie __del__
, nazwa foo
została już ustawiona na None
.
Rozwiązaniem tego nieco bardziej zaawansowanego problemu programowania w Pythonie byłoby użycie zamiast tego atexit.register()
. W ten sposób, kiedy twój program zakończy wykonywanie (to znaczy przy normalnym wyjściu), twoje zarejestrowane programy obsługi zostaną wyrzucone przed zamknięciem interpretera.
Mając to na uwadze, poprawka dla powyższego kodu mod.py
może wtedy wyglądać mniej więcej tak:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Ta implementacja zapewnia czysty i niezawodny sposób wywoływania wszelkich potrzebnych funkcji czyszczenia po normalnym zakończeniu programu. Oczywiście to od foo.cleanup
zależy, co zrobić z obiektem powiązanym z nazwą self.myhandle
, ale masz pomysł.
Zakończyć
Python to potężny i elastyczny język z wieloma mechanizmami i paradygmatami, które mogą znacznie poprawić produktywność. Podobnie jak w przypadku każdego oprogramowania lub języka, posiadanie ograniczonego zrozumienia lub oceny jego możliwości może czasami być bardziej utrudnieniem niż korzyścią, pozostawiając przysłowiowego „wiedzy na tyle, aby być niebezpiecznym”.
Zapoznanie się z kluczowymi niuansami Pythona, takimi jak (ale nie tylko) poruszane w tym artykule umiarkowanie zaawansowane problemy programistyczne, pomoże zoptymalizować użycie języka, jednocześnie unikając niektórych jego typowych błędów.
Możesz również zapoznać się z naszym Przewodnikiem Insider's Guide to Python Interviewing, gdzie znajdziesz sugestie dotyczące pytań, które mogą pomóc w zidentyfikowaniu ekspertów Pythona.
Mamy nadzieję, że wskazówki zawarte w tym artykule okazały się pomocne i czekamy na Twoją opinię.