Sytuacja kobiet w IT w 2024 roku
10.04.20216 min
Eden Au

Eden AuAdvisory Board Memberprojectaccess.co

4 częste błędy początkujących pythonowców

Poznaj częste błędy popełniane przez tych, którzy dopiero uczą się Pythona i sprawdź, jak ich uniknąć.

4 częste błędy początkujących pythonowców

Spójrzmy prawdzie w oczy - trudno jest nauczyć się programowania. Wiele osób się ze mną zgodzi, ale inni powiedzą, że wcale tak nie jest. Ja sam w to nie wierzyłem. A było tak, ponieważ zawsze odkrywałem rozwiązania, które sprawiały, że robiłem, co chciałem w różnych językach programowania. Myślałem wtedy, że dobrze je opanowałem, ale byłem w błędzie. Na ogół, możesz z kodem zrobić wszystko, ale nie zawsze jest to wskazane. 

Wkrótce zdałem sobie sprawę, że niektóre metody, z których korzystałem, były złymi praktykami. Zastanawiać może jednak, jak działający fragment kodu może być zły? Przekonałem się na własnej skórze, że jak najbardziej może.

1. Nieużywanie iteratorów

Każdy początkujący Pythonowiec to robi, niezależnie od tego, czy zna inne języki, czy nie. Od tego nie da się uciec. 

Weźmy na przykład listę my_list. W jaki sposób uzyskasz dostęp do każdego elementu listy za pomocą pętli for? Wiemy, że listy w Pythonie są indeksowane i dlatego możemy dostać się do elementu o numerze i przez my_list[i]. Następnie tworzymy iterator dla liczb całkowitych od 0 do len(my_list) dla pętli for, jak widać poniżej:

for i in range(len(my_list)):
    foo(my_list[i])

To działa. Nie ma żadnych problemów z kodem. Jest to również standardowy sposób konstruowania pętli for w takich językach jak C, ale w Pythonie możemy to zrobić sprawniej.

Jak?

Czy wiesz, że listy w Pythonie są iterowalne? Możemy pisać znacznie bardziej czytelny kod, wykorzystując to, że po listach da się iterować:

for element in my_list:
    foo(element)

Równoległe przechodzenie wielu list pętlą for można uzyskać za pomocą funkcji zip, podczas gdy ennumerate może być pomocne, jeśli chcesz uzyskać numer indeksu (tj. licznika) podczas iteracji po danym obiekcie.

2. Używanie zmiennych globalnych

Zmienna globalna jest deklarowana w skrypcie głównym o zasięgu globalnym, natomiast zmienna lokalna to zmienna zadeklarowana w ramach funkcji o zasięgu lokalnym. Użycie słowa kluczowego global w Pythonie pozwala na dostęp i wprowadzanie zmian do zmiennych globalnych w danej funkcji na poziomie lokalnym. Oto przykład:

a = 1 # a variable    

def increment():
    a += 1
    return a

def increment2():
    global a # can make changes to global variable "a"
    a += 1 
    return a
  
increment()  # UnboundLocalError: local variable 'a' referenced before assignment
increment2() # returns 2

Wielu początkujących to uwielbia, ponieważ wydaje się to ratować od przekazywania wszystkich argumentów potrzebnych do działania danej funkcji. Nie jest to jednak prawda - to po prostu utrudnia zrozumienie kodu.

Używanie zmiennych globalnych jest również niekorzystne przy debugowaniu. Funkcje powinny być traktowane, jak bloki wielokrotnego użytku, a funkcje, które modyfikują zmienne globalne, mogą powodować skutki uboczne, które są bardzo trudne do wykrycia. Może to spowodować, że kod będzie skomplikowany i ciężki do debugowania.

Modyfikowanie zmiennych globalnych w funkcji lokalnej jest złą praktyką programowania. Zmienną powinno się przekazać jako argument, zmodyfikować ją i zwrócić na końcu funkcji.

* Nie należy mylić zmiennych globalnych ze stałymi globalnymi, ponieważ użycie tych drugich jest w większości scenariuszy OK.

3. Brak zrozumienia koncepcji obiektów mutowalnych

Jest to być może najczęstsza niespodzianka dla nowych osób uczących się Pythona, ponieważ jest to raczej coś charakterystycznego wyłącznie dla Pythona.

W Pythonie istnieją dwa rodzaje obiektów: obiekty mutowalne, które mogą modyfikować swój stan lub zawartość podczas działania oraz niemutowalne, które nie mogą tego robić. Wiele wbudowanych typów obiektów są niemutowalne, w tym na przykład, int, float, string, bool i tuple.

st = 'A string' 
st[0] = 'B' # You cannot do this in Python

Z drugiej strony takie typy danych, jak list, set i dict, są mutowalne. Możesz zatem zmienić zawartość elementów na liście, np. my_list[0]='new'.

Gdy domyślne argumenty w funkcjach są mutowalne, wydarzy się coś nieoczekiwanego. Weźmy na przykład następującą funkcję, w której mutowalna pusta lista jest domyślną wartością dla parametru my_list:

def foo(element, my_list=[]):
    my_list.append(element)
    return my_list

Wywołajmy tę funkcję dwa razy bez podawania argumentu dla my_list, tak aby przyjmowała ona wartość domyślną. W idealnym przypadku nowa pusta lista byłaby tworzona za każdym razem, gdy wywoływana jest funkcja, jeśli nie podano drugiego argumentu.

a = foo(1) # returns [1]
b = foo(2) # returns [1,2], not [2]! WHY?

Co?

Okazuje się, że domyślne argumenty w Pythonie są ewaluowane tylko raz w momencie zdefiniowania funkcji. Oznacza to, że wywołanie funkcji nie odświeża domyślnych argumentów.

Dlatego, jeśli domyślny argument jest mutowalny, to jest on mutowany przy każdym wywołaniu funkcji. Zmutowany domyślny argument zachowałby się dla wszystkich przyszłych wywołań funkcji. „Standardową” poprawką jest użycie (niemutowalnego) None jako wartości domyślnej, jak pokazano poniżej.

def foo(element, my_list=None):
    if my_list is None:
        my_list = []
    list_.append(element)
    return my_list

4. Unikanie kopiowania

Pojęcie kopiowania może być obce, a nawet sprzeczne z intuicją dla tych, którzy się Pythona dopiero uczą. Załóżmy, że masz listę a=[[0,1],[2,3]], a następnie deklarujesz nową listę przez b=a. Masz teraz dwie listy z tymi samymi elementami. Zmienianie niektórych elementów na liście b nie powinno powodować żadnych efektów ubocznych na liście a, tak?

Nie.

a = [[0,1],[2,3]]
b = a

b[1][1] = 100

print(a,b) 
# [[0, 1], [2, 100]] [[0, 1], [2, 100]]
print(id(a)==id(b))
# True

Kiedy „kopiujesz” listę przy pomocy przypisania b=a, każda modyfikacja elementów jednej listy jest widoczna w obu. Operator przypisania tworzy binding do obiektu. Obie listy (a i b) mają zatem w tym przykładzie tę samą referencję, czyli id() w Pythonie.


Jak zatem kopiować obiekty?

Jeśli chcesz to robić i modyfikować tylko wartości w nowym (lub starym) obiekcie bez bindowania, istnieją dwie opcje: kopiowanie płytkie i głębokie. Dwa obiekty będą miały wtedy różne referencje. Korzystając z naszego poprzedniego przykładu, możesz utworzyć płytką kopię a przez b=copy.copy(a). Kopia ta tworzy nowy obiekt, w którym przechowywane są referencje do oryginalnych elementów. To może wydawać się skomplikowane, ale spójrzmy na następujący przykład:

import copy

a = [[0,1],[2,3]]
b = copy.copy(a)

print(id(a)==id(b))
# False

b[1] = 100
print(a,b)
# [[0, 1], [2, 3]] [[0, 1], 100]

b[0][0] = -999
print(a,b)
# [[-999, 1], [2, 3]] [[-999, 1], 100]
print(id(a[0]) == id(b[0]))
# True

Zaraz po utworzeniu płytkiej kopii zagnieżdżonej listy a, którą nazywamy b, dwie listy mają różne odwołania, a więc id(a)!=Id(b). Ich elementy mają jednak te same odwołania, a zatem id(a[0])==id(b[0]). Oznacza to, że zmiana elementów wewnątrz b nie wpływa na listę a, ale zmiana elementów wewnątrz b[1] wpływa na a[1]. Kopia jest zatem płytka.

Krótko mówiąc, wszelkie zmiany dokonane w elementach wewnątrz zagnieżdżonych obiektów w b pojawią się w funkcji a, jeśli b jest jej płytką kopią.

Jeśli chcesz skopiować zagnieżdżony obiekt bez żadnych bindowań między jego elementami, potrzebujesz głębokiej kopii a przez b=copy.deepcopy(a). Głęboka kopia tworzy zarówno nowy obiekt, jak i kopie zagnieżdżonych obiektów w oryginalnych elementach.

Krótko mówiąc, głęboka kopia powiela wszystko bez bindowania starych obiektów.

Podsumowanie

Oto 4 częste błędy popełniane przez tych, którzy się Pythona dopiero uczą. Mam nadzieję, że ten artykuł Ci się przyda i nie będziecie musieli przez pewne rzeczy już przechodzić.

Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>