Listy w Pythonie — Pytania Rekrutacyjne i Pułapki
Praca i rekrutacja

Listy w Pythonie — Pytania Rekrutacyjne i Pułapki

O
Odpytywarka.pl
| | | 28 wyświetleń

Dlaczego listy padają na rozmowach najczęściej

Listy to fundament Pythona — większość kodu, który napiszesz w pracy, dotyka list w pierwszej minucie. Rekruterzy o tym wiedzą i pierwsze pytania techniczne często krążą właśnie wokół list.

Ten artykuł zbiera zadania rekrutacyjne z Pythona o listach w jednym miejscu: definicje, idiomatyczne wzorce, złożoności operacji i klasyczne pułapki. Każdy snippet jest działający — możesz uruchomić lokalnie i przećwiczyć przed spotkaniem.

Wszystkie przykłady zakładają Python 3.12+. Używamy współczesnej składni typów: list[int] (Python 3.9+) i X | None zamiast Optional[X] (Python 3.10+).

Tworzenie list — literal, list(), comprehension

Listę tworzysz najczęściej literałem [], konstruktorem list(iterable) albo list comprehension. Literal jest najlepszy dla znanych wartości, list() materializuje iterable, a comprehension buduje nową listę z transformacją lub filtrem.

numbers = [1, 2, 3]
chars = list("abc")
squares = [x * x for x in range(5) if x % 2 == 0]

print(numbers)    # [1, 2, 3]
print(chars)     # ['a', 'b', 'c']
print(squares)  # [0, 4, 16]

Pułapka rekrutacyjna: nie używaj lista=[] jako default argumentu funkcji. Taki obiekt powstaje raz, więc kolejne wywołania współdzielą tę samą listę.

def add(x: int, result: list[int] | None = None) -> list[int]:
    if result is None:
        result = []
    result.append(x)
    return result

Indeksowanie i slicing

Listy są sekwencjami indeksowanymi od zera. a[0] pobiera pierwszy element, a[-1] ostatni, a a[start:stop:step] zwraca nową listę. stop jest wyłączny, więc a[:3] pobiera indeksy 0, 1, 2.

data = [10, 20, 30, 40, 50]

print(data[0])       # 10
print(data[-1])      # 50
print(data[1:4])     # [20, 30, 40]
print(data[::-1])    # [50, 40, 30, 20, 10]
print(data[::2])     # [10, 30, 50]

Pułapka rekrutacyjna: slicing tworzy płytką kopię wybranego fragmentu. Dla list zagnieżdżonych kopiuje zewnętrzną listę, ale nie kopiuje wewnętrznych list.

Mutable vs immutable — lista vs tuple

Lista jest mutable: możesz zmieniać elementy, dodawać i usuwać wartości. Tuple jest immutable: po utworzeniu nie zmienisz jej długości ani przypisań indeksowych. Wybieraj tuple dla stałych rekordów, a listę dla kolekcji roboczych.

points = [1, 2, 3]
points.append(4)

record = ("Ala", 32)
name, age = record

print(points)  # [1, 2, 3, 4]
print(name)    # Ala

Pułapka rekrutacyjna: a = b nie kopiuje listy, tylko tworzy alias do tego samego obiektu. Modyfikacja przez jedną nazwę będzie widoczna przez drugą.

b = [1, 2]
a = b
a.append(3)
print(b)  # [1, 2, 3]

Operacje na listach Pythona — złożoności

Dla CPython listy są tablicami dynamicznymi. Kluczowe złożoności do zapamiętania:

  • append(x) na końcu — amortyzowane O(1)
  • len(lista)O(1)
  • insert(i, x)O(n), przesuwa elementy
  • x in listaO(n), przeszukuje liniowo
  • pop() z końca — O(1)
  • pop(i) ze środka lub początku — O(n)
items = [1, 2, 3]

items.append(4)      # amortyzowane O(1)
items.insert(0, 0)   # O(n), przesuwa elementy
last = items.pop()   # O(1)
first = items.pop(0) # O(n), przesuwa resztę

print(len(items))    # O(1)
print(2 in items)    # O(n)

Pułapka rekrutacyjna: lista jako kolejka z pop(0) zwykle przegrywa na rozmowie. Dla częstego zdejmowania z początku użyj collections.deque (kolejka dwustronna z O(1) po obu końcach) — lista musi przesuwać elementy przy każdym pop(0).

List comprehension vs map/filter vs pętla for

List comprehension jest zwykle najczytelniejszy, gdy tworzysz listę z jednego iterable przez prostą transformację i opcjonalny filtr. map i filter zwracają lazy iterators, więc trzeba użyć list(), jeśli potrzebujesz materializacji.

numbers = [1, 2, 3, 4, 5]

a = [x * 2 for x in numbers if x % 2 == 1]
b = list(map(lambda x: x * 2, filter(lambda x: x % 2 == 1, numbers)))

c = []
for x in numbers:
    if x % 2 == 1:
        c.append(x * 2)

print(a == b == c)  # True

Pułapka rekrutacyjna: nie wybieraj map(lambda...) tylko dlatego, że wygląda funkcyjnie. Na rozmowach kwalifikacyjnych comprehension jest częściej idiomatyczny, a pętla for lepsza, gdy logika ma kilka kroków.

sort vs sorted — kiedy która metoda

list.sort() sortuje listę in-place i zwraca None. sorted(iterable) zwraca nową listę. Obie opcje przyjmują key do porównywania po wartości pochodnej oraz reverse=True dla kolejności malejącej.

people = [("Ala", 32), ("Ola", 25), ("Jan", 40)]

sorted_people = sorted(people, key=lambda person: person[1])
print(sorted_people)  # [('Ola', 25), ('Ala', 32), ('Jan', 40)]

people.sort(key=lambda person: person[0], reverse=True)
print(people)        # [('Ola', 25), ('Jan', 40), ('Ala', 32)]

Pułapka rekrutacyjna: lista = lista.sort() ustawi lista na None. To klasyczny błąd, bo metody mutujące w Pythonie zwykle nie zwracają zmienionego obiektu.

Shallow copy vs deep copy

copy(), [:] i list(x) tworzą shallow copy: nowa jest tylko zewnętrzna lista. Elementy pozostają tymi samymi obiektami. Dla list zagnieżdżonych, które mają być niezależne, użyj copy.deepcopy().

import copy

matrix = [[1, 2], [3, 4]]

shallow = matrix.copy()
deep = copy.deepcopy(matrix)

shallow[0].append(99)
print(matrix)  # [[1, 2, 99], [3, 4]]

deep[1].append(88)
print(matrix)   # [[1, 2, 99], [3, 4]] — bez 88, bo deepcopy skopiował wnętrze
print(deep)   # [[1, 2], [3, 4, 88]]

Pułapka rekrutacyjna: [:] nie rozwiązuje problemu zagnieżdżonych struktur. Na rozmowie warto powiedzieć wprost: shallow copy kopiuje kontener, deep copy kopiuje też obiekty wewnątrz.

Iterowanie — enumerate, zip, range

enumerate(lista) daje pary (index, value), zip(a, b) łączy elementy po pozycjach, a range(n) generuje liczby bez budowania listy. Gdy potrzebujesz indeksu i wartości, zwykle wybierz enumerate zamiast range(len(...)) — to bardziej idiomatyczny Python.

names = ["Ala", "Ola", "Jan"]
scores = [10, 20, 15]

for i, name in enumerate(names, start=1):
    print(i, name)

for name, score in zip(names, scores):
    print(f"{name}: {score}")

# range(len(...)) tylko gdy NAPRAWDĘ potrzebujesz samych indeksów
for i in range(len(names)):
    print(i)

Pułapka rekrutacyjna: zip zwraca iterator jednorazowego użycia. Po przejściu pętli jest wyczerpany, więc drugie list(pary) da pustą listę.

pairs = zip([1, 2], ["a", "b"])
print(list(pairs))  # [(1, 'a'), (2, 'b')]
print(list(pairs))  # []

Funkcje agregujące — sum, min, max, any, all

sum, min i max przechodzą po elementach, więc dla listy mają koszt O(n). any zwraca True, gdy znajdzie pierwszy truthy element, a all zwraca False, gdy znajdzie pierwszy falsy element. Obie funkcje są short-circuit.

numbers = [3, 7, 2, 9]

print(sum(numbers))                # 21
print(min(numbers), max(numbers))   # 2 9
print(any(x > 8 for x in numbers)) # True
print(all(x > 0 for x in numbers)) # True

Pułapka rekrutacyjna: sum(lista_list, []) działa, ale jest nieefektywne, bo wielokrotnie tworzy nowe listy. Do spłaszczania użyj comprehension z dwoma for.

data = [[1, 2], [3], [4, 5]]
flat = [x for group in data for x in group]
print(flat)  # [1, 2, 3, 4, 5]

Wzorce rekrutacyjne — reverse, duplikaty, two sum, frequency

Typowe zadania rekrutacyjne sprawdzają, czy umiesz dobrać strukturę danych. Odwracanie listy zrobisz przez nums[::-1] (nowa lista) albo nums.reverse() (in-place, zwraca None). Duplikaty wykryjesz setem, two sum rozwiążesz słownikiem, a najczęstszy element przez zliczanie.

from collections import Counter

def has_duplicates(nums: list[int]) -> bool:
    return len(nums) != len(set(nums))

def two_sum(nums: list[int], target: int) -> tuple[int, int] | None:
    seen: dict[int, int] = {}
    for i, x in enumerate(nums):
        need = target - x
        if need in seen:
            return seen[need], i
        seen[x] = i
    return None

nums = [2, 7, 11, 15, 7]

print(nums[::-1])                          # nowa odwrócona lista
print(has_duplicates(nums))                  # True
print(two_sum(nums, 9))                    # (0, 1)
print(Counter(nums).most_common(1)[0][0])  # 7

Pułapka rekrutacyjna: nie modyfikuj listy podczas iteracji po niej, bo możesz pominąć elementy. Iteruj po kopii albo buduj nową listę z filtrem.

nums = [1, 2, 3, 4]
odd_only = [x for x in nums if x % 2 != 0]
print(odd_only)  # [1, 3]

Sygnały rozpoznawcze: kiedy lista, kiedy inna struktura

Sygnał → wybór:

"sequence with index access"          → list (random access O(1))
"frequent membership check (in)"      → set (`O(1)` zamiast `O(n)`)
"key-value lookup"                    → dict
"queue / dequeue z przodu"            → collections.deque
                                        (lista pop(0) to O(n))
"stack (LIFO)"                        → list (append/pop = O(1))
"immutable record / tuple unpacking"  → tuple
"top-K, priority"                     → heapq (heap zamiast sortowania)
"frequency count"                     → collections.Counter

Sygnały odwrotne — kiedy NIE lista: częsty membership test (→ hash set/map), kolejka FIFO (→ deque), dynamiczny porządek (→ BST/heap).

Najczęstsze błędy

Mutable default argument. def add(item, items=[]) — domyślna lista jest współdzielona między wywołaniami funkcji. Mechanizm: Python tworzy [] raz przy definicji funkcji, każde wywołanie używa tego samego obiektu. Naprawa: def add(item, items=None): if items is None: items = [].

Aliasing zamiast kopii. b = a nie kopiuje listy, tylko nadaje drugą nazwę temu samemu obiektowi — modyfikacja b.append(x) zmienia też a. Mechanizm: Python przekazuje referencję, nie wartość. Naprawa: b = a[:] (shallow copy) albo b = a.copy(); dla zagnieżdżonych list copy.deepcopy(a).

list.sort() zwraca None. Klasyczny bug: result = numbers.sort()result jest None, bo sort() modyfikuje listę in-place. Mechanizm: konwencja Pythona — metody mutujące zwracają None, czyste zwracają nowy obiekt. Naprawa: result = sorted(numbers) (nowa lista) albo numbers.sort() osobno.

list.pop(0) jako kolejka. Wynik logicznie poprawny, ale koszt O(n) per operacja — Python przesuwa wszystkie pozostałe elementy w lewo. Dla n=10⁶ operacji daje O(n²) zamiast O(n). Naprawa: from collections import deque; queue.popleft() w O(1) — szczegóły Stos i Kolejka.

Modyfikacja listy podczas iteracji. for x in items: items.remove(x) pomija elementy — lista przesuwa indeksy podczas iteracji. Mechanizm: usunięcie elementu na indeksie i przesuwa wszystko, ale for idzie do i+1 na starym indeksie. Naprawa: iteruj po kopii (for x in items[:]) albo buduj nową listę przez comprehension z filtrem.

Najczęściej zadawane pytania

Czy lista i tuple to to samo w Pythonie?

Nie. Lista jest mutable (możesz zmieniać elementy), tuple jest immutable (po utworzeniu nie zmienisz długości ani indeksów). Tuple używasz dla stałych rekordów (np. współrzędne), listy dla kolekcji roboczych, które będą się zmieniać.

Jaka jest złożoność operacji append i insert na liście Pythona?

append to amortyzowane O(1) (czasem trzeba realokować, ale średnio stała). insert(i, x) to O(n), bo trzeba przesunąć wszystkie elementy od pozycji i w prawo. Z tego samego powodu pop(0) to też O(n), a pop() z końca to O(1).

Kiedy użyć list comprehension zamiast pętli for?

Gdy tworzysz nową listę z jednego iterable przez prostą transformację i opcjonalny filtr. Pętlę for zostaw na sytuacje z większą logiką, kilkoma krokami albo efektami ubocznymi (np. zapis do bazy). Comprehension jest zwykle szybszy niż pętla z append.

Dlaczego list.sort() zwraca None?

W Pythonie metody mutujące (jak sort, append, reverse) modyfikują obiekt w miejscu i zwracają None. To celowa konwencja — odróżnia operacje in-place od tych zwracających nowy obiekt. Jeśli potrzebujesz nowej posortowanej listy, użyj sorted(lista).

Jak zrobić głęboką kopię listy zagnieżdżonej?

Użyj copy.deepcopy(). Standardowe metody — lista.copy(), lista[:], list(lista) — robią shallow copy: kopiują tylko zewnętrzną listę, a wewnętrzne listy pozostają tymi samymi obiektami. Dla niezależnych zagnieżdżonych struktur tylko deepcopy działa.

Czy mogę modyfikować listę podczas iteracji?

Nie powinnaś — możesz pominąć elementy lub przejść po nich dwukrotnie. Bezpieczne podejścia: iteruj po kopii (for x in lista[:]) albo buduj nową listę z comprehension i przypisz na końcu.

Powiązane tematy

Sprawdź swoją wiedzę quizem

Najlepszy sposób na utrwalenie wiedzy z tych dziesięciu wzorców to przerobienie ich w formie quizu. Wygenerowaliśmy quiz na bazie tego artykułu — odpowiedz na 15 pytań i sprawdź, gdzie masz luki przed rozmową.

Rozwiąż quiz: Operacje na listach w Pythonie

Możesz też wgrać własny PDF z notatkami (do 20 MB) na odpytywarka.pl — w 30-60 sekund wygenerujemy quiz z 15 pytaniami jednokrotnego wyboru z pierwszych 3 stron dokumentu, za darmo i bez rejestracji do rozwiązania.

Źródła

O

Odpytywarka.pl

Platforma do automatycznego generowania quizów z materiałów edukacyjnych. Artykuły oparte na badaniach z zakresu psychologii uczenia się i edukacji.

Więcej o platformie

Powiązane artykuły

Stwórz własny quiz

Zamień swoje notatki, podręczniki lub dowolny PDF w interaktywny quiz. Nauka jeszcze nigdy nie była tak prosta!