Co zostanie wyświetlone po wykonaniu poniższego kodu ?

slownik = {'jeden': 1, 'dwa':2, 'trzy': 3}
lista1 = [slownik, 4, 5, 6]

def zmien_liste_albo_nie(lista):
    lista[1] = 40
    lista[0]['jeden'] = 10

print(lista1)
zmien_liste_albo_nie(lista1[:])
print(lista1)

To jest klasyk

To pytanie to chyba numer jeden wszystkich pytań rekrutacyjnych. Na 100% na nie trafisz. Było na wszystkich rozmowach, w których brałem udział. Oczywiście nie chodzi to u to konkretne pytanie tylko raczej o pewien typ pytań.

Odpowiedz

[{'jeden': 1, 'dwa': 2, 'trzy': 3}, 4, 5, 6]
[{'jeden': 10, 'dwa': 2, 'trzy': 3}, 4, 5, 6]

Jeśli nie do końca wiesz co tu się wydarzyło nic nie szkodzi. Zaraz wszytko będzie jasne.

Przekazywanie listy do funkcji

Lista jak wiemy jest mutable. Oznacza to tyle, że możemy ją modyfikować. Jest to całkiem logiczne bo przecież nie raz dodawałeś czy usuwałeś elementy z listy. Jeśli naszą listę przekażemy do funkcji jako jej parametr dalej mamy możliwość jej modyfikowania. Najlepiej zobrazuje to przykład:

lista = [1,2,3] #stworzenie listy

def zmien_liste(lista): #przekazanie listy do funkcji
    lista.append(4) #dodanie do listy
    lista.append(5) #dodanie do listy
    lista.append(6) #dodanie do listy

print(lista)
zmien_liste(lista)
print(lista)

Output:

[1, 2, 3]
[1, 2, 3, 4, 5, 6]

Jak widać modyfikacje dokonane na liście wewnątrz funkcji są widoczne poza nią. To dlatego, że w Pythonie parametry do funkcji przekazywane są przez referencje. Czyli wewnątrz funkcji pracujemy na oryginalnej liście.

Jak przekazać kopię listy do funkcji

Co jeśli nie chcemy by modyfikacje dokonane na liście wewnątrz funkcji „psuły” naszą oryginalną listę. Żaden problem. Przekażmy do funkcji kopię naszej listy. By to zrobić można posłużyć się metodącopy” lub zastosować coś takiego jak ”lista[:]”:

lista = [1,2,3]

def zmien_liste(lista):
    lista.append(4)
    lista.append(5)
    lista.append(6)

print(lista)
zmien_liste(lista[:])
print(lista)

Output:

[1, 2, 3]
[1, 2, 3]

Jak widać tym razem nasze modyfikacje wewnątrz funkcji nie spowodowały zmiany oryginalnej listy. Czyli modyfikowaliśmy kopię naszej listy.

Dwa rodzaje kopiowania

W Pythonie istnieją dwa rodzaje kopiowania. Kopiowanie płytkie „Shallow copy” oraz kopiowanie głębokie „Deep Copy”. Nasz sposób czyli ”list[:]” jest przykładem kopiowania płytkiego. Kopiowanie płytkie świetnie sprawdza się dla płaskich list. Płaska lista to taka złożona na przykład z samych liczb. Dla takich danych płytkie kopiowanie będzie działać jak należy. Jednak w naszym przykładzie jednym z elementów listy jest słownik. To właśnie on sprawia problem. Na nim nie działa już płaskie kopiowanie. Zatem w funkcji operujemy na liście, która w części jest kopią oryginału, a w części nie. Dlatego część listy możemy zmieniać z wnętrza funkcji, a część nie.

Nasza lista wyglądała tak:

slownik = {'jeden': 1, 'dwa':2, 'trzy': 3}
lista1 = [slownik, 4, 5, 6]

Jeśli taką listę przekażemy do funkcji używając płytkiego kopiowania efekt może być dla nas zaskakujący. Bowiem elementy listy typu int (liczby), rzeczywiście będą kopią. Natomiast pierwszy element listy czyli słownik dalej będzie oryginałem.

Jak to naprawić

Zakładając, że zależy nam by nasza funkcja nie zmieniała prawdziwej listy należałoby użyć kopiowania głębokiego:

from copy import deepcopy

slownik = {'jeden': 1, 'dwa':2, 'trzy': 3}
lista1 = [slownik, 4, 5, 6]

def zmien_liste_albo_nie(lista):
    lista[1] = 40
    lista[0]['jeden'] = 10

print(lista1)
zmien_liste_albo_nie(deepcopy(lista1))
print(lista1)

Output:

[{'jeden': 1, 'dwa': 2, 'trzy': 3}, 4, 5, 6]
[{'jeden': 1, 'dwa': 2, 'trzy': 3}, 4, 5, 6]

Teraz już mamy pewność, że pracujemy na kopii naszej listy. „deepcopy” zadba o dokładne skopiowanie wszystkich jej elementów.