7.1 Niepoprawne użycie wyrażeń jako wartości domyślnych dla argumentów funkcji
Już się wielu nauczyłeś, więc przeanalizujmy najczęstsze błędy początkujących – lepiej uczyć się na cudzych błędach niż na swoich – prawda?
Python pozwala na określenie, że funkcja może mieć opcjonalne argumenty przez nadanie im wartości domyślnych. To jest oczywiście bardzo wygodna cecha języka, ale może prowadzić do nieprzyjemnych konsekwencji, jeśli typ tej wartości jest modyfikowalny. Na przykład, rozważmy następującą definicję funkcji:
def foo(bar=[]): # bar - to opcjonalny argument
# i domyślnie jest pustą listą.
bar.append("baz") # ta linia może być problematyczna...
return bar
Popularnym błędem w tym przypadku jest myślenie, że wartość opcjonalnego argumentu będzie ustawiana na wartość domyślną za każdym razem, gdy funkcja zostanie wywołana bez wartości dla tego argumentu.
W powyższym kodzie, na przykład, można przypuszczać, że ponowne wywołanie funkcji foo()
(czyli bez wskazywania wartości dla argumentu bar
) zawsze zwróci ["baz"]
, ponieważ zakłada się, że za każdym razem, gdy wywołuje się foo()
(bez wskazania argumentu bar
), bar
jest ustawiane na []
(czyli nową pustą listę).
Ale zobaczmy, co się naprawdę dzieje:
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz", "baz"]
result = foo()
print(result) # ["baz", "baz", "baz"]
Dlaczego funkcja nadal dodaje wartość baz
do istniejącej listy za każdym razem, gdy wywołuje się foo()
zamiast tworzyć nową listę za każdym razem?
Odpowiedzią na to pytanie będzie głębsze zrozumienie, co dzieje się w Pythonie „pod maską”. Mianowicie: wartość domyślna dla funkcji jest inicjalizowana tylko raz, w momencie definicji funkcji.
W ten sposób, argument bar
jest inicjalizowany domyślnie (czyli pustą listą) tylko wtedy, gdy foo()
jest definiowana po raz pierwszy, ale kolejne wywołania foo()
(czyli bez wskazania argumentu bar
) będą nadal korzystać z tej samej listy, która została utworzona dla argumentu bar
w momencie pierwszej definicji funkcji.
Dla informacji, popularną „obejściem” dla tego błędu jest następująca definicja:
def foo(bar=None):
if bar is None:
bar = []
bar.append("baz")
return bar
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
7.2 Niepoprawne użycie zmiennych klasy
Przeanalizujmy następny przykład:
class A(object):
x = 1
class B(A):
pass
class C(A):
pass
print(A.x, B.x, C.x)
# 1 1 1
Wszystko wydaje się w porządku.
Teraz przypiszmy wartość do pola x
w klasie B
:
B.x = 2
print(A.x, B.x, C.x)
# 1 2 1
Wszystko zgodnie z oczekiwaniami.
A teraz zmienimy zmienną klasy A
:
A.x = 3
print(A.x, B.x, C.x)
# 3 2 3
Dziwne, przecież tylko zmieniliśmy A.x
. Dlaczego więc C.x
też się zmieniło?
W Pythonie zmienne klasy są traktowane jak słowniki i postępują zgodnie z tym, co często nazywa się Porządkiem rozwiązywania metod (MRO). W związku z tym, w powyższym kodzie, ponieważ atrybut x
nie został znaleziony w klasie C
, to zostanie znaleziony w jego klasach bazowych (w powyższym przykładzie tylko A
, chociaż Python obsługuje dziedziczenie wielokrotne).
Innymi słowy, C
nie ma własnej właściwości x
, niezależnej od A
. Odwołania do C.x
w rzeczywistości są odwołaniami do A.x
. Może to powodować problemy, jeśli się nie uwzględni takich przypadków należycie. Przy nauce Pythona zwróć szczególną uwagę na atrybuty klasy i pracę z nimi.
7.3 Niepoprawne wskazywanie parametrów dla bloku wyjątków
Załóżmy, że mamy następujący fragment kodu:
try:
l = ["a", "b"]
int(l[2])
except ValueError, IndexError: # Chcemy złapać oba wyjątki, prawda?
pass
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
Problem polega na tym, że stwierdzenie except
nie przyjmuje listy wyjątków podanych w ten sposób. W rezultacie w powyższym kodzie wyjątek IndexError
nie jest przechwytywany przez stwierdzenie except
; zamiast tego wyjątek kończy się przypisaniem do parametru o nazwie IndexError
.
Ważne!
Taki kod możesz spotkać w przykładach w internecie, ponieważ był używany w Pythonie 2.x.
Prawidłowy sposób przechwytywania wielu wyjątków za pomocą stwierdzenia except
polega na wskazaniu pierwszego parametru jako krotki zawierającej wszystkie wyjątki, które chcemy przechwycić. Dla lepszej czytelności możesz użyć słowa kluczowego as
, na przykład tak:
try:
l = ["a", "b"]
int(l[2])
except (ValueError, IndexError) as e:
pass
GO TO FULL VERSION