Streszczenie
Listy w Pythonie — dynamiczne kolekcje danych

Moduł w całości poświęcony jest jednej z najważniejszych i najczęściej używanych struktur danych w języku Python – dynamicznym listom (typ list). Omówiono w nim mechanizmy tworzenia list, indeksowania dodatniego i ujemnego oraz wycinków (slicing) z krokiem, które pozwalają na elastyczne wyodrębnianie fragmentów kolekcji. Przedstawiono bogaty zestaw wbudowanych metod modyfikujących listy w miejscu, takich jak append(), insert(), extend(), remove(), pop() oraz sort(), a także funkcje wbudowane len() i sorted(). Szczegółowo przeanalizowano mechanizmy kopiowania list, w tym kopiowanie powierzchowne (shallow copy) oraz pełne (deep copy) z modułu copy, wraz z pułapką aliasingu referencji. Materiał zawiera również zaawansowane techniki, takie jak wyrażenia listowe (list comprehension), listy zagnieżdżone oraz praktyczne programy i ćwiczenia utrwalające wiedzę.

Kluczowe zagadnienia modułu:

  • Tworzenie, indeksowanie i wycinki — definiowanie list za pomocą [], dostęp przez indeksy dodatnie i ujemne oraz wyodrębnianie fragmentów [start:stop:krok]
  • Modyfikacja w miejscu — dodawanie (append, insert, extend), usuwanie (remove, pop, del, clear) oraz sortowanie i odwracanie (sort, sorted, reverse)
  • Kopiowanie list i aliasing — kopiowanie powierzchowne (copy(), [:]) vs pełne (deepcopy()) oraz problem współdzielenia referencji w pamięci
  • Wyrażenia listowe (list comprehension) — składnia [wyrażenie for element in kolekcja if warunek] do szybkiej i czytelnej transformacji danych
  • Listy zagnieżdżone i macierze — tworzenie struktur wielowymiarowych, indeksowanie dwuwymiarowe oraz transpozycja macierzy
Streszczenie - Listy w Pythonie

Moduł poświęcony listom w języku Python stanowi kluczowy etap w kształceniu każdego programisty, ponieważ listy są jedną z najczęściej używanych struktur danych w tym języku. Dynamiczny charakter list, przejawiający się możliwością dodawania i usuwania elementów w trakcie działania programu, odróżnia je od statycznych tablic znanych z innych języków programowania. Zrozumienie mechanizmów indeksowania, wycinków oraz metod wbudowanych jest niezbędne do efektywnego przetwarzania danych w Pythonie. Szczególną uwagę poświęcono w tym module zagadnieniom kopiowania list, ponieważ błędy związane z aliasingiem są częstym źródłem problemów u początkujących programistów. Wyrażenia listowe, stanowiące zaawansowaną technikę transformacji danych, zostały przedstawione jako narzędzie zwiększające czytelność i wydajność kodu. Praktyczne programy i ćwiczenia zamieszczone na końcu modułu pozwalają zweryfikować zdobytą wiedzę w realnych scenariuszach programistycznych.

Opanowanie list otwiera drogę do zrozumienia bardziej zaawansowanych struktur danych, takich jak stosy, kolejki czy macierze, które są implementowane właśnie za pomocą list. Regularne ćwiczenie operacji na listach buduje solidne podstawy do nauki algorytmów i struktur danych na wyższym poziomie zaawansowania.

1/50
Czym jest lista
  • Lista w języku Python to uporządkowana i zmienna kolekcja, która pozwala na przechowywanie wielu elementów w jednej zmiennej.
  • W przeciwieństwie do prostych typów danych, takich jak liczby czy napisy, lista stanowi dynamiczny kontener, którego rozmiar może się swobodnie zmieniać w trakcie działania programu.
  • Kolejność elementów w liście jest ściśle zdefiniowana i gwarantowana przez interpreter, co oznacza, że elementy zawsze zachowują swoje pozycje.
  • Zmienność (ang. mutability) to kluczowa cecha listy, oznaczająca możliwość dodawania, usuwania i modyfikowania elementów bezpośrednio w pamięci bez tworzenia nowego obiektu.
  • Pod maską listy są zaimplementowane jako dynamiczne tablice wskaźników na obiekty, co zapewnia niezwykle szybki dostęp do dowolnego elementu.
Zapamiętaj: Lista to najbardziej podstawowa i uniwersalna dynamiczna struktura danych w Pythonie. Używaj jej zawsze, gdy potrzebujesz przechowywać uporządkowaną serię elementów.
owoce = ["jabłko", "banan", "wiśnia"]
print(owoce)  # Wyświetli całą listę
            
Schemat listy jako ciągu pudełek

Listy w Pythonie należą do kategorii typów mutowalnych, co oznacza, że po utworzeniu można modyfikować ich zawartość bez konieczności tworzenia nowego obiektu w pamięci. Wewnętrzna implementacja list opiera się na dynamicznych tablicach wskaźników, które przechowują referencje do obiektów, a nie same obiekty. Taka konstrukcja zapewnia szybki dostęp do dowolnego elementu w czasie stałym, ale jednocześnie powoduje, że wstawianie elementów w środek listy jest kosztowniejsze. Listy w Pythonie potrafią automatycznie zarządzać swoją pojemnością, alokując dodatkową pamięć z zapasem, co minimalizuje liczbę kosztownych operacji realokacji. Dzięki temu większość operacji append wykonuje się błyskawicznie, a nie co jakiś czas następuje dłuższe przetwarzanie związane ze zwiększeniem rozmiaru tablicy.

Warto zapamiętać, że zmienność list odróżnia je od krotek i napisów, co ma istotne znaczenie przy projektowaniu struktury danych w programie.

2/50
Tworzenie listy
  • Tworzenie nowej listy w Pythonie jest niezwykle proste i intuicyjne dla każdego programisty.
  • Najbardziej powszechną i zalecaną metodą jest użycie nawiasów kwadratowych [], w których umieszczamy poszczególne elementy oddzielone przecinkami.
  • Możemy również utworzyć całkowicie pustą listę za pomocą samych nawiasów kwadratowych lub za pomocą wbudowanego konstruktora list().
  • Konstruktor list() jest niezwykle użyteczny, ponieważ pozwala na konwersję innych typów iterowalnych (takich jak napisy czy krotki) na listę.
  • Użycie nawiasów kwadratowych jest preferowane ze względu na czytelność oraz wyższą wydajność, ponieważ interpreter optymalizuje ten zapis bezpośrednio w kodzie bajtowym.
Zapamiętaj: Zapis [] jest szybszy i bardziej pythoniczny niż użycie konstruktora list() do tworzenia pustej listy.
pusta_1 = []
pusta_2 = list()
liczby = [1, 2, 3, 4]
litery = list("abc")  # Tworzy ['a', 'b', 'c']
            
Przykłady tworzenia list

Tworzenie list za pomocą nawiasów kwadratowych jest nie tylko czytelniejsze, ale również szybsze od użycia konstruktora list(), ponieważ interpreter optymalizuje ten zapis bezpośrednio w kodzie bajtowym. Konstruktor list() jest jednak nieoceniony, gdy potrzebujemy przekonwertować inny typ iterowalny na listę, na przykład po przetworzeniu danych z pliku lub baz danych. W Pythonie listy mogą być tworzone również za pomocą wyrażeń generatorowych, choć wtedy trzeba jawnie przekazać generator do konstruktora list(). W praktyce programistycznej najczęściej spotyka się inicjalizację listy z konkretnymi wartościami podanymi wprost w kodzie, ewentualnie z użyciem mnożenia do szybkiego tworzenia list o jednorodnych elementach. Pusta lista, utworzona jako [] lub list(), jest często używana jako akumulator w pętlach, do którego stopniowo dodaje się elementy.

Należy pamiętać, że konstruktor list() akceptuje dowolny obiekt iterowalny, co czyni go uniwersalnym narzędziem konwersji między różnymi typami kolekcji w Pythonie.

3/50
Elementy różnych typów
  • Jedną z największych zalet list w Pythonie jest ich całkowita elastyczność w zakresie przechowywanych typów danych.
  • W przeciwieństwie do tradycyjnych tablic w językach C++ czy Java, lista w Pythonie jest strukturą heterogeniczną.
  • Oznacza to, że pojedyncza lista może jednocześnie przechowywać elementy o zupełnie różnych typach, takich jak liczby całkowite, napisy, wartości logiczne, a nawet inne listy.
  • Ta elastyczność wynika z faktu, że lista przechowuje referencje (wskaźniki) do obiektów, a nie same fizyczne wartości bezpośrednio w strukturze.
  • W codziennej praktyce zaleca się jednak tworzenie list homogenicznych (zawierających elementy jednego typu) w celu uniknięcia błędów podczas przetwarzania danych w pętlach.
Zapamiętaj: Chociaż Python pozwala na mieszanie typów w liście, staraj się grupować w nich obiekty tego samego typu. Ułatwi to pisanie bezpiecznych algorytmów.
mieszana = [1, "Python", 3.14, True, [5, 6]]
print(mieszana[1])  # Wyświetli 'Python'
            
Schemat listy z różnymi typami

Heterogeniczność list w Pythonie, czyli możliwość przechowywania elementów różnych typów, wynika bezpośrednio z referencyjnego modelu pamięci tego języka. Każdy element listy to tak naprawdę wskaźnik do obiektu znajdującego się gdzieś w pamięci, więc fizycznie lista nie przechowuje samych wartości, a jedynie referencje do nich. Dzięki temu w jednej liście możemy umieścić liczbę, napis, wartość logiczną, a nawet inną listę, nie martwiąc się o zgodność typów. Ta elastyczność jest ogromną zaletą w szybkim prototypowaniu i pisaniu kodu o ogólnym przeznaczeniu. W praktyce jednak, w większych projektach warto stosować listy homogeniczne, ponieważ ułatwiają one wnioskowanie o typach i zmniejszają ryzyko błędów w czasie wykonania. Adnotacje typów (type hints) w nowoczesnym Pythonie pozwalają na określenie oczekiwanego typu elementów listy, co poprawia czytelność kodu i umożliwia statyczną analizę typów.

Zaleca się, aby w kodzie produkcyjnym unikać mieszania typów w listach, chyba że jest to celowe i dobrze udokumentowane.

4/50
Indeksowanie listy
  • Dostęp do poszczególnych elementów listy odbywa się za pomocą mechanizmu indeksowania, który jest tożsamy z indeksowaniem łańcuchów znaków.
  • Każdy element w liście ma przypisany unikalny numer indeksu, zaczynając od 0 dla pierwszego elementu.
  • Python wspiera również ujemne indeksowanie, gdzie indeks -1 oznacza ostatni element, -2 przedostatni i tak dalej, co eliminuje potrzebę liczenia długości listy.
  • Jeśli spróbujemy odwołać się do indeksu, który nie istnieje w liście (np. s[10] w liście 3-elementowej), Python zgłosi błąd IndexError.
  • Indeksowanie odbywa się w czasie stałym O(1), co gwarantuje błyskawiczne pobieranie danych niezależnie od rozmiaru listy.
Zapamiętaj: Pamiętaj, że pierwszy element listy ma zawsze indeks 0, a ostatni ma indeks -1 lub len(lista) - 1.
auta = ["Audi", "BMW", "Ford"]
pierwszy = auta[0]    # 'Audi'
ostatni = auta[-1]    # 'Ford'
# auta[5]  # Wywoła IndexError!
            
Schemat indeksowania listy

Indeksowanie w Pythonie zostało zaprojektowane z myślą o maksymalnej wygodzie programisty, stąd obecność indeksów ujemnych, które pozwalają na dostęp do elementów od końca listy bez konieczności obliczania ich pozycji. Indeks -1 zawsze oznacza ostatni element, niezależnie od długości listy, co eliminuje potrzebę używania wyrażenia len(lista) - 1. Zrozumienie różnicy między indeksowaniem dodatnim a ujemnym jest kluczowe przy implementacji algorytmów przetwarzających listy od końca. Błąd IndexError występuje, gdy próbujemy odwołać się do indeksu spoza zakresu listy, co jest częstym problemem w pętlach używających ręcznego indeksowania. Warto wiedzieć, że dostęp do elementu przez indeks ma złożoność obliczeniową O(1), czyli czas dostępu nie zależy od rozmiaru listy. Dzięki temu listy doskonale nadają się do przechowywania danych, do których potrzebujemy częstego i szybkiego dostępu przez pozycję.

W przeciwieństwie do niektórych języków, Python nie wspiera indeksowania z krokami innymi niż 1 na poziomie pojedynczego elementu – do tego służą wycinki.

5/50
Wycinki list
  • Wycinki (ang. slicing) pozwalają na wyodrębnienie fragmentu listy i stworzenie na jego podstawie zupełnie nowej listy.
  • Składnia wycinków ma postać l[start:stop:step], gdzie start to indeks początkowy (włącznie), stop to indeks końcowy (wyłącznie), a step to krok.
  • Jeśli pominiemy start, Python domyślnie zacznie od początku listy, a jeśli pominiemy stop -- wytnie elementy do samego końca.
  • Podobnie jak przy napisach, wycinki są niezwykle bezpieczne, ponieważ przekroczenie zakresu indeksów nie generuje błędu IndexError.
  • Wycięty fragment jest płytką kopią oryginalnych danych, co oznacza, że modyfikacja wycinka nie wpływa na oryginalną listę.
Zapamiętaj: Używaj zapisu lista[:], aby w prosty i czytelny sposób utworzyć szybką kopię całej listy.
liczby = [10, 20, 30, 40, 50]
fragment = liczby[1:4]  # [20, 30, 40]
co_drugi = liczby[::2]   # [10, 30, 50]
            
Schemat wycinków listy

Wycinki są jednym z najpotężniejszych mechanizmów Pythona do pracy z sekwencjami, pozwalającym na zwięzłe i czytelne wyodrębnianie fragmentów kolekcji. Składnia wycinków, choć na pierwszy rzut oka może wydawać się skomplikowana, podlega prostym regułom: wartość start jest włączona, stop jest wyłączony, a krok określa kierunek i interwał poruszania się po sekwencji. Pominięcie któregokolwiek z parametrów powoduje użycie wartości domyślnych: start = 0, stop = długość listy, step = 1. Szczególnie przydatny jest zapis lista[::-1], który odwraca kolejność elementów w liście, tworząc jej pełną kopię w odwrotnej kolejności. Należy pamiętać, że wycinki zawsze tworzą nową listę, nawet jeśli wycinamy całą kolekcję za pomocą [:]. Wyjątkiem jest przypisanie do wycinka, które modyfikuje oryginalną listę w miejscu. Wycinki są bezpieczne w użyciu, ponieważ przekroczenie zakresu nie generuje błędu, a jedynie zwraca pustą listę lub fragment zawierający elementy do końca sekwencji.

Warto wykorzystywać wycinki do szybkiego tworzenia kopii list za pomocą [:] oraz do sprawnego wyodrębniania fragmentów danych bez pisania skomplikowanych pętli.

6/50
Modyfikacja elementów
  • Ponieważ listy są zmienne, możemy swobodnie modyfikować ich elementy w miejscu bez potrzeby tworzenia nowego obiektu.
  • Modyfikację realizujemy poprzez wskazanie konkretnego elementu za pomocą jego indeksu w nawiasach kwadratowych i przypisanie mu nowej wartości przy użyciu operatora =.
  • Taka operacja natychmiastowo zastępuje dotychczasową referencję nowym obiektem w wybranej komórce dynamicznej tablicy.
  • Wszelkie inne zmienne, które wskazują na tę samą listę w pamięci, natychmiast zobaczą tę modyfikację.
  • Jest to diametralna różnica w porównaniu do łańcuchów znaków, gdzie zmiana znaku na danej pozycji była bezwzględnie zabroniona.
  • Modyfikacja w miejscu jest niezwykle szybka i wydajna pod kątem zużycia pamięci operacyjnej.
Zapamiętaj: Modyfikacja elementu listy pod konkretnym indeksem zmienia oryginalny obiekt listy bezpośrednio w pamięci RAM.
zwierzeta = ["pies", "kot", "ryba"]
zwierzeta[2] = "ptak"  # Zamiana 'ryba' na 'ptak'
print(zwierzeta)  # ['pies', 'kot', 'ptak']
            
Schemat modyfikacji elementu

Możliwość modyfikowania elementów listy w miejscu jest bezpośrednim następstwem mutowalności tego typu danych. W przeciwieństwie do napisów, które po utworzeniu są niezmienne, listy pozwalają na swobodne zastępowanie wartości pod określonym indeksem. Ta właściwość jest niezwykle przydatna przy implementacji algorytmów sortujących, gdzie elementy muszą być przestawiane w miejscu. Modyfikacja w miejscu ma istotny wpływ na wydajność, ponieważ nie wymaga alokacji nowej pamięci i kopiowania całej struktury. Należy jednak zachować ostrożność, ponieważ modyfikacja listy wpływa na wszystkie zmienne, które wskazują na ten sam obiekt w pamięci. To zjawisko, znane jako aliasing, jest częstym źródłem trudnych do wykrycia błędów, szczególnie gdy lista jest współdzielona między różnymi częściami programu. Dlatego przed modyfikacją listy, która może być współdzielona, warto rozważyć utworzenie jej kopii.

Operacja przypisania do indeksu ma złożoność obliczeniową O(1), ponieważ Python bezpośrednio aktualizuje wskaźnik w dynamicznej tablicy.

7/50
Modyfikacja wycinków
  • Python pozwala na modyfikację całych fragmentów listy za jednym razem przy użyciu niezwykle potężnej składni wycinkowej.
  • Przypisując nową kolekcję iterowalną do wycinka listy, możemy zastąpić wycięte elementy nowymi wartościami bezpośrednio w oryginalnej liście.
  • Co ciekawe, nowa wstawiana kolekcja wcale nie musi mieć tej samej długości co wycinany fragment.
  • Python automatycznie rozsunie lub skurczy listę, dopasowując jej rozmiar w pamięci do wstawianej liczby elementów.
  • Ta technika pozwala na masowe operacje modyfikacyjne, wstawianie elementów w środek listy lub jednoczesne usuwanie wielu elementów.
  • Jest to zaawansowana i bardzo wydajna metoda manipulacji strukturą danych.
Zapamiętaj: Do modyfikacji wycinka musisz zawsze przypisać obiekt iterowalny (np. inną listę), nawet jeśli wstawiasz tylko jeden element.
liczby = [1, 2, 3, 4]
liczby[1:3] = [99, 100, 101]  # Zastępuje [2, 3] trzema nowymi liczbami
print(liczby)  # [1, 99, 100, 101, 4]
            
Schemat modyfikacji wycinka

Modyfikacja wycinków w liście to zaawansowana technika, która łączy w sobie czytanie fragmentu z zastąpieniem go nową zawartością. W przeciwieństwie do zwykłego wycinku, który tworzy nową listę, przypisanie do wycinka modyfikuje oryginalną strukturę w miejscu. Elastyczność tej operacji polega na tym, że nowa kolekcja może mieć zupełnie inną długość niż zastępowany fragment, co powoduje automatyczne rozszerzenie lub skurczenie się listy. Python radzi sobie z tym przez odpowiednie przesunięcie elementów w dynamicznej tablicy. Można również użyć pustego wycinka do wstawienia elementów w dowolne miejsce bez usuwania istniejących, na przykład lista[2:2] = [x] wstawi x na pozycję 2 bez usuwania żadnego elementu. Ta technika jest wydajniejsza niż łączne użycie insert w pętli, ponieważ operacja przesunięcia wykonywana jest na poziomie natywnego kodu C. W praktyce modyfikacja wycinków jest używana przy implementacji złożonych transformacji danych oraz w algorytmach edycyjnych.

Należy pamiętać, że do przypisania wycinka wymagany jest obiekt iterowalny, więc pojedynczy element trzeba umieścić w liście lub krotce.

8/50
Długość listy -- len()
  • Wbudowana funkcja len() służy do sprawdzania, jak wiele elementów znajduje się aktualnie w danej liście.
  • Podobnie jak w przypadku typów tekstowych, funkcja ta działa niezwykle szybko i efektywnie, wykonując się w czasie stałym O(1).
  • Python nie przelicza fizycznie elementów listy za każdym razem, lecz odczytuje zapisaną wartość bezpośrednio z wewnętrznego nagłówka obiektu w pamięci.
  • Jest to szczególnie przydatne przy kontrolowaniu zakresów pętli oraz sprawdzaniu, czy lista nie jest całkowicie pusta przed rozpoczęciem przetwarzania.
  • Pusta lista zawsze zwraca długość 0, co można wygodnie wykorzystać w warunkach logicznych instrukcji if.
Zapamiętaj: Użycie logicznego sprawdzania listy w postaci 'if not lista:' jest bardziej pythoniczne niż pisanie 'if len(lista) == 0:'.
kolory = ["czerwony", "zielony", "niebieski"]
ilosc = len(kolory)  # Zwróci 3
print(f"Mamy {ilosc} kolorów.")
            
Przykład len() dla list

Funkcja len() jest jedną z najczęściej używanych funkcji wbudowanych w Pythonie, a jej działanie opiera się na odczycie wartości przechowywanej w nagłówku obiektu. Każdy obiekt sekwencyjny w Pythonie przechowuje informację o swojej długości jako atrybut, co sprawia, że len() działa w czasie stałym O(1). Dzięki temu można bezpiecznie wywoływać len() na listach dowolnej wielkości bez obaw o wydajność. W Pythonie len() jest często używane w pętlach do kontrolowania zakresu iteracji, choć bardziej pythoniczne jest bezpośrednie iterowanie po elementach. Warto również wiedzieć, że w Pythonie wartość logiczna listy zależy od jej długości – pusta lista jest fałszywa (False), a lista zawierająca co najmniej jeden element jest prawdziwa (True). Ta właściwość jest często wykorzystywana w instrukcjach warunkowych do sprawdzania, czy lista zawiera jakiekolwiek dane. W połączeniu z operatorami porównania, len() umożliwia szybkie porównywanie wielkości różnych kolekcji.

Wbudowana funkcja len() działa nie tylko na listach, ale na wszystkich typach sekwencyjnych i kolekcjach w Pythonie.

9/50
Dodawanie elementów -- append()
  • Metoda .append() to najbardziej podstawowy i najczęściej używany sposób na dynamiczne powiększanie listy.
  • Służy ona do dodawania dokładnie jednego elementu (dowolnego typu) na sam koniec istniejącej listy.
  • Element wstawiany staje się ostatnim elementem o nowym, najwyższym indeksie, a długość listy rośnie o 1.
  • Metoda .append() modyfikuje listę w miejscu i nie zwraca żadnej nowej wartości (zwraca None), więc przypisywanie jej wyniku do zmiennej jest poważnym błędem.
  • Pod maską Python alokuje pamięć z zapasem, dzięki czemu większość operacji .append() wykonuje się natychmiastowo w czasie stałym O(1).
Zapamiętaj: Nigdy nie pisz 'lista = lista.append(x)'. Metoda ta modyfikuje listę w miejscu i zwraca None, co zniszczy Twoją zmienną.
koszyk = ["chleb"]
koszyk.append("masło")  # Dodanie na koniec
print(koszyk)  # ['chleb', 'masło']
            
Schemat append()

Metoda append() jest najczęściej używaną metodą dodawania elementów do listy, głównie ze względu na jej wydajność i prostotę. Wewnętrznie Python alokuje pamięć z pewnym zapasem, więc większość wywołań append() wykonuje się w czasie stałym. Dopiero gdy lista przekroczy swoją zarezerwowaną pojemność, następuje realokacja, która jest operacją kosztowniejszą, ale dzięki odpowiedniej strategii wzrostu pojemności zdarza się rzadko. Częstym błędem początkujących jest przypisywanie wyniku działania append() do zmiennej, ponieważ metoda ta zwraca None, a nie zmodyfikowaną listę. Append() modyfikuje listę w miejscu, co oznacza, że wszystkie referencje wskazujące na tę listę zobaczą nowy element. W praktyce append() jest niezastąpione przy budowaniu list w pętlach, szczególnie gdy nie znamy z góry liczby elementów, które będą dodane. Alternatywą dla append() w przypadku dodawania wielu elementów jednocześnie jest metoda extend().

Warto pamiętać, że append() dodaje tylko jeden element – jeśli przekazano listę, zostanie ona dodana jako pojedynczy, zagnieżdżony element.

10/50
Dodawanie elementów -- insert()
  • Kiedy zachodzi potrzeba dodania nowego elementu na ściśle określoną pozycję w liście, a nie na jej koniec, używamy metody .insert().
  • Metoda ta przyjmuje dwa argumenty: .insert(indeks, element), gdzie pierwszy argument wskazuje docelową pozycję.
  • Wszystkie elementy znajdujące się od wskazanego indeksu w prawo zostaną automatycznie przesunięte o jedną pozycję w celu zrobienia miejsca.
  • Jeśli przekażemy indeks większy niż aktualna długość listy, element zostanie wstawiony bez błędów na sam koniec.
  • Ponieważ operacja ta wymaga przesunięcia wielu elementów w pamięci RAM, jej złożoność czasowa wynosi O(N), co czyni ją wolniejszą niż .append().
Zapamiętaj: Używanie insert(0, element) wstawia element na sam początek listy, co przy dużych listach może negatywnie wpłynąć na wydajność programu.
liczby = [1, 2, 4]
liczby.insert(2, 3)  # Wstaw liczbę 3 pod indeks 2
print(liczby)  # [1, 2, 3, 4]
            
Schemat insert()

Metoda insert() różni się od append() przede wszystkim tym, że pozwala na precyzyjne określenie pozycji, na której ma zostać wstawiony nowy element. Wstawienie na początek listy za pomocą insert(0, element) jest kosztowniejsze niż append(), ponieważ wymaga przesunięcia wszystkich istniejących elementów o jedną pozycję w prawo. Z tego powodu przy częstym dodawaniu elementów na początek lepiej użyć struktury deque z modułu collections. Insert() akceptuje również indeksy ujemne, co pozwala na wstawianie elementów od końca listy. Jeśli podany indeks przekracza długość listy, element zostanie wstawiony na koniec, a Python nie zgłosi błędu. Ta cecha różni insert() od bezpośredniego przypisania do indeksu, które generuje IndexError przy próbie dostępu poza zakres. W praktyce insert() jest używane rzadziej niż append(), głównie w sytuacjach wymagających utrzymania określonej kolejności elementów, na przykład w kolejkach priorytetowych.

Należy pamiętać, że złożoność obliczeniowa insert() wynosi O(N), ponieważ przesunięcie elementów wymaga kopiowania w pamięci.

11/50
Łączenie list -- extend()
  • Do połączenia dwóch list w jedną całość możemy użyć dedykowanej metody .extend().
  • Metoda ta pobiera jako argument inną kolekcję iterowalną (np. listę lub krotkę) i dodaje wszystkie jej elementy po kolei na koniec bieżącej listy.
  • Podobnie jak .append(), metoda .extend() modyfikuje oryginalną listę bezpośrednio w miejscu i nie zwraca żadnej nowej wartości.
  • Różnica polega na tym, że .append() dodałoby całą przekazaną listę jako pojedynczy, zagnieżdżony element (listę w liście).
  • Z kolei .extend() rozpakowuje przekazaną kolekcję i dodaje jej pojedyncze elementy jeden po drugim, co jest bardziej wydajne niż wykonywanie wielu operacji append w pętli.
Zapamiętaj: extend() to idealne narzędzie do masowego dodawania wielu elementów z innej listy bez tworzenia zagnieżdżonych struktur.
l1 = [1, 2]
l2 = [3, 4]
l1.extend(l2)  # l1 zostaje rozszerzona o elementy z l2
print(l1)  # [1, 2, 3, 4]
            
Schemat extend() vs append()

Różnica między extend() a append() jest często źródłem nieporozumień wśród osób uczących się Pythona. Append() dodaje przekazany obiekt jako jeden element, nawet jeśli jest to lista, tworząc strukturę zagnieżdżoną. Extend() natomiast iteruje po przekazanym obiekcie i dodaje każdy jego element oddzielnie, co daje efekt spłaszczenia o jeden poziom zagnieżdżenia. Extend() jest wydajniejsze niż wielokrotne wywoływanie append() w pętli, ponieważ działa na poziomie zoptymalizowanego kodu C. Wewnętrznie extend() może skorzystać z wydajnych operacji kopiowania pamięci, szczególnie gdy łączymy dwie listy tego samego typu. Warto wiedzieć, że extend() akceptuje dowolny obiekt iterowalny, nie tylko listy – można nim dodać elementy z krotki, zbioru, a nawet generatora. W praktyce extend() jest preferowane, gdy chcemy połączyć dwie listy w miejscu, bez tworzenia nowego obiektu.

Alternatywą dla extend() jest operator +=, który również rozszerza listę w miejscu, działając analogicznie do extend().

12/50
Łączenie list -- operator +
  • Alternatywną metodą łączenia list jest użycie operatora dodawania +, który realizuje operację konkatenacji.
  • W przeciwieństwie do metody .extend(), użycie operatora + nie modyfikuje żadnej z oryginalnych list w miejscu.
  • Zamiast tego operator ten tworzy i zwraca całkowicie nową listę w pamięci, będącą połączeniem elementów obu list wejściowych.
  • Należy pamiętać, że próba połączenia listy z obiektem innego typu (np. napisem czy pojedynczą liczbą) za pomocą + wywoła błąd TypeError.
  • Konkatenacja za pomocą operatora + jest niezwykle wygodna, lecz przy łączeniu bardzo dużych list może być mniej wydajna niż użycie .extend().
Zapamiętaj: Użyj operatora '+', gdy chcesz zachować nienaruszone listy wejściowe i potrzebujesz stworzyć nowy obiekt z połączonymi danymi.
a = ["kot"]
b = ["pies"]
c = a + b  # c jest nową listą ['kot', 'pies']
print(c)   # a i b pozostały nienaruszone
            
Schemat konkatenacji list

Operator konkatenacji + dla list tworzy nowy obiekt w pamięci, co różni go od extend() i +=, które modyfikują istniejącą listę. Wybór między tymi podejściami ma istotne implikacje dla wydajności i zarządzania pamięcią. Przy łączeniu dużych list operator + może być mniej wydajny, ponieważ wymaga alokacji nowej pamięci i skopiowania wszystkich elementów z obu list źródłowych. Z drugiej strony, jeśli chcemy zachować oryginalne listy w nienaruszonym stanie, operator + jest właściwym wyborem, ponieważ nie modyfikuje żadnej z nich. Python nie pozwala na łączenie listy z innymi typami za pomocą +, co chroni przed przypadkowymi błędami typów. Operator konkatenacji jest często używany w kodzie do tworzenia nowych list w jednej linii, szczególnie gdy łączymy kilka niewielkich kolekcji. W nowoczesnym Pythonie często spotyka się również rozpakowywanie list za pomocą operatora * jako alternatywę dla konkatenacji.

Warto pamiętać, że operator + zawsze zwraca nowy obiekt, więc nie wpływa na oryginalne listy użyte w wyrażeniu.

13/50
Usuwanie elementów -- remove()
  • Metoda .remove() pozwala na usunięcie elementu z listy na podstawie jego konkretnej wartości, a nie pozycji indeksu.
  • Kiedy wywołujemy .remove(wartosc), Python przeszukuje listę od lewej do prawej i usuwa pierwsze napotkane wystąpienie tej wartości.
  • Pozostałe elementy znajdujące się po prawej stronie zostaną automatycznie przesunięte w lewo, aby wypełnić lukę w pamięci.
  • Jeśli szukana wartość nie występuje w liście, interpreter zgłosi błąd ValueError, co może zatrzymać działanie programu.
  • Z tego powodu dobrą praktyką jest wcześniejsze upewnienie się, czy element znajduje się w liście za pomocą operatora in.
Zapamiętaj: remove() usuwa TYLKO pierwsze wystąpienie danej wartości w liście. Jeśli wartość występuje wielokrotnie, pozostałe kopie nie zostaną usunięte.
zwierzeta = ["kot", "pies", "kot"]
zwierzeta.remove("kot")  # Usuwa pierwszego kota
print(zwierzeta)  # ['pies', 'kot']
            
Schemat remove()

Metoda remove() przeszukuje listę sekwencyjnie od lewej do prawej, co oznacza, że jej złożoność obliczeniowa wynosi O(N). Po znalezieniu pierwszego pasującego elementu, jest on usuwany, a pozostałe elementy przesuwane w lewo, co również wymaga czasu proporcjonalnego do długości listy. Ważną konsekwencją tej implementacji jest to, że jeśli lista zawiera duplikaty, remove() usunie tylko pierwsze wystąpienie wartości. Aby usunąć wszystkie wystąpienia, konieczne jest użycie pętli while lub wyrażenia listowego z filtrowaniem. Metoda remove() zgłasza wyjątek ValueError, jeśli szukana wartość nie występuje w liście, dlatego przed jej użyciem warto sprawdzić obecność elementu operatorem in. W praktyce remove() jest używane, gdy znamy wartość do usunięcia, ale nie jej pozycję w liście. Jeśli znamy indeks, wydajniejsze jest użycie del lub pop().

Należy pamiętać, że remove() porównuje elementy za pomocą operatora ==, a nie is, więc działa na wartości, a nie na tożsamości obiektów.

14/50
Usuwanie elementów -- pop()
  • Metoda .pop() to unikalny i bardzo wygodny sposób na usuwanie elementów z listy, ponieważ łączy usuwanie z odczytem.
  • Wywołanie .pop() bez żadnych argumentów usuwa ostatni element z listy i jednocześnie zwraca go jako wynik swojej pracy.
  • Możemy również przekazać opcjonalny indeks jako argument, np. .pop(i), co usunie i zwróci element znajdujący się na dokładnie tej pozycji.
  • Po usunięciu elementu ze środka listy, wszystkie elementy po prawej stronie automatycznie przesuwają się w lewo w celu zapełnienia luki.
  • Próba wywołania .pop() na całkowicie pustej liście lub podanie nieistniejącego indeksu wywoła błąd IndexError.
Zapamiętaj: Metoda pop() bez argumentów jest niezwykle szybka (czas O(1)) i idealnie nadaje się do implementacji struktur typu stos.
kolory = ["czerwony", "zielony", "niebieski"]
ostatni = kolory.pop()  # Usuwa i zwraca 'niebieski'
pierwszy = kolory.pop(0)  # Usuwa i zwraca 'czerwony'
print(kolory)  # ['zielony']
            
Schemat pop()

Metoda pop() jest unikalna, ponieważ łączy w sobie dwie operacje: usunięcie elementu z listy i zwrócenie go jako wartości. Wywołanie pop() bez argumentów usuwa ostatni element, co jest operacją o złożoności O(1), ponieważ nie wymaga przesuwania elementów w tablicy. To sprawia, że lista w Pythonie doskonale nadaje się do implementacji struktury danych typu stos (LIFO). Gdy wywołujemy pop() z indeksem, usuwany jest element na tej pozycji, a wszystkie następne przesuwane w lewo, co ma złożoność O(N). Warto zwrócić uwagę, że pop() na pustej liście zgłasza IndexError, podobnie jak próba usunięcia nieistniejącego indeksu. W projektach programistycznych pop() jest często używane przy implementacji algorytmów cofania operacji, przeglądania historii przeglądarki czy analizie wyrażeń nawiasowych. Połączenie append() i pop() pozwala na symulację stosu w czystym Pythonie bez importowania dodatkowych modułów.

W przeciwieństwie do remove(), pop() wymaga podania indeksu, a nie wartości, co jest kluczową różnicą między tymi metodami.

15/50
Usuwanie elementów -- del
  • Instrukcja del to wbudowane słowo kluczowe Pythona, które pozwala na usuwanie elementów z listy bez pobierania ich wartości.
  • Może służyć do usuwania pojedynczego elementu na podstawie indeksu, np. del lista[i], co modyfikuje listę w miejscu.
  • Co jest niezwykle przydatne, instrukcja del współpracuje z wycinkami, umożliwiając szybkie skasowanie całego fragmentu listy za jednym razem, np. del lista[1:3].
  • Słowo kluczowe del może również posłużyć do całkowitego usunięcia zmiennej z pamięci programu, co wyczyści referencję.
  • Jest to bardzo niskopoziomowa, szybka i uniwersalna konstrukcja ułatwiająca zarządzanie pamięcią w Pythonie.
Zapamiętaj: Używaj instrukcji del z wycinkami do szybkiego usuwania wielu elementów z listy bez pisania pętli.
liczby = [10, 20, 30, 40, 50]
del liczby[1]      # Usuwa 20
del liczby[1:3]    # Usuwa wycinek [30, 40]
print(liczby)      # [10, 50]
            
Schemat del

Instrukcja del w Pythonie jest uniwersalnym narzędziem do usuwania obiektów, które nie jest metodą listy, a wbudowanym słowem kluczowym. W kontekście list, del pozwala na usuwanie elementów przez indeks oraz na usuwanie całych wycinków w jednej operacji. W przeciwieństwie do pop(), del nie zwraca usuniętego elementu, co czyni je odpowiednim wyborem, gdy wartość nie jest potrzebna po usunięciu. del potrafi również usuwać całe zmienne z przestrzeni nazw, co zwalnia pamięć zajmowaną przez obiekt. W przypadku list, del lista[1:3] jest wydajniejszy niż wielokrotne wywoływanie pop() w pętli, ponieważ przesunięcie elementów następuje jednorazowo. Warto wiedzieć, że del współpracuje również z krokami, na przykład del lista[::2] usunie co drugi element. Należy jednak uważać na modyfikację listy podczas iteracji, ponieważ może to prowadzić do pomijania elementów. W codziennej praktyce del jest niezastąpione przy czyszczeniu fragmentów list.

Różnica między del a pop() polega na tym, że pop() zwraca usunięty element, a del nie zwraca niczego.

16/50
Czyszczenie listy -- clear()
  • Jeżeli chcemy pozbyć się wszystkich elementów z listy, ale zachować sam obiekt listy w pamięci programu, używamy metody .clear().
  • Wywołanie .clear() natychmiastowo usuwa absolutnie wszystkie elementy, zmniejszając długość listy do 0 i pozostawiając ją całkowicie pustą.
  • Jest to o wiele bardziej optymalne podejście niż przypisanie do zmiennej nowej, pustej listy w postaci lista = [].
  • Przypisanie nowej listy tworzy bowiem całkowicie nowy obiekt w pamięci RAM, a stary obiekt musi zostać usunięty przez systemowy Garbage Collector.
  • Z kolei .clear() czyści wewnętrzną tablicę wskaźników istniejącego obiektu, co pozwala na bezpieczne ponowne użycie tej samej instancji listy.
Zapamiętaj: Użycie metody clear() gwarantuje, że wszystkie inne referencje wskazujące na tę samą listę również zobaczą, że została wyczyszczona.
koszyk = ["chleb", "mleko"]
koszyk.clear()  # Wyczyszczenie listy
print(koszyk)  # []
            
Schemat clear()

Metoda clear() została wprowadzona w Pythonie 2.7 i 3.1 jako wygodny sposób na opróżnienie listy bez tworzenia nowego obiektu. Jej główną zaletą nad przypisaniem lista = [] jest to, że wszystkie referencje wskazujące na tę listę również zobaczą, że lista została wyczyszczona. Przypisanie nowej pustej listy tworzy nowy obiekt, a stary obiekt (wskazywany przez inne zmienne) pozostaje w pamięci nienaruszony. Wewnętrznie clear() działa poprzez ustawienie wskaźnika na nową, pustą tablicę i zresetowanie licznika długości, co jest bardzo szybkie. W praktyce clear() jest używane, gdy chcemy wielokrotnie używać tej samej listy jako bufora lub akumulatora, bez ryzyka utraty referencji przez inne części programu. Jest to szczególnie przydatne w programach wielowątkowych lub gdy lista jest przechowywana jako część większego obiektu. Warto pamiętać, że clear() działa wyłącznie na listach, w przeciwieństwie do del które jest bardziej uniwersalne.

Po wywołaniu clear() długość listy wynosi 0, a lista jest gotowa do ponownego użycia bez alokowania nowej pamięci.

17/50
Sprawdzanie przynależności -- in
  • Sprawdzanie, czy określony element znajduje się wewnątrz listy, to jedno z najbardziej fundamentalnych zadań w codziennym programowaniu.
  • Python oferuje do tego celu genialny i niezwykle czytelny operator in oraz jego zaprzeczenie not in.
  • Zapis element in lista zwraca wartość logiczną True, jeśli element występuje w kolekcji, lub False w przeciwnym wypadku.
  • Operator ten jest w pełni zintegrowany z instrukcjami warunkowymi if, co pozwala pisać kod zbliżony do naturalnego języka angielskiego.
  • Pod maską operator in przeszukuje listę sekwencyjnie element po elemencie, co oznacza, że jego złożoność czasowa wynosi O(N) i rośnie liniowo wraz z rozmiarem listy.
Zapamiętaj: Używaj operatora in do weryfikacji obecności elementu przed wywołaniem metody remove() lub index(), aby uniknąć błędów ValueError.
baza = ["admin", "user", "moderator"]
if "admin" in baza:
    print("Dostęp przyznany.")
            
Przykłady operatora in

Operator in jest jednym z najbardziej charakterystycznych elementów składni Pythona, umożliwiającym naturalne językowo sprawdzanie przynależności do kolekcji. W przypadku list, operator in wykonuje sekwencyjne przeszukiwanie, co oznacza złożoność obliczeniową O(N). Dzięki temu operator in jest idealny dla małych i średnich kolekcji, ale dla bardzo dużych list warto rozważyć użycie zbioru (set), który oferuje sprawdzanie w czasie stałym. Operator in jest często używany w połączeniu z not, tworząc czytelny zapis element not in lista. W Pythonie operator in działa nie tylko na listach, ale na wszystkich typach sekwencyjnych i kolekcjach, w tym na napisach, słownikach i zbiorach. W przypadku słowników in sprawdza przynależność klucza, a nie wartości. Użycie operatora in przed wywołaniem metod remove() czy index() jest dobrą praktyką, która zapobiega nieoczekiwanym wyjątkom. W nowoczesnym Pythonie operator in jest często implementowany przez specjalną metodę __contains__(), którą można przeciążyć we własnych klasach.

Warto zapamiętać, że operator in sprawdza równość wartości (==), a nie tożsamość obiektu (is).

18/50
Zliczanie -- count()
  • Metoda .count() służy do sprawdzania, jak wiele razy dana wartość występuje jako element wewnątrz listy.
  • Kiedy wywołujemy .count(wartosc), Python przeszukuje całą listę i zlicza elementy, które są równe przekazanemu argumentowi pod kątem wartości (używając operatora porównania ==).
  • Jeśli badana wartość w ogóle nie występuje w liście, metoda nie generuje żadnych błędów, lecz po prostu zwraca wartość 0.
  • Jest to niezwykle przydatne narzędzie przy zbieraniu statystyk w raportach tekstowych, zliczaniu punktów w prostych grach czy weryfikacji liczby wystąpień identyfikatorów.
  • Metoda ta przeszukuje listę od początku do końca, więc jej czas wykonania zależy liniowo od długości listy.
Zapamiętaj: Metoda count() porównuje elementy pod kątem wartości (==), a nie tożsamości obiektów w pamięci (is).
głosy = ["tak", "nie", "tak", "tak"]
poparcie = głosy.count("tak")  # Zwróci 3
print(f"Głosów na tak: {poparcie}")
            
Przykład count()

Metoda count() jest prostym, ale użytecznym narzędziem do analizy częstotliwości występowania wartości w liście. Podobnie jak remove(), count() przeszukuje całą listę sekwencyjnie, co daje złożoność O(N). W przypadku potrzeby zliczania wielu różnych wartości w tej samej liście, wielokrotne wywołanie count() może być nieefektywne – lepiej wtedy użyć collections.Counter. Metoda count() jest bezpieczna, ponieważ dla wartości nieobecnej w liście zwraca 0, nie zgłaszając błędu. W praktyce count() znajduje zastosowanie przy zliczaniu głosów, analizie wyników testów, czy sprawdzaniu liczby duplikatów w danych. Należy pamiętać, że count() porównuje elementy za pomocą operatora ==, co oznacza, że może poprawnie zliczać różne typy danych, o ile są porównywalne. W połączeniu z len() i warunkami, count() umożliwia szybką weryfikację, czy dana wartość dominuje w kolekcji.

W przypadku napisów i innych typów, count() działa podobnie, ale na listach jest najczęściej używany do zliczania elementów prostych typów.

19/50
Szukanie pozycji -- index()
  • Metoda .index() pozwala na zlokalizowanie fizycznej pozycji (numeru indeksu) określonego elementu w liście.
  • Gdy wywołujemy .index(wartosc), Python skanuje listę od lewej strony i zwraca indeks pierwszego napotkanego elementu o tej wartości.
  • Jeśli szukana wartość nie występuje w liście, interpreter natychmiast zgłosi błąd ValueError i zakończy działanie programu.
  • Aby napisać w pełni bezpieczny kod, warto przed użyciem .index() sprawdzić obecność elementu za pomocą operatora in.
  • Metoda ta pozwala również na opcjonalne parametry start i stop, które ograniczają obszar wyszukiwania do wybranego fragmentu listy.
Zapamiętaj: Metoda index() zwraca indeks TYLKO pierwszego napotkanego wystąpienia wartości. Ignoruje kolejne powtórzenia w dalszej części listy.
studenci = ["Jan", "Anna", "Maria"]
pozycja = studenci.index("Anna")  # Zwróci 1
print(f"Anna jest pod indeksem: {pozycja}")
            
Przykład index()

Metoda index() jest niezbędna, gdy potrzebujemy poznać pozycję elementu w liście, a nie tylko sprawdzić jego obecność. W odróżnieniu od operatora in, który zwraca wartość logiczną, index() zwraca liczbę całkowitą określającą indeks pierwszego wystąpienia wartości. Metoda ta zgłasza ValueError, jeśli szukana wartość nie występuje w liście, co często wymaga zabezpieczenia się przed wyjątkiem. Opcjonalne parametry start i stop pozwalają na ograniczenie zakresu wyszukiwania, co jest przydatne przy znajdowaniu kolejnych wystąpień tej samej wartości. W połączeniu z pętlą while, index() może być użyte do iteracyjnego znajdowania wszystkich pozycji danej wartości. W praktyce index() jest często używane w algorytmach wymagających znajomości pozycji elementu, takich jak sortowanie, wyszukiwanie czy manipulacja danymi. Warto pamiętać, że podobnie jak remove(), index() znajduje tylko pierwsze wystąpienie – aby znaleźć kolejne, trzeba użyć parametru start.

Jeśli potrzebujemy tylko sprawdzić obecność elementu bez znajomości jego pozycji, wydajniejszy jest operator in.

20/50
Sortowanie -- sort()
  • Metoda .sort() służy do układania elementów listy w określonym porządku bezpośrednio w miejscu (ang. in-place).
  • Sortowanie to modyfikuje oryginalną listę, niszcząc jej dotychczasowy układ elementów w pamięci i nie zwraca żadnej nowej wartości (zwraca None).
  • Domyślnie sortowanie odbywa się rosnąco, lecz możemy to zmienić przekazując parametr reverse=True.
  • Metoda pozwala również na podanie parametru key=funkcja, który definiuje własne kryterium sortowania (np. sortowanie po długości napisów za pomocą key=len).
  • Pod maską Python stosuje algorytm Timsort, który jest niezwykle wydajnym, stabilnym hybrydowym algorytmem o złożoności O(N log N).
Zapamiętaj: Próba posortowania listy zawierającej nieporównywalne typy danych (np. liczby zmieszane z napisami) zakończy się błędem TypeError.
liczby = [5, 2, 8, 1]
liczby.sort()  # Sortowanie rosnąco w miejscu
print(liczby)  # [1, 2, 5, 8]
liczby.sort(reverse=True)  # Malejąco
            
Schemat sortowania listy

Metoda sort() implementuje algorytm Timsort, który jest hybrydowym algorytmem sortującym łączącym sortowanie przez wstawianie z sortowaniem przez scalanie. Timsort został opracowany przez Tima Petersa w 2002 roku specjalnie dla Pythona i od tego czasu został zaadoptowany przez wiele innych języków i platform. Algorytm ten jest stabilny, co oznacza, że elementy o równych wartościach zachowują swoją względną kolejność sprzed sortowania. Jest to szczególnie ważne przy sortowaniu wielopoziomowym, gdzie najpierw sortujemy według jednego kryterium, a potem według drugiego. Parametr key metody sort() pozwala na sortowanie według własnego kryterium, na przykład sortowanie napisów według długości: lista.sort(key=len). Warto wiedzieć, że sort() modyfikuje listę w miejscu i zwraca None, co odróżnia ją od wbudowanej funkcji sorted(), która zwraca nową listę. Próba sortowania listy zawierającej nieporównywalne typy zakończy się wyjątkiem TypeError, co jest zabezpieczeniem przed nielogicznymi porównaniami.

W nowoczesnym Pythonie sort() jest zoptymalizowany pod kątem rozpoznawania wzorców w danych, co przyspiesza sortowanie częściowo już uporządkowanych list.

21/50
Sortowanie -- sorted()
  • Wbudowana funkcja sorted() to bezpieczna alternatywa dla metody .sort(), która jest kluczowa, gdy chcemy zachować oryginalną kolejność elementów.
  • Zamiast modyfikować listę wejściową, funkcja sorted() tworzy, sortuje i zwraca całkowicie nową listę, pozostawiając oryginał w nienaruszonym stanie.
  • Co jest niezwykle ważne, sorted() może przyjąć dowolną strukturę iterowalną (np. krotkę, słownik, napis), a wynikiem jej działania zawsze będzie posortowana lista.
  • Funkcja ta również obsługuje przydatne parametry reverse=True oraz key=funkcja.
  • Dzięki swojej uniwersalności i bezpieczeństwu dla danych, jest wysoce zalecana w pythonicznym stylu programowania.
Zapamiętaj: Używaj sorted(), gdy nie chcesz bezpowrotnie niszczyć pierwotnego układu danych w oryginalnej liście.
oryginal = [3, 1, 2]
nowa = sorted(oryginal)  # nowa: [1, 2, 3], oryginal: [3, 1, 2]
print(oryginal, nowa)
            
Porównanie sort() vs sorted()

Funkcja sorted() różni się od metody sort() fundamentalnie: tworzy nową, posortowaną listę, pozostawiając oryginalną kolekcję bez zmian. Ta cecha sprawia, że sorted() jest bezpieczniejsza w kodzie, gdzie oryginalna kolejność danych może być potrzebna później. Kolejną zaletą sorted() jest jej uniwersalność – działa na dowolnym obiekcie iterowalnym, nie tylko na listach. Można ją zastosować do krotek, zbiorów, słowników (zwraca posortowane klucze) i innych sekwencji. Funkcja sorted() podobnie jak sort() przyjmuje parametry reverse i key, co zapewnia pełną kontrolę nad procesem sortowania. Złożoność obliczeniowa sorted() wynosi O(N log N), co jest optymalną wartością dla sortowania przez porównania. W praktyce sorted() jest preferowana nad sort(), gdy nie chcemy modyfikować oryginalnej listy lub gdy potrzebujemy posortować kolekcję, która nie jest listą. Funkcja ta jest również wygodniejsza w użyciu w łańcuchach wywołań funkcji i wyrażeniach.

Warto zapamiętać, że sorted() zawsze zwraca listę, niezależnie od typu obiektu wejściowego, co zapewnia spójne zachowanie.

22/50
Odwracanie -- reverse()
  • Metoda .reverse() służy do odwracania kolejności wszystkich elementów listy bezpośrednio w miejscu (ang. in-place).
  • Sprawia ona, że pierwszy element staje się ostatnim, drugi przedostatnim i tak dalej, całkowicie modyfikując oryginalną strukturę danych w pamięci.
  • Metoda ta nie sortuje elementów ani nie porównuje ich wartości – po prostu zamienia je miejscami od zewnątrz do środka listy, co czyni ją wysoce efektywną pamięciowo.
  • Jeśli potrzebujesz odwróconej kopii bez modyfikacji oryginału, użyj wbudowanej funkcji reversed() lub zapisu wycinkowego lista[::-1].
Zapamiętaj: Metoda reverse() modyfikuje oryginalny obiekt w pamięci. Jeśli chcesz zachować oryginał, użyj funkcji reversed(lista).
litery = ["a", "b", "c"]
litery.reverse()  # Odwrócenie w miejscu
print(litery)  # ['c', 'b', 'a']
            
Schemat reverse()

Metoda reverse() jest prostsza niż sort() – nie porównuje elementów, a jedynie odwraca ich kolejność w liście. Działanie reverse() polega na zamianie miejscami pierwszego z ostatnim, drugiego z przedostatnim i tak dalej, aż do środka listy. Ta operacja ma złożoność obliczeniową O(N/2), ponieważ każda para elementów jest zamieniana tylko raz. Podobnie jak sort(), reverse() modyfikuje listę w miejscu i zwraca None. Alternatywą dla reverse(), która nie modyfikuje oryginału, jest użycie wycinka z ujemnym krokiem lista[::-1] lub wbudowanej funkcji reversed(). reversed() zwraca iterator, który przechodzi przez elementy w odwrotnej kolejności, co jest bardziej wydajne pamięciowo niż tworzenie nowej listy. W praktyce reverse() jest używane, gdy chcemy odwrócić kolejną danych bezpośrednio w strukturze, na przykład po wczytaniu danych w kolejności odwrotnej do potrzebnej. W połączeniu z sort() i sorted(), reverse() umożliwia pełną kontrolę nad kolejnością elementów w liście.

Wybór między reverse() a reversed() zależy od tego, czy chcemy modyfikować istniejącą listę, czy tylko potrzebujemy przeglądać elementy w odwrotnej kolejności.

23/50
Kopiowanie list -- problem
  • Jednym z najczęstszych źródeł błędów u początkujących programistów jest nieprawidłowe kopiowanie list i zjawisko tzw. aliasingu.
  • W Pythonie zwykłe przypisanie listy do nowej zmiennej za pomocą operatora =, np. b = a, nie tworzy żadnej kopii danych.
  • Zamiast tego Python tworzy nową zmienną (alias), która wskazuje na dokładnie ten sam fizyczny obiekt listy w pamięci RAM.
  • W rezultacie jakakolwiek modyfikacja listy za pomocą zmiennej b natychmiastowo zmieni zawartość listy widzianą przez zmienną a.
  • Zjawisko to wynika z faktu, że zmienne w Pythonie są jedynie referencjami (wskaźnikami) do fizycznych obiektów w pamięci.
Zapamiętaj: Przypisanie zmiennej 'b = a' nie tworzy nowej listy. Obie zmienne wskazują od tej pory na dokładnie ten sam obiekt w pamięci.
a = [1, 2, 3]
b = a  # b wskazuje na to samo co a
b.append(49)
print(a)  # Niespodzianka: wyświetli [1, 2, 3, 49]!
            
Schemat aliasingu list

Zjawisko aliasingu w Pythonie wynika bezpośrednio z referencyjnego modelu pamięci, w którym zmienne są etykietami, a nie pojemnikami. Gdy przypisujemy listę do innej zmiennej, nie tworzymy kopii danych, a jedynie nową referencję wskazującą na ten sam obiekt w pamięci. To zachowanie różni się od typów prostych, takich jak liczby czy napisy, które są niemutowalne i zachowują się jak wartości. Aliasing jest częstym źródłem błędów, ponieważ modyfikacja listy za pośrednictwem jednej zmiennej wpływa na wszystkie zmienne współdzielące ten sam obiekt. Problem ten staje się szczególnie widoczny przy przekazywaniu list do funkcji, gdzie modyfikacje wewnątrz funkcji są widoczne na zewnątrz. Aby uniknąć aliasingu, należy jawnie tworzyć kopie list za pomocą metody copy(), wycinka [:] lub funkcji deepcopy() dla struktur zagnieżdżonych. Zrozumienie aliasingu jest kluczowe dla pisania bezpiecznego i przewidywalnego kodu w Pythonie.

W przypadku typów niemutowalnych, takich jak krotki, aliasing nie stanowi problemu, ponieważ ich zawartość nie może być zmieniona.

24/50
Kopiowanie powierzchowne
  • Aby utworzyć niezależny obiekt listy, na którym możemy bezpiecznie wykonywać modyfikacje, musimy zastosować mechanizm kopiowania.
  • Najprostszą metodą jest wykonanie kopiowania powierzchownego (ang. shallow copy), które tworzy nowy obiekt listy i wypełnia go referencjami do tych samych elementów.
  • Kopiowanie powierzchowne możemy zrealizować za pomocą metody .copy(), zapisu wycinkowego lista[:] lub przekazania listy do konstruktora list().
  • Wszystkie te trzy warianty dają identyczny rezultat i są w pełni bezpieczne przy pracy z listami jednowymiarowymi.
  • Modyfikacja elementów w skopiowanej liście nie wpłynie wtedy w żaden sposób na oryginalną listę.
Zapamiętaj: Kopiowanie powierzchowne tworzy nową listę, ale jeśli lista zawiera inne listy (struktury zagnieżdżone), ich kopia nie zostanie utworzona.
oryginal = [1, 2, 3]
kopia = oryginal.copy()  # Nowy obiekt
kopia.append(99)
print(oryginal)  # Bez zmian: [1, 2, 3]
            
Schemat kopiowania powierzchownego

Kopiowanie powierzchowne (shallow copy) tworzy nową listę, ale wypełnia ją referencjami do tych samych obiektów, które znajdowały się w oryginalnej liście. Dla list jednowymiarowych zawierających typy proste, shallow copy jest w pełni wystarczające, ponieważ modyfikacja elementów w kopii nie wpływa na oryginał. W Pythonie dostępne są trzy równoważne sposoby wykonania shallow copy: metoda copy(), wycinek [:] i konstruktor list(oryginal). Wszystkie te metody działają identycznie i wybór między nimi jest kwestią preferencji stylistycznych i konwencji przyjętych w zespole. Problem z shallow copy pojawia się, gdy lista zawiera obiekty mutowalne, takie jak inne listy czy słowniki. Wtedy kopia współdzieli wewnętrzne obiekty z oryginałem, a modyfikacja tych obiektów jest widoczna w obu listach. W takich przypadkach konieczne jest użycie deep copy, która rekurencyjnie kopiuje wszystkie zagnieżdżone obiekty. Zrozumienie różnicy między shallow a deep copy jest niezbędne przy pracy ze złożonymi strukturami danych.

W codziennej praktyce, jeśli nie mamy pewności co do struktury kopiowanych danych, warto użyć domyślnie shallow copy i głębiej analizować tylko w przypadku rzeczywistej potrzeby.

25/50
Kopiowanie pełne
  • Problem z kopiowaniem powierzchownym pojawia się w momencie, gdy nasza lista jest strukturą wielopoziomową (czyli zawiera wewnątrz inne listy).
  • Ponieważ kopiowanie powierzchowne kopiuje jedynie wskaźniki, wewnętrzne listy wciąż będą współdzielone przez oryginał i kopię, co prowadzi do błędów.
  • Aby stworzyć całkowicie niezależną kopię na wszystkich poziomach zagnieżdżenia, musimy przeprowadzić kopiowanie pełne (ang. deep copy).
  • W tym celu importujemy wbudowany moduł copy i wywołujemy funkcję copy.deepcopy().
  • Ta funkcja rekurencyjnie obchodzi całą strukturę i tworzy zupełnie nowe obiekty dla każdego poziomu zagnieżdżenia, co gwarantuje pełną izolację danych.
Zapamiętaj: Zawsze używaj copy.deepcopy() przy kopiowaniu list wielowymiarowych lub zagnieżdżonych struktur obiektów.
import copy
oryginal = [[1, 2], [3, 4]]
kopia = copy.deepcopy(oryginal)  # Pełna kopia
kopia[0][0] = 99
print(oryginal[0])  # Bez zmian: [1, 2]!
            
Schemat kopiowania pełnego

Pełne kopiowanie (deep copy) jest operacją kosztowniejszą obliczeniowo i pamięciowo niż shallow copy, ponieważ wymaga rekurencyjnego przejścia przez całą strukturę danych i utworzenia niezależnych kopii każdego obiektu na każdym poziomie zagnieżdżenia. Funkcja deepcopy() z modułu copy radzi sobie nawet z cyklicznymi referencjami, dzięki wewnętrznemu mechanizmowi śledzenia już skopiowanych obiektów. W praktyce deepcopy() jest używane, gdy potrzebujemy w pełni niezależnej kopii złożonej struktury, na przykład przy tworzeniu kopii zapasowych danych w pamięci lub przy implementacji algorytmów wymagających izolacji danych. Należy jednak pamiętać, że deepcopy() nie zawsze może skopiować dowolny obiekt – niektóre obiekty systemowe, takie jak deskryptory plików czy gniazda sieciowe, nie obsługują kopiowania. W takich przypadkach deepcopy() zgłasza wyjątek. Własne klasy mogą kontrolować proces kopiowania przez zdefiniowanie metod __copy__() i __deepcopy__(). Wybór między shallow a deep copy powinien być podyktowany strukturą danych i wymaganiami projektu.

Dzięki deepcopy() możemy bezpiecznie pracować na złożonych strukturach danych bez ryzyka nieoczekiwanej modyfikacji oryginału.

26/50
Listy zagnieżdżone
  • Listy zagnieżdżone (inaczej listy wielowymiarowe) to po prostu listy, których elementami są inne listy.
  • Jest to podstawowy i niezwykle popularny sposób na reprezentowanie dwuwymiarowych struktur danych, takich jak macierze matematyczne, arkusze kalkulacyjne czy siatki gier.
  • Dostęp do pojedynczych elementów w zagnieżdżonej liście odbywa się za pomocą wielokrotnego indeksowania zapisanego obok siebie, np. macierz[i][j].
  • Pierwszy indeks i wskazuje wiersz (czyli wewnętrzną listę), a drugi indeks j wskazuje kolumnę (konkretny element w tym wierszu).
  • Python nie nakłada żadnych limitów na poziom zagnieżdżenia list, dzięki czemu możemy tworzyć nawet struktury trójwymiarowe.
Zapamiętaj: Każdy element listy zagnieżdżonej jest pełnoprawnym obiektem listy, na którym można wywoływać metody takie jak append() czy pop().
macierz = [[1, 2], [3, 4]]
wiersz = macierz[0]      # [1, 2]
element = macierz[1][0]  # Zwróci 3
print(wiersz, element)
            
Schemat listy zagnieżdżonej

Listy zagnieżdżone są naturalnym rozszerzeniem koncepcji listy, pozwalającym na modelowanie danych wielowymiarowych. W praktyce programistycznej listy zagnieżdżone są używane do reprezentowania macierzy matematycznych, tabel danych, siatek gier, obrazów w formacie bitmapowym i wielu innych struktur. Dostęp do elementów w liście zagnieżdżonej wymaga wielokrotnego indeksowania, gdzie każdy kolejny indeks zagłębia się o jeden poziom. Python nie narzuca ograniczeń na głębokość zagnieżdżenia, choć w praktyce rzadko przekracza się trzy poziomy. Ważnym aspektem pracy z listami zagnieżdżonymi jest świadomość, że każda wewnętrzna lista jest niezależnym obiektem, który można modyfikować oddzielnie. List comprehension doskonale nadają się do tworzenia i przekształcania list zagnieżdżonych, oferując zwięzły zapis operacji na strukturach wielowymiarowych. W przypadku pracy z dużymi macierzami warto rozważyć bibliotekę NumPy, która oferuje znacznie wydajniejsze operacje na tablicach wielowymiarowych.

W czystym Pythonie, listy zagnieżdżone pozostają jednak najbardziej elastycznym narzędziem do modelowania danych dwuwymiarowych.

27/50
Iterowanie po liście
  • Przeglądanie elementów listy za pomocą pętli to jedno z najczęstszych zadań realizowanych w programowaniu.
  • Python oferuje do tego niezwykle elegancką pętlę for element in lista, która w każdym kroku automatycznie przypisuje kolejną wartość do zmiennej sterującej.
  • Nie musimy ręcznie zarządzać indeksami ani liczyć długości listy, co minimalizuje ryzyko popełnienia błędów typu "off-by-one".
  • Jeśli jednak w ciele pętli potrzebujemy również informacji o aktualnym indeksie elementu, optymalnym i zalecanym podejściem jest użycie wbudowanej funkcji enumerate().
  • Funkcja ta zwraca dwuelementowe krotki (indeks, element), które możemy rozpakować bezpośrednio w nagłówku pętli.
Zapamiętaj: Unikaj stosowania konstrukcji 'for i in range(len(lista))' do zwykłego przeglądania elementów. Użycie enumerate() jest o wiele bardziej pythoniczne.
owoce = ["jabłko", "banan"]
for i, owoc in enumerate(owoce):
    print(f"Indeks {i}: {owoc}")
            
Schemat iterowania po liście

Iterowanie po elementach listy za pomocą pętli for jest podstawowym wzorcem przetwarzania danych w Pythonie. Pythonowa pętla for różni się od odpowiedników w innych językach tym, że automatycznie pobiera kolejne elementy z iteratora, eliminując potrzebę ręcznego zarządzania indeksami. Funkcja enumerate() jest pythonicznym rozwiązaniem, gdy potrzebujemy jednocześnie indeksu i wartości elementu. Wewnętrznie enumerate() zwraca iterator produkujący krotki (indeks, element), co pozwala na rozpakowanie w nagłówku pętli. W praktyce enumerate() jest preferowane nad ręcznym zliczaniem za pomocą osobnej zmiennej, ponieważ eliminuje ryzyko błędów związanych z nieprawidłową inkrementacją. Iterowanie po liście z użyciem range(len(lista)) jest uznawane za antywzorzec, chyba że faktycznie potrzebujemy modyfikować elementy listy w miejscu. Python oferuje również zaawansowane funkcje iteracyjne, takie jak zip() do równoległego przeglądania wielu list, czy itertools dla złożonych wzorców iteracji.

Zrozumienie różnych technik iteracji po liście jest kluczowe dla pisania wydajnego i czytelnego kodu w Pythonie.

28/50
Funkcje min(), max(), sum()
  • Python dostarcza zbiór wbudowanych i wysoko zoptymalizowanych funkcji do błyskawicznego wyciągania podstawowych statystyk z list liczbowych.
  • Funkcja sum(lista) dodaje do siebie wszystkie elementy i zwraca ich łączną sumę arytmetyczną.
  • Funkcje min(lista) oraz max(lista) przeszukują całą kolekcję w czasie liniowym O(N) i zwracają odpowiednio najmniejszą oraz największą wartość.
  • Użycie tych wbudowanych funkcji jest wysoce zalecane, ponieważ są one napisane w zoptymalizowanym kodzie C, co czyni je nieporównywalnie szybszymi od ręcznie pisanych pętli.
  • Funkcje te mogą być wywołane na dowolnej liście, której elementy można ze sobą logicznie porównywać.
Zapamiętaj: Połączenie sum(lista) / len(lista) to najprostszy i najbardziej optymalny sposób na obliczenie średniej ocen w Pythonie.
oceny = [4, 5, 3, 5]
razem = sum(oceny)  # 17
najlepsza = max(oceny)  # 5
print(f"Suma: {razem}, Max: {najlepsza}")
            
Przykłady min, max, sum

Wbudowane funkcje min(), max() i sum() są zoptymalizowanymi narzędziami do szybkiego wyciągania statystyk z list liczbowych. Wszystkie trzy funkcje są zaimplementowane w języku C i działają znacznie szybciej niż ręczne pętle w Pythonie. Funkcja sum() jest szczególnie użyteczna w połączeniu z wyrażeniami listowymi i generatorowymi, co pozwala na sumowanie przefiltrowanych lub przekształconych danych w jednej linii. Funkcje min() i max() przyjmują opcjonalny parametr key, który pozwala na określenie kryterium porównania, na przykład max(lista, key=len) znajdzie najdłuższy element. Wszystkie trzy funkcje działają na dowolnych obiektach iterowalnych, nie tylko na listach. Należy jednak pamiętać, że sum() działa tylko na typach liczbowych, a próba sumowania napisów zakończy się błędem TypeError – do łączenia napisów służy metoda join(). W praktyce funkcje te są niezastąpione przy szybkiej analizie danych i często używane w połączeniu z innymi funkcjami wbudowanymi. Obliczenie średniej za pomocą sum() / len() to jeden z najczęstszych wzorców w analizie danych w Pythonie.

W przypadku pustej listy, funkcje min() i max() zgłaszają ValueError, co wymaga zabezpieczenia przed wywołaniem.

29/50
List comprehension -- wprowadzenie
  • List Comprehension (wyrażenie listowe) to jedna z najbardziej spektakularnych, wydajnych i cenionych cech składniowych języka Python.
  • Pozwala ona na dynamiczne generowanie nowej listy na podstawie istniejącej kolekcji w jednej, niezwykle czytelnej linijce kodu.
  • Zamiast pisać pustą listę, deklarować pętlę for i wywoływać metodę .append(), możemy zapisać cały ten proces wewnątrz nawiasów kwadratowych: [wyrażenie for element in kolekcja].
  • Taki zapis jest nie tylko o wiele krótszy i bardziej elegancki, ale również działa znacznie szybciej.
  • Wynika to z faktu, że pętla wewnątrz wyrażenia listowego jest wykonywana bezpośrednio na poziomie zoptymalizowanego kodu bajtowego interpretera.
Zapamiętaj: List comprehension to standard w nowoczesnym kodzie Python. Używaj go do prostych transformacji danych w celu poprawy wydajności i czytelności.
liczby = [1, 2, 3]
kwadraty = [x * x for x in liczby]  # [1, 4, 9]
print(kwadraty)
            
Schemat list comprehension

Wyrażenia listowe (list comprehension) są uznawane za jedną z najbardziej eleganckich i pythonicznych cech języka. Ich składnia została zainspirowana notacją matematyczną, co czyni ją naturalną dla osób przyzwyczajonych do formalnego zapisu zbiorów. Wyrażenia listowe są nie tylko krótsze, ale również często szybsze od tradycyjnych pętli for z append(), ponieważ interpreter może zoptymalizować ich wykonanie na poziomie kodu bajtowego. Podstawowa składnia [expr for var in iterable] jest równoważna z czteroliniową pętlą for, czyniąc kod znacznie bardziej zwięzłym. Wyrażenia listowe można łączyć z warunkami if, zarówno do filtrowania (po for), jak i do warunkowej transformacji (przed for). Wewnętrzna zmienna sterująca w wyrażeniu listowym jest widoczna również poza nim w Pythonie 2, ale w Pythonie 3 jest lokalna dla wyrażenia, co zapobiega zanieczyszczaniu przestrzeni nazw. W codziennej praktyce wyrażenia listowe są preferowane do prostych transformacji i filtrowania danych.

Należy jednak unikać zbyt skomplikowanych wyrażeń listowych, ponieważ mogą one stać się nieczytelne – w takim wypadku lepiej użyć tradycyjnej pętli.

30/50
List comprehension z warunkiem
  • Wyrażenia listowe w Pythonie posiadają wbudowane wsparcie dla filtrowania elementów na etapie ich dodawania do nowej listy.
  • Możemy to osiągnąć poprzez dopisanie klauzuli warunkowej if na samym końcu wyrażenia listowego: [wyrażenie for element in kolekcja if warunek].
  • W takim układzie, badany element zostanie poddany transformacji i wstawiony do wynikowej listy tylko wtedy, gdy warunek logiczny po słowie if będzie prawdziwy (True).
  • Pozwala to na jednoczesne filtrowanie i transformowanie danych w jednej i bardzo czytelnej operacji.
  • Ta unikalna konstrukcja zastępuje wielolinijkowe bloki pętli for połączone z instrukcjami warunkowymi.
Zapamiętaj: Użycie warunku 'if' na końcu wyrażenia listowego działa jak wbudowany filtr, pomijając elementy niespełniające kryteriów.
dane = [1, 12, 8, 15, 3]
parzyste = [x for x in dane if x % 2 == 0]  # [12, 8]
print(parzyste)
            
Schemat list comprehension z if

Łączenie filtrowania z transformacją w wyrażeniach listowych to potężne narzędzie, które pozwala na przetwarzanie danych w jednej, czytelnej linii. Warunek if na końcu wyrażenia listowego działa jak filtr – element jest dodawany do nowej listy tylko, jeśli spełnia zadany warunek. W ten sposób można w prosty sposób wyselekcjonować tylko parzyste liczby, tylko napisy o długości większej niż 5, czy tylko dodatnie wartości. Połączenie filtrowania z transformacją jest szczególnie przydatne przy oczyszczaniu i przygotowywaniu danych do analizy. Wyrażenia listowe z warunkiem są wydajniejsze niż łączne użycie filter() i map(), ponieważ wykonują obie operacje w jednym przebiegu. W praktyce często spotyka się wyrażenia listowe z warunkiem do walidacji danych, na przykład do odrzucenia pustych lub niepoprawnych wartości. Zrozumienie różnicy między warunkiem filtrującym (po for) a wyrażeniem warunkowym (przed for) jest kluczowe dla poprawnego stosowania wyrażeń listowych.

Warunek na końcu wyrażenia listowego zmniejsza liczbę elementów w wyniku, podczas gdy wyrażenie warunkowe przed for zachowuje długość kolekcji.

31/50
List comprehension -- przykłady
  • Przyjrzyjmy się praktycznym zastosowaniom wyrażeń listowych w codziennej pracy programisty Python w celu utrwalenia tej wiedzy.
  • Klasycznym przykładem jest transformacja wielkości liter w liście napisów, np. konwersja wszystkich nazw na wielkie litery za pomocą .upper().
  • Wyrażenia listowe są również niezastąpione przy oczyszczaniu surowych danych, na przykład przy usuwaniu zbędnych spacji z tekstu za pomocą metody .strip().
  • Możemy w ten sposób łatwo rzutować typy danych, na przykład konwertować listę napisów zawierających cyfry na listę rzeczywistych liczb całkowitych.
  • Wszystkie te operacje realizowane są w sposób czytelny, zwięzły i zoptymalizowany pamięciowo.
Zapamiętaj: List comprehension potrafi zastąpić skomplikowane pętle operujące na tekstach, czyniąc kod bardziej profesjonalnym i szybszym.
surowe = ["  jabłko ", " banan  "]
oczyszczone = [owoc.strip().upper() for owoc in surowe]
print(oczyszczone)  # ['JABŁKO', 'BANAN']
            
Przykłady list comprehension

Praktyczne zastosowania wyrażeń listowych w codziennej pracy programisty Pythona są niezwykle różnorodne. Często używa się ich do transformacji danych, na przykład do konwersji temperatur z Celsjusza na Fahrenheita, do wyodrębniania konkretnych pól z listy obiektów, czy do normalizacji danych. Wyrażenia listowe doskonale sprawdzają się przy pracy z napisami, gdzie można je łączyć z metodami klasy str, takimi jak strip(), lower(), upper() czy replace(). W analizie danych wyrażenia listowe są używane do szybkiego tworzenia list pochodnych na podstawie istniejących kolekcji. W połączeniu z funkcjami wbudowanymi, takimi jak sum(), min() czy max(), wyrażenia listowe umożliwiają zwięzłe obliczenia agregujące. Wyrażenia listowe mogą również zawierać zagnieżdżone pętle for, co pozwala na spłaszczanie list wielowymiarowych lub generowanie kombinacji elementów. Warto pamiętać, że wyrażenia listowe zawsze tworzą nową listę w pamięci, co może być problematyczne przy bardzo dużych zbiorach danych.

W takich przypadkach warto rozważyć użycie wyrażeń generatorowych, które są leniwe i nie przechowują całego wyniku w pamięci.

32/50
Zagnieżdżone list comprehension
  • Wyrażenia listowe w Pythonie mogą być również zagnieżdżane, co pozwala na generowanie lub przetwarzanie struktur wielowymiarowych.
  • Klasycznym zastosowaniem zagnieżdżonego wyrażenia listowego jest dynamiczne generowanie dwuwymiarowej macierzy wypełnionej określonymi wartościami (np. zerami).
  • Innym częstym wzorcem jest tzw. spłaszczanie (ang. flattening) listy zagnieżdżonej, czyli konwersja dwuwymiarowej listy w jedną płaską listę jednowymiarową.
  • Składnia takiego wyrażenia może na początku wydawać się skomplikowana, ponieważ kolejność pętli for odpowiada kolejności, w jakiej pisalibyśmy je w tradycyjny sposób.
  • Zrozumienie tej mechaniki pozwala na błyskawiczne przekształcanie dowolnych struktur siatkowych.
Zapamiętaj: Pamiętaj, że w zagnieżdżonych wyrażeniach listowych pierwsza pętla for od lewej to pętla zewnętrzna, a kolejna to pętla wewnętrzna.
macierz = [[1, 2], [3, 4]]
plaska = [x for wiersz in macierz for x in wiersz]
print(plaska)  # [1, 2, 3, 4]
            
Schemat zagnieżdżonego LC

Zagnieżdżone wyrażenia listowe to zaawansowana technika, która pozwala na przetwarzanie struktur wielowymiarowych w zwięzły sposób. Kolejność pętli for w zagnieżdżonym wyrażeniu listowym odpowiada kolejności zagnieżdżenia w tradycyjnej pętli – pierwsza pętla jest najbardziej zewnętrzna. Spłaszczanie listy dwuwymiarowej, czyli utworzenie jednowymiarowej listy zawierającej wszystkie elementy z podlist, jest klasycznym zastosowaniem zagnieżdżonego wyrażenia listowego. Innym popularnym zastosowaniem jest transpozycja macierzy, gdzie za pomocą zagnieżdżonego wyrażenia listowego zamieniamy wiersze na kolumny. Zagnieżdżone wyrażenia listowe mogą zawierać również warunki if na dowolnym poziomie, co pozwala na precyzyjne filtrowanie na każdym etapie przetwarzania. Należy jednak pamiętać, że zbyt głęboko zagnieżdżone wyrażenia listowe stają się nieczytelne – w takich przypadkach lepiej użyć tradycyjnych pętli. W praktyce zagnieżdżone wyrażenia listowe są używane do szybkiego prototypowania i przetwarzania danych w analizie danych.

Jeśli zagnieżdżone wyrażenie listowe staje się zbyt skomplikowane, warto podzielić je na kilka prostszych operacji.

33/50
Lista jako stos
  • Lista w Pythonie może być w niezwykle łatwy sposób wykorzystana do zaimplementowania klasycznej struktury danych typu stos (ang. stack).
  • Stos działa w oparciu o zasadę LIFO (ang. Last In, First Out), co oznacza, że element wstawiony jako ostatni jest usuwany jako pierwszy.
  • Do odzwierciedlenia zachowania stosu używamy wyłącznie dwóch wbudowanych metod listy: .append() do odkładania elementu na wierzchołek stosu oraz .pop() bez argumentów do zdejmowania elementu z wierzchołka.
  • Obie te metody są wysoko zoptymalizowane i wykonują się w czasie stałym O(1), co gwarantuje maksymalną wydajność działania.
  • Stosy są powszechnie używane przy implementacji algorytmów cofania operacji (Undo) oraz analizie składniowej.
Zapamiętaj: Używanie listy jako stosu jest wysoce zalecane i w pełni wydajne, ponieważ operacje na końcu listy nie wymagają przesuwania elementów.
stos = []
stos.append("strona_1")  # Odłożenie na stos
stos.append("strona_2")
biezaca = stos.pop()       # Pobiera i usuwa 'strona_2'
print(biezaca, stos)
            
Schemat stosu

Implementacja stosu za pomocą listy w Pythonie jest naturalna i wydajna, ponieważ lista oferuje metody append() i pop(), które działają na końcu listy w czasie stałym O(1). Stos jest strukturą danych działającą na zasadzie LIFO, używaną w wielu algorytmach i aplikacjach. W informatyce stosy są stosowane do zarządzania wywołaniami funkcji (call stack), implementacji cofania operacji (undo), przeszukiwania grafów w głąb (DFS) oraz analizy wyrażeń matematycznych. W Pythonie, ze względu na wygodę użycia list, implementacja stosu sprowadza się do kilku linii kodu bez potrzeby importowania dodatkowych bibliotek. Należy jednak pamiętać, że lista w Pythonie nie jest strukturalnie ograniczona do zachowania LIFO – programista musi sam pilnować, aby nie usuwać elementów z środka stosu. W praktyce lista jako stos jest używana w implementacji algorytmów, gdzie potrzebujemy dostępu tylko do ostatnio dodanego elementu. Dla bardziej zaawansowanych zastosowań, gdzie potrzebujemy również ograniczenia rozmiaru, można użyć collections.deque z parametrem maxlen.

W porównaniu do własnej implementacji stosu, lista w Pythonie jest zoptymalizowana i gotowa do użycia bez dodatkowego kodu.

34/50
Lista jako kolejka
  • Próba użycia zwykłej listy w Pythonie jako kolejki działającej w oparciu o zasadę FIFO (ang. First In, First Out) napotyka na poważny problem wydajnościowy.
  • Kolejka FIFO wymaga dodawania elementów na koniec (za pomocą .append()), ale usuwania z samego początku (za pomocą .pop(0)).
  • Usunięcie elementu z indeksu 0 wymusza na interpreterze przesunięcie wszystkich pozostałych N-1 elementów w pamięci RAM, co zajmuje czas O(N) i drastycznie zwalnia przy dużych zbiorach.
  • Aby rozwiązać ten problem, Python dostarcza specjalistyczną strukturę danych collections.deque (kolejkę dwukierunkową).
  • Klasa deque umożliwia błyskawiczne dodawanie i usuwanie elementów z obu końców w optymalnym czasie stałym O(1).
Zapamiętaj: Nigdy nie używaj pop(0) na dużych listach do implementacji kolejki. Zamiast tego zaimportuj deque z modułu collections.
from collections import deque
kolejka = deque(["klient_1"])
kolejka.append("klient_2")  # Dodanie na koniec
pierwszy = kolejka.popleft()  # Pobiera 'klient_1' w czasie O(1)!
print(pierwszy)
            
Schemat kolejki

Użycie zwykłej listy jako kolejki FIFO jest nieefektywne, ponieważ usuwanie elementu z początku listy (pop(0)) wymaga przesunięcia wszystkich pozostałych elementów w lewo, co ma złożoność O(N). Moduł collections oferuje klasę deque, która jest zoptymalizowana do szybkiego dodawania i usuwania elementów z obu końców. deque jest zaimplementowany jako podwójnie połączona lista, co zapewnia stały czas dostępu do obu końców. W praktyce deque jest używane nie tylko do implementacji kolejek, ale również jako wydajny bufor kołowy. Klasa deque oferuje metody popleft() i appendleft() do operacji na lewym końcu, które są równie wydajne jak pop() i append() na prawym końcu. W Pythonie 3.5+ deque jest zaimplementowany w języku C, co dodatkowo poprawia jego wydajność. W zastosowaniach wymagających przetwarzania danych w kolejności FIFO, deque jest zdecydowanie lepszym wyborem niż lista. Warto również wiedzieć, że deque obsługuje ograniczenie maksymalnego rozmiaru za pomocą parametru maxlen.

Przekroczenie maxlen powoduje automatyczne usunięcie elementu z przeciwnego końca, co jest przydatne przy implementacji buforów cyklicznych.

35/50
Rozpakowywanie list
  • Rozpakowywanie (ang. unpacking) to niezwykle elegancka funkcja języka Python pozwalająca na bezpośrednie przypisanie elementów listy do osobnych zmiennych w jednej linijce.
  • Aby rozpakowywanie zakończyło się sukcesem, liczba zmiennych po lewej stronie operatora = musi dokładnie odpowiadać długości listy po prawej stronie.
  • Jeśli lista ma inną długość, Python zgłosi błąd ValueError.
  • Bardzo pomocnym rozszerzeniem tego mechanizmu jest użycie operatora gwiazdki *, który pozwala na przechwycenie nadmiarowych elementów.
  • Zmienna oznaczona gwiazdką (np. *reszta) automatycznie zbierze wszystkie pozostałe elementy listy i zapisze je w postaci nowej listy.
Zapamiętaj: Użycie operatora gwiazdki '*' w rozpakowywaniu eliminuje ryzyko wystąpienia błędu ValueError przy nieznanej długości listy.
barwy = ["czerwony", "zielony", "niebieski"]
pierwsza, *reszta = barwy  # pierwsza: 'czerwony', reszta: ['zielony', 'niebieski']
print(pierwsza, reszta)
            
Schemat rozpakowywania

Rozpakowywanie list to jedna z najbardziej eleganckich cech Pythona, która pozwala na przypisanie elementów sekwencji do indywidualnych zmiennych w jednej linii. Mechanizm ten działa nie tylko z listami, ale z dowolnymi obiektami iterowalnymi, w tym krotkami i napisami. Operator gwiazdki (*) w rozpakowywaniu pozwala na elastyczne przypisywanie nadmiarowych elementów do listy, co jest szczególnie przydatne, gdy długość sekwencji nie jest znana. W Pythonie 3.5+ operator rozpakowywania (*) może być użyty nie tylko w przypisaniach, ale również przy tworzeniu nowych list i wywołaniach funkcji. Odpowiednikiem gwiazdki dla słowników jest operator **, który rozpakowuje pary klucz-wartość. Rozpakowywanie jest często używane w pętlach, szczególnie w połączeniu z enumerate() i zip(), gdzie zwracane krotki są bezpośrednio przypisywane do zmiennych. W praktyce rozpakowywanie znacząco poprawia czytelność kodu, eliminując potrzebę odwoływania się do elementów przez indeksy. Należy jednak pamiętać, że liczba zmiennych musi odpowiadać liczbie elementów w sekwencji, chyba że użyjemy operatora *.

Rozpakowywanie z gwiazdką działa również w środku większych sekwencji, na przykład [first, *middle, last] = lista.

36/50
Listy zagnieżdżone jako reprezentacja macierzy
  • Praca z danymi dwuwymiarowymi, takimi jak macierze matematyczne czy tabele baz danych, opiera się w czystym Pythonie na zagnieżdżaniu list.
  • Każdy wiersz macierzy jest reprezentowany jako osobna lista wewnętrzna, a cała macierz to lista zewnętrzna przechowująca te wiersze.
  • Taka reprezentacja jest niezwykle elastyczna, ponieważ wiersze mogą teoretycznie mieć różne długości (tzw. jagged arrays), choć w macierzach dbamy o równy wymiar.
  • Aby pobrać konkretną wartość z danej komórki macierzy, stosujemy podwójne indeksowanie w formacie macierz[wiersz][kolumna].
  • Pierwszy krok wybiera cały wiersz (który sam jest listą), a drugi pobiera z niego pojedynczą wartość w czasie stałym O(1).
Zapamiętaj: Pamiętaj, że macierz w Pythonie to lista list. Indeksowanie zaczyna się od zera zarówno dla wierszy, jak i dla kolumn.
matrix = [
    [1, 2, 3],
    [4, 5, 6]
]
element = matrix[1][2]  # Pobiera wiersz 1, kolumnę 2 -> 6
print(element)
            
Tabelka macierzy 3x3

Listy zagnieżdżone jako reprezentacja macierzy są naturalnym wyborem w czystym Pythonie, ponieważ nie wymagają importowania dodatkowych bibliotek. Każdy wiersz macierzy jest osobną listą, a dostęp do elementu wymaga podania dwóch współrzędnych: wiersza i kolumny. Taka reprezentacja jest elastyczna, ponieważ wiersze mogą mieć różne długości, co pozwala na modelowanie tablic postrzępionych. W praktyce programistycznej macierze w czystym Pythonie są używane w małych projektach edukacyjnych, prototypach i zastosowaniach, gdzie wydajność numeryczna nie jest kluczowa. Dla zaawansowanych obliczeń macierzowych zaleca się użycie biblioteki NumPy, która oferuje znacznie wydajniejsze operacje dzięki implementacji w języku C i Fortran. Mimo to, zrozumienie macierzy jako list zagnieżdżonych jest ważne, ponieważ buduje intuicję dotyczącą indeksowania wielowymiarowego. Operacje takie jak dodawanie macierzy, mnożenie czy transpozycja mogą być implementowane za pomocą zagnieżdżonych pętli lub wyrażeń listowych. W edukacji programistycznej macierze są często używane do nauki indeksowania dwuwymiarowego i zagnieżdżonych pętli.

Listy zagnieżdżone jako macierze są fundamentem do zrozumienia bardziej zaawansowanych struktur danych i bibliotek numerycznych.

37/50
Transponowanie macierzy przez list comprehension
  • Transpozycja macierzy to klasyczna operacja matematyczna polegająca na zamianie jej wierszy z kolumnami (wiersz staje się kolumną, a kolumna wierszem).
  • W Pythonie możemy zrealizować tę operację w niezwykle elegancki i profesjonalny sposób przy użyciu zagnieżdżonego wyrażenia listowego.
  • Idea polega na iterowaniu po indeksach kolumn oryginalnej macierzy i dla każdej z nich pobieraniu elementów z poszczególnych wierszy na tej pozycji.
  • Taki zapis: [[wiersz[i] for wiersz in macierz] for i in range(3)] pozwala na dokonanie pełnej transpozycji w jednej, zwięzłej linijce.
  • Jest to doskonały przykład pokazujący, jak potężne i ekspresyjne mogą być wyrażenia listowe w zaawansowanych obliczeniach.
Zapamiętaj: Zagnieżdżone wyrażenia listowe pozwalają na wykonywanie skomplikowanych przekształceń macierzowych bez potrzeby importowania zewnętrznych bibliotek.
m = [[1, 2], [3, 4]]
transponowana = [[wiersz[i] for wiersz in m] for i in range(2)]
print(transponowana)  # [[1, 3], [2, 4]]
            
Wizualizacja transpozycji

Transpozycja macierzy to klasyczna operacja, która w Pythonie może być zrealizowana na kilka sposobów, z których każdy ma swoje zalety. Zagnieżdżone wyrażenie listowe jest najbardziej zwięzłym i eleganckim rozwiązaniem, pozwalającym na transpozycję w jednej linii kodu. Alternatywnie można użyć wbudowanej funkcji zip() z operatorem rozpakowywania: list(zip(*macierz)), co jest równie czytelnym i wydajnym rozwiązaniem. Tradycyjne zagnieżdżone pętle są najbardziej przejrzyste dydaktycznie i pozwalają na lepsze zrozumienie procesu transpozycji. W czystym Pythonie, dla małych macierzy, różnice w wydajności między tymi podejściami są pomijalne. Dla dużych macierzy warto użyć biblioteki NumPy, która oferuje metodę .T lub funkcję numpy.transpose(). Zrozumienie transpozycji jest ważne w wielu dziedzinach, od grafiki komputerowej po analizę danych. W praktyce programistycznej transpozycja macierzy jest często używana przy przekształcaniu danych tabelarycznych i przygotowywaniu danych do wizualizacji.

Należy pamiętać, że transpozycja macierzy prostokątnej zmienia jej wymiary: macierz NxM staje się MxN.

38/50
List comprehension z warunkiem if oraz else
  • Często zachodzi potrzeba transformacji elementów w taki sposób, aby dla wartości spełniających warunek logiczny zastosować inną operację niż dla elementów niespełniających tego warunku.
  • W takich sytuacjach stosujemy specyficzny zapis wyrażenia warunkowego (tzw. ternary operator) umieszczony przed pętlą for w wyrażeniu listowym: [x if warunek else y for x in kolekcja].
  • Należy pamiętać o fundamentalnej różnicy: warunek na końcu (po for) filtruje elementy (usuwa je z wyniku).
  • Z kolei wyrażenie if-else na początku modyfikuje każdy element, zachowując oryginalną długość kolekcji.
  • Zrozumienie tej różnicy w pozycjonowaniu słów kluczowych jest kluczem do swobodnego korzystania z wyrażeń listowych.
Zapamiętaj: Umieszczenie 'if-else' na początku wyrażenia listowego służy do transformacji każdego elementu, a nie do filtrowania listy.
liczby = [1, -2, 3, -4]
wynik = [x if x > 0 else 0 for x in liczby]  # Ujemne zamienia na 0
print(wynik)  # [1, 0, 3, 0]
            
Schemat wyrażenia warunkowego w LC

Połączenie wyrażenia warunkowego (ternary operator) z wyrażeniem listowym to zaawansowana technika, która pozwala na transformację każdego elementu kolekcji w zależności od spełnienia warunku. Kluczowe jest zrozumienie różnicy między umieszczeniem if-else przed for (transformacja warunkowa) a umieszczeniem if po for (filtrowanie). W pierwszym przypadku długość listy wynikowej jest taka sama jak oryginalnej, ale elementy są modyfikowane zgodnie z warunkiem. W drugim przypadku lista wynikowa może być krótsza, ponieważ elementy niespełniające warunku są pomijane. Ternary operator w Pythonie ma składnię x if warunek else y, gdzie x jest wartością zwracaną, gdy warunek jest prawdziwy, a y gdy fałszywy. W połączeniu z wyrażeniami listowymi, ternary operator pozwala na implementację złożonych transformacji w jednej linii. W praktyce jest to używane do normalizacji danych, maskowania wartości, czy warunkowego formatowania. Należy jednak zachować umiar – zbyt skomplikowane wyrażenia warunkowe w wyrażeniach listowych stają się nieczytelne.

W przypadku złożonych transformacji warto rozbić kod na osobną funkcję i użyć jej w wyrażeniu listowym, co poprawia czytelność.

39/50
Metoda index() z zakresem wyszukiwania (start, stop)
  • Metoda .index() w Pythonie posiada ukrytą, bardzo przydatną funkcjonalność pozwalającą na ograniczenie obszaru przeszukiwania listy.
  • Domyślnie przeszukuje ona całą listę od indeksu 0, ale możemy przekazać dodatkowe parametry: .index(wartosc, start, stop).
  • Parametr start określa indeks, od którego Python rozpocznie skanowanie listy w poszukiwaniu dopasowania.
  • Parametr stop określa górną granicę zakresu (wyłącznie), powyżej której wyszukiwanie zostanie przerwane.
  • Pozwala to na wydajne wyszukiwanie kolejnych wystąpień tej samej wartości bez potrzeby skanowania całej listy od nowa lub ręcznego wycinania fragmentów listy, co oszczędza pamięć RAM.
Zapamiętaj: Zakres wyszukiwania w metodzie index() pozwala na optymalizację czasu wyszukiwania w bardzo dużych kolekcjach danych.
liczby = [10, 20, 10, 30]
poz_2 = liczby.index(10, 1)  # Szuka liczby 10 zaczynając od indeksu 1
print(poz_2)  # Zwróci 2
            
Schemat zakresu index()

Parametry start i stop metody index() znacząco zwiększają jej elastyczność i wydajność w scenariuszach wymagających wielokrotnego wyszukiwania. Dzięki ograniczeniu zakresu wyszukiwania, można efektywnie znajdować kolejne wystąpienia tej samej wartości bez konieczności tworzenia podlist. Jest to szczególnie przydatne przy analizie danych, gdzie ta sama wartość może występować wielokrotnie w różnych częściach listy. W połączeniu z pętlą while, index() z zakresem pozwala na iteracyjne znajdowanie wszystkich pozycji danej wartości. Należy pamiętać, że parametr stop jest wyłączny, co jest zgodne z ogólną konwencją Pythona dla zakresów i wycinków. Użycie zakresu wyszukiwania jest wydajniejsze niż wycinanie fragmentu listy i szukanie na nim, ponieważ nie wymaga tworzenia nowego obiektu w pamięci. W praktyce technika ta jest używana przy przetwarzaniu logów, gdzie wielokrotnie szukamy tych samych kodów błędów w różnych fragmentach pliku. Zrozumienie parametrów zakresu index() pozwala na pisanie bardziej wydajnego kodu.

Warto wiedzieć, że parametry start i stop działają także z ujemnymi indeksami, co pozwala na wyszukiwanie od końca listy.

40/50
Usuwanie wszystkich wystąpień wartości w pętli while
  • Jak już wiemy, metoda .remove() usuwa wyłącznie pierwsze napotkane wystąpienie danej wartości w liście.
  • Jeśli nasza lista zawiera duplikaty i chcemy usunąć je wszystkie, wywołanie pojedynczego .remove() nie rozwiąże problemu.
  • Idealnym, bezpiecznym rozwiązaniem jest użycie pętli while połączonej z operatorem przynależności in.
  • Pętla będzie wykonywać się tak długo, jak szukana wartość znajduje się w liście: while wartosc in lista: lista.remove(wartosc).
  • Taki zapis gwarantuje całkowite wyczyszczenie duplikatów bez ryzyka zgłoszenia błędu ValueError, który pojawiłby się przy próbie usunięcia nieistniejącego już elementu.
Zapamiętaj: Użycie pętli 'while element in lista:' to najprostszy i najbezpieczniejszy sposób na pozbycie się wszystkich duplikatów wartości z listy.
oceny = [1, 5, 1, 4, 1]
while 1 in oceny:
    oceny.remove(1)  # Usuwa wszystkie jedynki
print(oceny)  # [5, 4]
            
Pętla usuwająca elementy z listy

Usuwanie wszystkich wystąpień wartości z listy za pomocą pętli while i operatora in jest prostym i skutecznym wzorcem. Operator in sprawdza, czy wartość nadal znajduje się w liście, a remove() usuwa pierwsze wystąpienie. Dzięki pętli while, proces jest powtarzany aż do usunięcia wszystkich duplikatów. Należy jednak pamiętać, że ta metoda jest nieefektywna dla dużych list, ponieważ każde wywołanie remove() ma złożoność O(N), a w najgorszym przypadku wykonujemy N takich operacji, co daje łączną złożoność O(N²). Dla dużych kolekcji wydajniejszym rozwiązaniem jest użycie wyrażenia listowego z filtrowaniem lub konstrukcji [x for x in lista if x != wartosc]. Wyrażenie listowe tworzy nową listę w jednym przebiegu, co ma złożoność O(N). Alternatywnie, można użyć pętli for iterującej po kopii listy, co jest bezpieczne i czytelne. W praktyce, jeśli lista jest niewielka, pętla while z remove() jest w pełni akceptowalna. Dla dużych zbiorów danych zdecydowanie lepsze są wyrażenia listowe lub funkcje z modułu itertools.

Przy wyborze metody usuwania duplikatów warto uwzględnić nie tylko wydajność, ale również czytelność i zrozumiałość kodu dla innych programistów.

41/50
Zastosowanie operatora mnożenia list (*) do inicjalizacji
  • Operator mnożenia list * to niezwykle przydatne narzędzie pozwalające na błyskawiczne inicjalizowanie list o z góry określonym rozmiarze.
  • Zapis [wartość] * N tworzy nową listę składającą się z N kopii danej wartości (np. inicjalizacja listy 10 zer: [0] * 10).
  • Jest to bardzo szybka i optymalna operacja alokacji pamięci na poziomie interpretera.
  • Należy jednak zachować ekstremalną ostrożność, gdy inicjalizujemy w ten sposób listy wielowymiarowe, np. [[0] * 2] * 3.
  • W takim przypadku Python nie utworzy niezależnych podlist, lecz skopiuje referencje do tej samej jednej podlisty w pamięci RAM, co doprowadzi do błędów przy modyfikacjach.
Zapamiętaj: Nigdy nie inicjalizuj macierzy za pomocą mnożenia typu '[[0]*M]*N'. Użyj zamiast tego list comprehension: '[[0]*M for _ in range(N)]'.
zerowe = [0] * 5  # Inicjalizacja: [0, 0, 0, 0, 0]
# POPRAWNA inicjalizacja macierzy 3x2:
matrix = [[0] * 2 for _ in range(3)]
            
Inicjalizacja listy mnożeniem

Operator mnożenia list (*) jest wygodnym narzędziem do szybkiej inicjalizacji, ale niesie ze sobą pułapkę związaną z referencyjną naturą Pythona. Gdy mnożymy listę zawierającą obiekty mutowalne, Python nie tworzy niezależnych kopii tych obiektów, a jedynie powiela referencje do tego samego obiektu. W przypadku list jednowymiarowych z typami prostymi nie stanowi to problemu, ale przy inicjalizacji macierzy prowadzi do współdzielenia wierszy. Aby utworzyć prawidłową macierz, należy użyć wyrażenia listowego: [[0] * M for _ in range(N)], które w każdej iteracji tworzy nowy, niezależny wiersz. Operator mnożenia jest często używany do tworzenia listy wypełnionej domyślną wartością, co jest przydatne przy alokacji buforów czy tablic wynikowych. Warto pamiętać, że mnożenie listy przez 0 daje pustą listę, co może być użyteczne w niektórych scenariuszach. Operator mnożenia działa również w drugą stronę: liczba * lista, z identycznym efektem. W praktyce inicjalizacja mnożeniem jest najszybszym sposobem na utworzenie listy o zadanej długości.

Zapamiętanie różnicy między [0] * N a podobnym wyrażeniem dla list zagnieżdżonych pozwala uniknąć trudnych do wykrycia błędów.

42/50
Program: prosta baza studentów
  • Napiszmy kompletny, praktyczny program konsolowy demonstrujący zastosowanie dynamicznych list do zarządzania bazą studentów.
  • Program będzie umożliwiał dodawanie nowych osób, usuwanie studentów z listy oraz wyszukiwanie studenta na podstawie podanego imienia.
  • Wykorzystamy metodę .append() do rejestracji nowych studentów oraz operator in do sprawdzania, czy dana osoba widnieje w naszej bazie.
  • Usunięcie z bazy zrealizujemy za pomocą metody .remove(), upewniając się wcześniej, że student istnieje, by zapobiec awarii programu.
  • Ten prosty program doskonale obrazuje łączenie pętli, instrukcji warunkowych i podstawowych metod manipulacji listami.
Zapamiętaj: Dynamiczne listy są idealną strukturą do przechowywania prostych rejestrów danych w małych aplikacjach konsolowych.
baza = ["Anna", "Jan"]
# Dodanie nowej osoby:
baza.append("Katarzyna")
# Bezpieczne usuwanie:
if "Jan" in baza:
    baza.remove("Jan")
print(f"Aktualna lista: {baza}")
            
Konsolowa baza danych

Program prostej bazy studentów to praktyczne zastosowanie list w scenariuszu zbliżonym do rzeczywistego. Tego typu programy są często pierwszym krokiem w nauce tworzenia aplikacji konsolowych zarządzających danymi. Wykorzystanie listy jako magazynu danych jest naturalne, ponieważ lista dynamicznie dostosowuje swój rozmiar do liczby przechowywanych elementów. Operator in zapewnia szybkie sprawdzenie, czy dany student znajduje się już w bazie, co zapobiega dodawaniu duplikatów. Metoda remove() z zabezpieczeniem in gwarantuje bezpieczne usuwanie bez ryzyka wyjątku ValueError. Taki program można łatwo rozszerzyć o dodatkowe funkcje, takie jak przechowywanie większej ilości informacji o studencie (oceny, adres) za pomocą słowników lub klas. W edukacji programistycznej ten wzorzec jest często rozwijany do bardziej zaawansowanych systemów CRUD. Wykorzystanie pętli i instrukcji warunkowych w połączeniu z listami daje pełną kontrolę nad przepływem danych w programie. Implementacja takiego systemu uczy również projektowania interfejsu użytkownika w konsoli.

Baza studentów oparta na liście jest doskonałym przykładem ilustrującym, jak proste struktury danych mogą służyć do rozwiązywania praktycznych problemów.

43/50
Program: system rezerwacji miejsc w kinie
  • Stwórzmy zaawansowany program symulujący system rezerwacji miejsc w małej sali kinowej za pomocą dwuwymiarowej listy.
  • Siatka foteli zostanie zainicjalizowana jako lista list o wymiarach 3x4, gdzie wartość "Wolne" oznacza dostępne miejsce.
  • Rezerwację konkretnego miejsca realizujemy poprzez zmianę wartości w wybranej komórce macierzy na "Zajęte" za pomocą współrzędnych wiersza i kolumny.
  • Przed zapisaniem rezerwacji program sprawdzi, czy wybrane miejsce nie jest już zarezerwowane, by zapobiec konfliktom.
  • Program ten uczy praktycznej pracy ze strukturami zagnieżdżonymi, indeksowaniem dwuwymiarowym oraz sterowaniem przepływem danych.
Zapamiętaj: Użycie list dwuwymiarowych pozwala na łatwe modelowanie fizycznych układów przestrzennych, takich jak sale kinowe czy plansze gier.
sala = [["Wolne"] * 4 for _ in range(3)]
# Rezerwacja miejsca (rząd 1, fotel 2):
if sala[1][2] == "Wolne":
    sala[1][2] = "Zajęte"
print(sala[1])  # Pokazuje stan drugiego rzędu
            
Siatka foteli kinowych

System rezerwacji miejsc w kinie to doskonały przykład zastosowania list dwuwymiarowych do modelowania rzeczywistej przestrzeni fizycznej. Każdy wiersz macierzy reprezentuje rząd w sali kinowej, a każda kolumna pojedyncze miejsce. Inicjalizacja macierzy za pomocą wyrażenia listowego gwarantuje, że każdy wiersz jest niezależnym obiektem, co zapobiega współdzieleniu stanu między rzędami. Sprawdzenie stanu miejsca przed rezerwacją to przykład walidacji danych, która zapobiega konfliktom i podwójnym rezerwacjom. Taki system można rozbudować o wizualizację sali w konsoli, pokazywanie dostępnych miejsc, czy różne kategorie cenowe. W praktyce podobne systemy są używane w rzeczywistych aplikacjach do rezerwacji biletów, miejsc parkingowych czy sal konferencyjnych. Implementacja tego programu uczy myślenia w kategoriach współrzędnych i operacji na strukturach dwuwymiarowych. Wykorzystanie pętli do wyświetlania stanu sali i walidacji danych wejściowych to umiejętności niezbędne w każdym większym projekcie programistycznym.

Taki program doskonale ilustruje, jak wiedza o listach zagnieżdżonych przekłada się na rozwiązywanie rzeczywistych problemów.

44/50
Dobre praktyki: unikanie skomplikowanych list comprehension
  • Wyrażenia listowe są wspaniałym narzędziem, ale ich nadużywanie i nadmierna komplikacja to jeden z najczęstszych grzechów programistów.
  • Oficjalny przewodnik po stylu PEP 8 wyraźnie stawia czytelność kodu ponad jego zwięzłość (zwięzłość za wszelką cenę).
  • Jeśli Twoje wyrażenie listowe zawiera zagnieżdżone pętle for, skomplikowane filtry logiczne if-else i zajmuje więcej niż jedną linijkę, porzuć je.
  • W takich sytuacjach klasyczna pętla for z odpowiednimi wcięciami, komentarzami i jasną strukturą jest o wiele łatwiejsza do zrozumienia i debugowania.
  • Dobry programista pisze kod, który jest łatwy do przeczytania i utrzymania przez innych członków zespołu.
Zapamiętaj: Złota zasada Pythona: Proste jest lepsze niż złożone. Jeśli kod w list comprehension jest zbyt skomplikowany, napisz tradycyjną pętlę for.
# ZŁA PRAKTYKA (nieczytelne LC):
# x = [[x*y for x in l1 if x > 2] for y in l2 if y < 5]

# DOBRA PRAKTYKA (klasyczny for z komentarzami):
wynik = []
# ... (tradycyjna pętla for z wcięciami)
            
Porównanie czytelnego i nieczytelnego LC

Dobre praktyki w używaniu wyrażeń listowych są zgodne z ogólną filozofią Pythona, która stawia czytelność ponad zwięzłością. PEP-8, oficjalny przewodnik po stylu kodowania w Pythonie, zaleca, aby wyrażenia listowe były krótkie i proste. Jeśli wyrażenie listowe nie mieści się w standardowej linii 79 znaków, należy rozważyć rozbicie go na kilka linii lub zastąpienie tradycyjną pętlą. Zagnieżdżone wyrażenia listowe z więcej niż dwiema pętlami są uznawane za nieczytelne i powinny być unikane. W praktyce, jeśli logika transformacji jest złożona, lepiej napisać tradycyjną pętlę z komentarzami niż zwięzłe, ale niezrozumiałe wyrażenie listowe. Wspólna praca nad kodem wymaga, aby był on zrozumiały dla wszystkich członków zespołu, nie tylko dla autora. Dlatego warto kierować się zasadą: pisz kod tak, aby był zrozumiały dla programisty o poziom niżej doświadczenia. W dobrym kodzie intencje są jasne i nie wymagają dodatkowych komentarzy wyjaśniających skomplikowaną logikę wyrażeń listowych.

Zrównoważone podejście do wyrażeń listowych łączy korzyści płynące ze zwięzłości kodu z jego czytelnością dla innych programistów.

45/50
Ćwiczenie: usuwanie duplikatów
  • Napiszmy algorytm, który usuwa powtarzające się elementy z listy, zachowując jednocześnie ich pierwotną kolejność występowania.
  • Najprostsza konwersja na zbiór list(set(lista)) usuwa duplikaty błyskawicznie, ale bezpowrotnie niszczy oryginalny porządek elementów.
  • Aby zachować kolejność, stworzymy nową pustą listę pomocniczą (np. unikalne = []) służącą jako nasz filtr.
  • Następnie w pętli for będziemy sprawdzać każdy element oryginalnej listy za pomocą operatora in.
  • Jeśli element nie został jeszcze dodany do naszej nowej listy, wstawiamy go tam za pomocą .append(), uzyskując idealnie oczyszczoną listę.
Zapamiętaj: Zachowanie kolejności przy usuwaniu duplikatów wymaga sekwencyjnego przeszukiwania listy w pętli lub użycia słownika.
surowe = [1, 2, 1, 3, 2]
unikalne = []
for x in surowe:
    if x not in unikalne:
        unikalne.append(x)
print(unikalne)  # [1, 2, 3]
            
Lista przed i po usunięciu duplikatów

Usuwanie duplikatów z listy z zachowaniem kolejności jest klasycznym problemem algorytmicznym, który ma kilka rozwiązań o różnej wydajności i złożoności. Najprostsze rozwiązanie z pętlą i operatorem in ma złożoność O(N²), ponieważ dla każdego elementu sprawdzamy, czy znajduje się już w nowej liście. Dla dużych zbiorów danych lepiej użyć zbioru do śledzenia już napotkanych elementów, co redukuje złożoność do O(N). W Pythonie 3.7+ można wykorzystać fakt, że słowniki zachowują kolejność: list(dict.fromkeys(lista)) usunie duplikaty przy zachowaniu kolejności w czasie O(N). Konwersja na zbiór (list(set(lista))) usuwa duplikaty najszybciej, ale nie zachowuje kolejności elementów. Wybór odpowiedniej metody zależy od priorytetów: jeśli kolejność jest ważna, używamy dict.fromkeys() lub pętli ze zbiorem pomocniczym. Jeśli wydajność jest kluczowa, a kolejność nie ma znaczenia, wybieramy konwersję na zbiór. W praktyce problem usuwania duplikatów pojawia się często przy przetwarzaniu danych z plików CSV, baz danych czy API.

Zrozumienie różnych metod usuwania duplikatów i ich złożoności obliczeniowej jest ważne przy projektowaniu wydajnych algorytmów.

46/50
Ćwiczenie: łączenie i sortowanie
  • Zaimplementujmy ćwiczenie polegające na połączeniu dwóch osobnych list liczbowych i posortowaniu wynikowej kolekcji w porządku malejącym.
  • Najpierw dokonamy scalenia list za pomocą operatora konkatenacji +, co da nam nową, nieuporządkowaną listę.
  • Następnie użyjemy wbudowanej funkcji sorted(), przekazując naszą połączoną listę jako pierwszy argument.
  • Aby uzyskać porządek malejący, ustawimy parametr reverse=True w wywołaniu funkcji.
  • Wynik przypiszemy do nowej zmiennej, dzięki czemu oryginalne listy wejściowe pozostaną całkowicie nienaruszone.
  • Jest to bardzo częsty wzorzec przetwarzania danych przed ich prezentacją.
Zapamiętaj: Połączenie operatora '+' oraz funkcji sorted(..., reverse=True) pozwala na eleganckie rozwiązywanie zadań sortowania w jednej linii.
l1 = [15, 3]
l2 = [8, 20]
połączona = l1 + l2
wynik = sorted(połączona, reverse=True)
print(wynik)  # [20, 15, 8, 3]
            
Schemat łączenia list

Ćwiczenie łączenia i sortowania list łączy w sobie dwie podstawowe operacje na kolekcjach: konkatenację i sortowanie. Operator konkatenacji + tworzy nową listę, która jest połączeniem elementów z obu list źródłowych, zachowując ich pierwotną kolejność. Następnie funkcja sorted() porządkuje połączoną listę według określonych kryteriów. Parametr reverse=True zmienia kolejność sortowania z domyślnie rosnącej na malejącą. Warto zauważyć, że łączenie i sortowanie można wykonać w jednej linii: wynik = sorted(l1 + l2, reverse=True). W praktyce ten wzorzec jest często używany przy agregacji danych z różnych źródeł przed prezentacją lub analizą. Łączenie list za pomocą operatora + jest wygodne, ale dla bardzo dużych list może być nieefektywne pamięciowo, ponieważ tworzy nową listę zawierającą kopie wszystkich elementów. Alternatywnie można użyć metody extend() do rozszerzenia jednej listy o elementy drugiej. W edukacji programistycznej to ćwiczenie uczy sekwencyjnego stosowania różnych operacji na danych.

Umiejętność łączenia różnych operacji na listach w czytelny sposób jest cechą dojrzałego programisty Pythona.

47/50
Ćwiczenie: macierz transponowana
  • Przećwiczmy samodzielne napisanie algorytmu transpozycji macierzy 2x3 za pomocą tradycyjnych, zagnieżdżonych pętli for.
  • Zaczynamy od zadeklarowania pustej listy t = [] przed rozpoczęciem wszelkich operacji.
  • Pętla zewnętrzna będzie kontrolować indeksy kolumn oryginalnej macierzy (od 0 do 2), generując nowy wiersz dla nowej macierzy.
  • Pętla wewnętrzna będzie iterować po wierszach oryginalnej macierzy (od 0 do 1) i pobierać elementy na danej pozycji kolumny.
  • Pobrane elementy są dynamicznie gromadzone w nowym wierszu, który po zakończeniu pętli wewnętrznej jest dodawany do macierzy wynikowej.
Zapamiętaj: Napisanie transpozycji za pomocą tradycyjnych pętli uczy logicznego myślenia i ułatwia zrozumienie działania indeksowania dwuwymiarowego.
m = [[1, 2, 3], [4, 5, 6]]
t = []
for col in range(3):
    nowy_wiersz = []
    for row in range(2):
        nowy_wiersz.append(m[row][col])
    t.append(nowy_wiersz)
print(t)  # [[1, 4], [2, 5], [3, 6]]
            
Schemat transpozycji macierzy

Implementacja transpozycji macierzy za pomocą tradycyjnych zagnieżdżonych pętli jest najbardziej przejrzystym dydaktycznie rozwiązaniem. Pętla zewnętrzna iteruje po kolumnach oryginalnej macierzy, a wewnętrzna po wierszach, co daje efekt zamiany współrzędnych. W każdym kroku pętli wewnętrznej pobieramy element macierz[row][col] i dodajemy go do nowego wiersza. Po zakończeniu pętli wewnętrznej, nowy wiersz jest dodawany do macierzy wynikowej. Ten algorytm ma złożoność obliczeniową O(N×M), gdzie N to liczba wierszy, a M to liczba kolumn oryginalnej macierzy. Zrozumienie tego procesu krok po kroku jest niezbędne przed przejściem do bardziej zwięzłych form zapisu, takich jak wyrażenia listowe. W praktyce, mimo że wyrażenia listowe są bardziej zwięzłe, tradycyjne pętle są często preferowane w kodzie, gdzie priorytetem jest jasne pokazanie logiki algorytmu. W nowoczesnym Pythonie, do transpozycji macierzy często używa się funkcji zip() z operatorem rozpakowywania: list(zip(*macierz)).

Niezależnie od wybranej metody, zrozumienie istoty transpozycji jako zamiany wierszy z kolumnami jest kluczowe dla poprawnej implementacji.

48/50
Typowe błędy z listami
  • Podsumujmy najczęstsze błędy i niebezpieczne pułapki, na które narażeni są programiści podczas pracy z listami w Pythonie. Modyfikacja w pętli: Próba usuwania elementów z listy, po której bezpośrednio iterujemy, prowadząca do omijania sprawdzania sąsiednich wartości. Błędne kopiowanie: Przypisywanie list przez operator = (aliasing), co powoduje modyfikowanie oryginału przez inną zmienną. Przekroczenie zakresu: Błędy IndexError wywołane sięganiem po nieistniejący indeks elementu w liście. Błędne inicjalizowanie: Mnożenie zagnieżdżonych list [[0]*M]*N, co prowadzi do współdzielenia referencji podlist w pamięci.
  • Świadomość tych zagrożeń pozwala zaoszczędzić czas i pisać bezbłędny kod.
Zapamiętaj: Zawsze używaj metody copy() przy kopiowaniu list i nigdy nie usuwaj elementów bezpośrednio w pętli for iterującej po tej samej liście.
liczby = [1, 2, 3]
# BŁĄD: index = liczby[5]  # Wywoła IndexError!
# BŁĄD: k = liczby.sort()  # k otrzyma None, a nie posortowaną listę!
            
Lista typowych błędów

Typowe błędy z listami w Pythonie można podzielić na kilka kategorii, z których każda wymaga innego podejścia do debugowania. Błędy aliasingu wynikają z niezrozumienia referencyjnego modelu pamięci i objawiają się nieoczekiwaną modyfikacją współdzielonych obiektów. Błędy modyfikacji w pętli (zmiana rozmiaru listy podczas iteracji) prowadzą do pomijania elementów lub błędów indeksu. Błędy indeksowania poza zakresem (IndexError) są najłatwiejsze do wychwycenia, ponieważ Python natychmiast zgłasza wyjątek. Błędy inicjalizacji macierzy za pomocą mnożenia są podstępne, ponieważ nie powodują natychmiastowego błędu, a jedynie nieprawidłowe zachowanie przy modyfikacji. Świadomość tych zagrożeń i stosowanie dobrych praktyk, takich jak tworzenie kopii przed modyfikacją, znacznie redukuje liczbę błędów w kodzie. W praktyce większość błędów z listami wynika z nieświadomości różnicy między modyfikacją w miejscu a tworzeniem nowego obiektu. Znajomość typowych pułapek pozwala na świadome unikanie ich już na etapie projektowania kodu.

Regularne testowanie kodu i stosowanie asercji do weryfikacji poprawności działania operacji na listach pomaga wychwycić błędy na wczesnym etapie.

49/50
Podsumowanie części 8
  • Gratulacje! W tej części kursu opanowałeś jedną z najważniejszych i najbardziej wszechstronnych struktur danych w Pythonie – dynamiczne listy.
  • Nauczyłeś się tworzyć listy na kilka sposobów, bezpiecznie pobierać ich elementy za pomocą indeksowania dodatniego i ujemnego oraz wycinać fragmenty (slicing).
  • Poznałeś bogaty zestaw wbudowanych metod modyfikujących listy w miejscu (append, insert, extend, remove, pop, clear) oraz słowo kluczowe del.
  • Opanowałeś mechanikę bezpiecznego kopiowania list (powierzchowne vs pełne) w celu unikania błędów aliasingu.
  • Na koniec poznałeś genialną i wysoce wydajną składnię wyrażeń listowych (list comprehension) do szybkiej transformacji danych.
Zapamiętaj: Opanowanie dynamicznych list to milowy krok w Twojej nauce programowania w Pythonie. Listy są fundamentem większości algorytmów.
# Kluczowy zestaw wiedzy z części 8:
# - Listy są zmienne (modyfikacja w miejscu)
# - Kopiowanie wymaga metody copy() lub modułu copy
# - List comprehension to szybka i elegancka transformacja
            
Mapa myśli z pojęciami z części 8

Podsumowanie modułu poświęconego listom to dobry moment na refleksję nad zdobytą wiedzą i umiejętnościami. Listy są fundamentem, na którym opierają się bardziej zaawansowane struktury danych w Pythonie, takie jak stosy, kolejki, listy powiązane czy macierze. Opanowanie indeksowania, wycinków i metod wbudowanych to dopiero początek – prawdziwa siła list ujawnia się w połączeniu z pętlami, wyrażeniami listowymi i funkcjami wbudowanymi. Warto w tym momencie powtórzyć materiał, kładąc nacisk na praktyczne ćwiczenia. Każde z omawianych zagadnień, od prostego append() po zaawansowane wyrażenia listowe, znajduje zastosowanie w codziennej pracy programisty. Regularne eksperymentowanie z kodem i próbowanie różnych podejść do rozwiązania tego samego problemu to najlepszy sposób na ugruntowanie wiedzy. Listy w Pythonie są narzędziem uniwersalnym, którego znajomość jest niezbędna niezależnie od specjalizacji programistycznej.

Gratulacje dla wszystkich, którzy dotrwali do końca tego wymagającego modułu – zdobyta wiedza będzie procentować w kolejnych częściach kursu.

50/50
Co dalej -- zapowiedź części 9
  • W następnej części kursu wejdziemy w świat kolejnych fundamentalnych i wysoce zoptymalizowanych struktur danych w Pythonie.
  • Przeanalizujemy szczegółowo anatomię krotek (typ tuple), które są niezmiennymi odpowiednikami list, zapewniającymi bezpieczeństwo i wysoką wydajność.
  • Poznamy zbiory (typ set), czyli kolekcje unikalnych wartości pozwalające na błyskawiczne wykonywanie operacji matematycznych, takich jak suma czy przecięcie zbiorów.
  • Na koniec opanujemy słowniki (typ dict), czyli wysoko zoptymalizowane struktury przechowujące pary klucz-wartość, stanowiące serce wielu mechanizmów Pythona.
  • Ta wiedza pozwoli Ci na swobodne modelowanie dowolnych relacji między danymi.
Zapamiętaj: Kolejna część kursu pokaże Ci, jak dobierać optymalne struktury danych w zależności od specyfiki rozwiązywanego problemu.
# W części 9 poznamy m.in.:
# - Niezmienne krotki (tuples) i ich zalety
# - Unikalne zbiory (sets) i operacje na zbiorach
# - Słowniki (dicts) - pary klucz-wartość w czasie O(1)
            
Ikony tematów z części 9