Zadanie
Tym razem nasze zadanie jest proste :). Trzeba naprawić kawałek kodu bo jak to bywa z kawałkami kodu nie działa jak trzeba. Dodam, że dzisiejszy przykład pytania rekrutacyjnego jest najbardziej autentyczny i błąd znajdujący się w tym kodzie jest często popełniany przez początkujących programistów.
Co robi nasza felerna funkcja ?
Cały problem dotyczy funkcji o nazwie def dodaj_10_do_listy(moja_lista)
. Funkcja ta robi dwie rzeczy. Po pierwsze do każdego elementu przekazanej listy dodaje wartość 10, po drugie sprawdza czy w przekazanej liście znajduje się wartość 2 jak tak zwraca True
w przeciwnym wypadku zwraca False
.
Oto kod naszej funkcji:
def dodaj_10_do_listy(moja_lista): znalazlem_2 = False # jeśli nie znajdziemy liczby 2 ta zmienna pozostanie False def zmien_liste():# funkcja wewnętrzna for i, x in enumerate(moja_lista):#enumeracja po liście przekazanej jako parametr if x == 2:# sprawdzamy element po elemencie czy nie jest równy 2 znalazlem_2 = True# jeśli znajdźmy dwa ustawiamy zmienna na True moja_lista[i] += 10# do każdego elementu listy dodajemy 10 zmien_liste()# wywołanie funkcji wewnętrznej return znalazlem_2# zwracamy wartość znalazłem_2 moja_lista = [10,10,10] znalazlem = dodaj_10_do_listy(moja_lista)#wywołanie naszej funkcji #sprawdzenie wyników print(moja_lista) print(znalazlem)
Zobaczmy jak będzie wynik wywołania naszej funkcji dla moja_lista = [10,10,10]
Output:
[20, 20, 20] False
Czyli udało się. Nasza funkcja dodała 10 do każdego elementu listy i z uwagi, że w podanej liście nie było wartości 2 zwróciła False. Wygląda, że wszytko działa jak należy. Spróbujmy wywołać funkcje dla innych danych wejściowych. Na przykład dla moja_lista = [1,2,3,4]
def dodaj_10_do_listy(moja_lista): znalazlem_2 = False # jeśli nie znajdziemy liczby 2 ta zmienna pozostanie False def zmien_liste():# funkcja wewnętrzna for i, x in enumerate(moja_lista):#enumeracja po liście przekazanej jako parametr if x == 2:# sprawdzamy element po elemencie czy nie jest równy 2 znalazlem_2 = True# jeśli znajdźmy dwa ustawiamy zmienna na True moja_lista[i] += 10# do każdego elementu listy dodajemy 10 zmien_liste()# wywołanie funkcji wewnętrznej return znalazlem_2# zwracamy wartość znalazłem_2 moja_lista = [1, 2, 3, 4] znalazlem = dodaj_10_do_listy(moja_lista)#wywołanie naszej funkcji #sprawdzenie wyników print(moja_lista) print(znalazlem)
Zobaczmy jest output z wykonania programu:
[11, 12, 13, 14] False
Tak jak się spodziewaliśmy do każdego elementu listy dodana została wartość 10. Jednak skąd ten False ? Przecież w liście podanej jako parametr do funkcji znajdowała się wartość 2. Zatem spodziewaliśmy się, że nasza funkcja zwróci True a nie False. No i mamy problem. Funkcja nie działa tak jak zamierzaliśmy. Dodam, że w tej postaci nasza funkcja zawsze będzie zwracała False. Niezależnie czy w podanej liście będzie wartość 2 czy nie.
Dlaczego to nie działa?
Problemem jest zmienna znalazlem_2
, a dokładniej z dwie zmienne znalazlem_2
. Tak zgadza się, w naszej funkcji mamy dwie zmienne o takiej samej nazwie. Jedna zmienna dla funkcji dodaj_10_do_listy
, którą ustawiamy na False oraz drugą o tej nazwie dla funkcji wewnętrznej zmien_liste
. Zapewne domyślasz się już, że zawsze zwracamy tą pierwszą. Spójrzmy jeszcze raz na kod:
def dodaj_10_do_listy(moja_lista): znalazlem_2 = False # tu deklarujemy zmienną i ustawiamy jej wartość na False def zmien_liste(): for i, x in enumerate(moja_lista): if x == 2: znalazlem_2 = True # to nie jest przypisanie to deklaracja nowej zmiennej dostępnej tylko z funkcji wewnętrznej moja_lista[i] += 10
Jak to naprawić ?
By naprawić naszą funkcję należałoby z funkcji wewnętrznej jakoś modyfikować zmienną z funkcji zewnętrznej, czyli nadrzędnej. Wtedy byłaby już tylko jedna zmienna o nazwie znalazlem_2
i to właśnie ona zostałaby zwrócona przez funkcję nadrzędną.
Nonlocal
Dodanie tego słowa kluczowego przed użyciem w funkcji podrzędnej zmusza interpreter do szukania jej w funkcji nadrzędnej. Czyli jest to dokładnie to czego nam potrzeba. I tak jedna drobna zmiana naprawi naszą funkcję:
def dodaj_do_listy(moja_lista): znalazlem_2 = False def zmien_liste(): nonlocal znalazlem_2 #to naprawia for i, x in enumerate(moja_lista): if x == 2: znalazlem_2 = True moja_lista[i] += 10 zmien_liste() return znalazlem_2 moja_lista = [1,2,3,4,5,6,7,8] znalazlem = dodaj_do_listy(moja_lista) print(moja_lista) print(znalazlem)
Output:
[11, 12, 13, 14, 15, 16, 17, 18] True
I wszytko działa 🙂 Ja jednak nie jestem fanem tego rozwiązania. Z uwagi, że nie widać jednoznacznie do czego odnosi się dana zmienna może to wprowadzić dużo bałaganu. Jeśli naprawdę nie musisz, nie używaj nolocal.
Zwracanie return
Zamiast walczyć z zakresem widoczności zmiennych warto przemyśleć zastosowanie najprostszego rozwiązania. Niech nasza funkcja zewnętrzna zwraca zmienną lokalną i w ten sposób przekazuje informacje o znalezieniu wartości 2 do funkcji nadrzędnej:
def dodaj_do_listy(moja_lista): def zmien_liste(): znalazlem_2 = False for i, x in enumerate(moja_lista): if x == 2: znalazlem_2 = True moja_lista[i] += 10 return znalazlem_2# teraz nasz funkcja wewnętrzna zwraca lokalną zmienną return zmien_liste()# wywołanie wewnętrznej funkcji i zwrócenie jej wartości na zewnątrz moja_lista = [1,2,3,4,5,6,7,8] znalazlem = dodaj_do_listy(moja_lista) print(moja_lista) print(znalazlem)
Output:
[11, 12, 13, 14, 15, 16, 17, 18] True
To rozwiązanie jest o wiele bardziej czytelne niż poprzednie. Czytelność to co powinno być dla nas najważniejsze.
Użycie klasy
Jeżeli zamiast funkcji użyjemy klasy dostaniemy w gratisie możliwość dostępu do jej składowych. Daje to nam możliwość trzymania stanu. Dodatkowo aby nasza klasa bardziej przypominała funkcję możemy dodać do niej metodę def __call__(self):
będzie można wywoływać ją jak funkcję:
class DodajDoListy: def __init__(self): self.znalazlem_2 = None# teraz nasza mienna jest składową klasy def __call__(self, moja_lista):# dzięki tej metodzie można użwyać naszego obiektu jak funkcji self.znalazlem_2 = False def zmien_liste(): for i, x in enumerate(moja_lista): if x == 2: self.znalazlem_2 = True# do zmiennej składowej mżna odwołć się za pomoca self moja_lista[i] += 10 zmien_liste() return self.znalazlem_2 dodaj_do_listy = DodajDoListy() moja_lista = [1,2,3,4,5,6,7,8] znalazlem = dodaj_do_listy(moja_lista) print(moja_lista) print(znalazlem)
Output:
[11, 12, 13, 14, 15, 16, 17, 18] True
Podsumowanie
Przeszliśmy wspólnie przez różne propozycje rozwiązania problemu. Pamiętaj, że to na czym powinno Ci zależeć najbardziej to czytelność twojego kodu. Problem zaprezentowany dzisiaj jest bardzo popularny wśród początkujących programistów. Teraz dzięki nam nie wpadniesz w tą pułapkę.
Dodatkowe info
Hej jeden z czytelników, a mianowicie Karol Staniek zwrócił uwagę, że kod użyty w tym przykładzie mógłby być odrobinę bardziej Pythonic way. Karol zasugerował użycie funkcji any.
Zatem nowy kod prezentuje się następująco:
def dodaj_do_listy(moja_lista): def zmien_liste(): for i in range(len(moja_lista)): moja_lista[i] += 10 return any(x==2 for x in moja_lista) return zmien_liste()# wywołanie wewnętrznej funkcji i zwrócenie jej wartości na zewnątrz moja_lista = [1,2,3,4,5,6,7,8] znalazlem = dodaj_do_listy(moja_lista) print(moja_lista) print(znalazlem)
Postanowiłem przy okazji zmierzyć czas wykonania obydwu rozwiązań. Ku mojemu zdziwieniu okazało się, że rozwiązsnie z użyciem funkcji any
jest szybsze. Nasz algorytm przyśpieszył o 0.02 ms.
Dzięki Karol dobra robota.