Wielki Test Wiedzy o Pythonie cz. II

17.01.2020

Artykuł

W tym języku możesz: automatyzować testy, tworzyć aplikacje webowe, czy wreszcie korzystać z przebogatych możliwości wykorzystania bibliotek do Machine Learningu i Deep Learningu (Data Science). Znasz Pythona? Programujesz na co dzień w tym języku? Pora sprawdzić swoje umiejętności.

Zgodnie z zapowiedzią – publikujemy kolejną część Wielkiego Testu Wiedzy o Pythonie. Tym razem masz okazję zmierzyć się z trudniejszymi zagadnieniami. Publikujemy 15 pytań z puli od łatwej – aż po trudne – w kolejności – według poziomu trudności. Zapoznaj się z zagadnieniami, które przygotował i szczegółowo wyjaśnił nasz trener, a gdy będziesz gotów – sprawdź się w teście z Mikołajem Lewandowskim.

Jeśli chcesz wrócić do pierwszej części Testu, kliknij:
Wielki Test Wiedzy o pythonie – Część I

1. Jak zwyczajowo nazywa się pierwszy argument metody (funkcji w klasie)?

Poprawna odpowiedź to: self.

Pierwszym argumentem, który otrzyma metoda jest referencja wskazująca na instancję danej klasy (obiekt), na którym została wywołana ta metoda. Argument ten przekazywany jest niejawnie (wywołując metodę, nie przekazujemy go, Python sam “doda go” uruchamiając wywołanie funkcji).

Przykładowa metoda:

class Contract:
  def generate(self, law_policy):
    pass

Oraz jej wywołanie:

from law_policy import polish_law

employee_contract = Contract()
employee_contract.generate(polish_law)

W powyższym przykładzie, w ciele metody generate za pomocą argumentu self można dostać się do obiektu zapisanego w zmiennej employee_contract, zaś w argumencie law_policy znajdzie się wartość przekazana jako polish_law.

Nazwanie pierwszego argumentu metody w inny sposób (np. object_reference) byłoby poprawne składniowo (zadziałałoby) jednak jest niezgodne z konwencją i zasadami stylistycznymi dotyczącymi Pythonowego kodu, zdefiniowanymi przez PEP 8:
https://www.python.org/dev/peps/pep-0008/#function-and-method-arguments

2. Jaki jest wynik działania 1_2 + 3_4 ?

Poprawna odpowiedź to 46.

Zapis 1_2 jest równoważny zapisowi 12 – to po prostu liczba 12. Podkreślenia zostaną zignorowane przez interpreter. Analogicznie w przypadku 3_4. Notacja z podkreśleniem została wprowadzona w Pythonie 3.6 dla ułatwienia zapisu zwłaszcza dużych liczb, np. zamiast

money = 10000000

możliwe jest teraz zapisanie:

money = 10_000_000

co zdecydowanie poprawia czytelność.
Pomysł ten został wprowadzony przez PEP 515:
https://www.python.org/dev/peps/pep-0515/

3. Za pomocą jakiego słowa kluczowego możemy zastosować context manager’a?

Poprawna odpowiedź to: with.

Context manager pozwala stworzyć czytelną i re-używalną implementację sytuacji, w której pewne instrukcje powinny zawsze wykonać się przed oraz po, innej dynamicznej części. Często spotykanym przykładem jest praca z plikami:

with open("path/to/file") as data_file:
  for line in data_file:
    print(line)

Pracując z różnymi plikami zawsze chcemy:

  • otworzyć wskazany plik
  • wykonać na nim pewne operacje
  • zamknąć plik

Co ważne, zamknięcie pliku powinno nastąpić zawsze, niezależnie od tego, czy wykonywane na nim operacje zakończyły się powodzeniem czy też zostały przerwane wyjątkiem. Zastosowanie context manager’a gwarantuje powyższe. Alternatywną składnią byłoby zastosowanie bloku try…except…finally jednak np. w przypadku pracy na plikach byłoby to rozwiązanie mniej czytelne i powielające logikę obsługi błędów oraz zamykania plików.

Więcej szczegółów o koncepcji context manager’a można znaleźć w dokumentacji: https://docs.python.org/3/reference/compound_stmts.html#with

4. Jaka instrukcja umożliwi nam wykorzystanie w naszym kodzie modułu csv?

Poprawna odpowiedź to: import csv.

W Pythonie moduł to plik zawierający Pythonowy kod. Aby w implementowanym rozwiązaniu wykorzystać inny moduł (oraz dostępne w nim funkcje, klasy, zmienne, itp.) należy go zaimportować. Jest to informacja dla interpretera, że w poniższym kodzie najpewniej pojawi się odwołanie do tego modułu więc powinien go odszukać. Równocześnie w ten sposób przekazywana jest informacja, gdzie znajduje się dany moduł oraz pod jakim symbolem będzie dostępny. System importowania należy zastosować aby użyć:

  • modułu z biblioteki standardowej
  • modułu z zewnętrznej, zainstalowanej wcześniej biblioteki
  • innego modułu, zaimplementowanego w ramach tej samej aplikacji (np. klasy albo funkcji z innego pliku)

Moduły są zorganizowane w hierarchiczne struktury – podobnie jak pliki na dysku. Taka struktura składa się z modułów nazywanych pakietami (będących odpowiednikiem katalogu na dysku komputera), które mogą zawierać inne pakiety (podkatalogi) albo moduły nie będące pakietami (pliki). Importując konkretny moduł, jego lokalizację należy wskazać stosując notację z kropkami, dla rozdzielenia kolejnych poziomów zagłębienia:

>>> import os.path
>>> print(os.path.join("directory", "file"))
"directory/file"

Aby móc odwołać się do importowanego modułu, funkcji, itp. bez notacji kropkowej należy zastosować konstrukcję from … import …

>>> from os import path
>>> print(path.join("directory", "file"))
"directory/file"

Możliwe jest również nadanie aliasu importowanemu modułowi i odwoływanie się do niego poprzez ten właśnie alias:

>>> from os import path as system_path
>>> print(system_path.join("directory", "file"))
"directory/file"

Ścieżkę do modułu można podawać zarówno w sposób bezwzględny (preferowany w większości przypadków) jak i relatywny.

Więcej szczegółów dotyczących tematu importowania modułów w Pythonie można znaleźć w dokumentacji:
https://docs.python.org/3/reference/import.html

PEP 8 (importowanie):
https://www.python.org/dev/peps/pep-0008/#imports

Artykuły:
https://www.digitalocean.com/community/tutorials/how-to-import-modules-in-python-3
https://realpython.com/absolute-vs-relative-python-imports/

5. Jaka instrukcja zwróci długość ciągu znaków "abc" (liczbę liter/znaków)?

Poprawna odpowiedź to: len("abc").

Wbudowana funkcja len zwraca liczbę elementów (długość) w danym obiekcie. Typ str jest sekwencją znaków i w tym przypadku funkcja len zwraca długość tej sekwencji (liczbę znaków/liter). Dla listy zwrócona zostanie liczba elementów, dla słownika liczba par klucz: wartość. Aby umożliwić działanie funkcji len dla własnej klasy, należy spełnić oczekiwany protokół. W tym przypadku zaimplementować metodę __len__. Gdy na obiekcie tej klasy ktoś użyje funkcji len, metoda __len__ zostanie wywołana, a wartość przez nią zwrócona, będzie wynikiem wywołania funkcji len.

Informacje o funkcji len w dokumentacji:
https://docs.python.org/3/library/functions.html#len

Podstawowe informacje o __len__:
https://stackoverflow.com/questions/2481421/difference-between-len-and-len

6. Następujący fragment kodu: some_variable = [number for number in range(10)] to przykład użycia…?

Poprawna odpowiedź to: List comprehension

List comprehension to skrócona forma zapisu umożliwiająca stworzenie nowej listy na podstawie już istniejącej (listy lub innej sekwencji). Jest to alternatywa dla wykorzystania standardowej pętli for.

some_variable = [number for number in range(10)]

Można zapisać również jako:

some_variable = []
for number in range(10):
  some_variable.append(number)

W tym przypadku zastosowanie list comprehension pozwala uzyskać zwięzłą i czytelną formę. Rolę istniejącej sekwencji pełni w przykładzie range(10), który jest sekwencją liczb od 0 do 9 włącznie. List comprehension może posiadać również warunek, który pozwoli na przefiltrowanie elementów:

some_variable = [number for number in range(10) if number > 5]

W nieco bardziej rozbudowanym przykładzie:

class Student:
  def __init__(self, final_grade):
    self.final_grade = final_grade

students = [Student(3), Student(5), Student(6), Student(5)]
best_students = [student for student in students if student.final_grade > 4]

Analogicznie do tworzenia pętli zagnieżdżonych, możliwe jest również zagnieżdżanie list comprehension. Należy mieć jednak na uwadze, że w przypadku bardziej skomplikowanych wyrażeń lepiej jest posłużyć się podejściem wykorzystującym pętle, gdyż złożone list comprehensions stają się bardzo szybko nieczytelne.
W Pythonie występują również dict comprehensions, set comprehensions (działające analogicznie do list comprehensions, tyle że na innych typach danych) oraz generator expressions (działające w podobny sposób).

Więcej szczegółów można znaleźć w dokumentacji:
https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions

Oraz poniższym artykule:
https://djangostars.com/blog/list-comprehensions-and-generator-expressions/

Informacje o typie range:
https://docs.python.org/3.7/library/stdtypes.html#ranges

7. Który z typów jest mutable?

Poprawna odpowiedź to: list.

W Pythonie, tak jak i w większości języków programowania, typy danych mogą być mutable albo immutable. Obiekty typu immutable nie mogą zostać zmienione. Przykładem może być typ str oraz operacja konkatenacji (łączenia, zobacz: “Która z poniższych komend przypisze do zmiennej result ciąg znaków "Python"?”). Aby uzupełnić imię o brakującą część można zastosować operator +=:

name = "Mik"
name += "ołaj"

Jednak w tym przypadku oryginalny obiekt, reprezentujący ciąg znaków “Mik” nie zostanie zmodyfikowany, zaś do zmiennej name przypisany zostanie nowy obiekt, utworzony z połączenia “Mik” oraz “ołaj”. Obrazuje to dokładniej poniższy przykład:

>>> part_name = "Mik"
>>> name = part_name
>>> name is part_name
True
>>> name += "ołaj"
>>> name is part_name
False
>>> print(name)
Mikołaj
>>> print(part_name)
Mik

Tak więc modyfikacja typu immutable jest niemożliwa. Za każdym razem utworzony zostanie nowy obiekt, a “stary” pozostanie nienaruszony. Wśród wbudowanych w Pythona typów immutable są również:

  • bool
  • int
  • float
  • tuple
  • frozenset
  • oraz wspominany już str

Natomiast obiekt typu mutable może zostać zmodyfikowany. Przykładem typów mutable jest:

  • list
  • set
  • dict

Analogiczny do wcześniejszego przykład dla listy wyglądałby w ten sposób:

>>> numbers = [1, 2, 3]
>>> new_numbers = numbers
>>> new_numbers is numbers
True
>>> new_numbers.append(4)
>>> new_numbers is numbers
True
>>> print(new_numbers)
[1, 2, 3, 4]
>>> print(numbers)
[1, 2, 3, 4]

W powyższym przykładzie następuje modyfikacja obiektu listy, a zmiana widoczna jest zarówno w “nowej” jak i “starej” zmiennej, gdyż obie wskazują nadal na ten sam obiekt (nie powstał żaden nowy obiekt, jak w przypadku typu str). Ta właściwość ma implikacje widoczne np. podczas definiowania funkcji z argumentem typu mutable o domyślnej wartości.

Więcej szczegółów można znaleźć w dokumentacji:
https://docs.python.org/3/library/stdtypes.html#immutable-sequence-types
https://docs.python.org/3/glossary.html#term-immutable
https://docs.python.org/3/glossary.html#term-mutable

Oraz w artykule:
https://medium.com/@meghamohan/mutable-and-immutable-side-of-python-c2145cf72747

8. Jak zwrócić ostatni element z listy grades = [5, 3, 2, 5]?

Poprawna odpowiedź to: grades[-1].

Do poszczególnych elementów listy w Pythonie można odwoływać się za pomocą indeksu, podając numer elementu w kwadratowych nawiasach. Pierwszy element na liście posiada indeks równy 0. Tak więc wywołanie:

print(grades[1])

Wypisze wartość 3.
Aby rozpocząć indeksowanie od końca listy, należy zastosować wartości ujemne. I tak -1 jest indeksem ostatniego elementu w liście, -2 przedostatniego itd.

Operacje na sekwencjach:
https://docs.python.org/3/library/stdtypes.html#common-sequence-operations

Informacje o listach:
https://www.programiz.com/python-programming/list

9. Jakie słowo kluczowe NIE jest elementem bloku obsługi wyjątków (try…)?

Poprawna odpowiedź to: catch.

W Pythonie blok obsługi wyjątków rozpoczyna się słowem kluczowym try: po którym umieszczony jest kod wykonywany w ramach tego bloku. Dalej znajduje się sekcja obsługi wyjątków (except, może wystąpić kilkukrotnie) lub sekcja finally, która zostanie wykonana zawsze, niezależnie od tego czy wystąpił wyjątek. Po słowie kluczowym except można podać typ wyjątku jaki ma zostać “złapany” i obsłużony w danym bloku. Jeżeli rzucony wyjątek nie będzie pasował do podanego typu, interpreter będzie poszukiwał kolejnej sekcji except z pasującym typem. Nie podanie żadnego typu powoduje przechwytywanie wszystkich rodzajów wyjątków. Takie podejście jest niezalecane, gdyż ogranicza elastyczność implementacji. Dobrym wzorcem jest podawanie jak najdokładniejszego typu łapanych wyjątków. Aby obsłużyć kilka różnych typów wyjątków w różny sposób należy dla każdego z nich przygotować oddzielną sekcję except.
W bloku obsługi wyjątków dopuszczone jest pominięcie sekcji except albo finally jednak jedna z nich musi wystąpić (jeżeli obydwie, to w kolejności: najpierw except potem finally). Na końcu może znaleźć się opcjonalna sekcja else wykonywana w sytuacji, gdy nie zaszedł wyjątek.

Przykład bloku obsługi wyjątków może wyglądać następująco:

data_file = open("path/to/file")
try:
  for line in data_file:
    print(line)
except IOError:
  print("Something went wrong...")
finally:
  data_file.close()
else:
  print("No problem :)")

Należy mieć na uwadze, że powyższy przykład jest mocno uproszczony, dla celów zaprezentowania możliwości samej konstrukcji try…. Dotyczy to przede wszystkim obsługi błędu w bloku except. Wypisanie wiadomości na ekran nie jest dobrym sposobem obsłużenia sytuacji wyjątkowej. Jeżeli wszystko co powinniśmy zrobić z danym wyjątkiem to zapisać informacje o jego wystąpieniu, należałoby skorzystać np. z modułu logging i odłożyć bardziej szczegółowe dane w logu programu. W kwestii samego obsługiwania plików preferowanym rozwiązaniem jest skorzystanie z dostępnego context manager’a (zobacz “Za pomocą jakiego słowa kluczowego możemy zastosować context manager’a?”).

Informacje o wyjątkach i ich obsłudze:
https://docs.python.org/3.7/tutorial/errors.html

10. W jaki sposób zadeklarować klasę FullTimeContract, która dziedziczy po klasie Contract?

Poprawna odpowiedź to: class FullTimeContract(Contract):.

Dziedziczenie oznacza, że dany typ (dziedziczący, dziecko) jest bardziej szczegółową wersją typu ogólnego (typ bazowy, rodzic). W przykładzie z kontraktem, FullTimeContract to pewien szczególny rodzaj ogólnego Contract. Pozwala to traktować obiekty różnych typów dziedziczących po tym samym rodzicu, w ten sam sposób i określić, z której klasy ma pochodzić wywoływana metoda w momencie jej wywoływania. Koncepcja ta nazywana jest polimorfizmem.

class Contract:
  def calculate_value(self, salary_per_hour):
    return salary_per_hour

class FullTimeContract(Contract):
  def calculate_value(self, salary_per_hour):
    return 160 * salary_per_hour

class PartTimeContract(Contract):
  def calculate_value(self, salary_per_hour):
    return 80 * salary_per_hour

contracts = [FullTimeContract(), PartTimeContract()
total_amount = 0
for contract in contracts:
  total_amount += contract.calculate_value(salary_per_hour=100)

W implementacji klasy potomnej można odwoływać się do właściwości i metod zdefiniowanych w klasie bazowej za pomocą konstrukcji super(). Klasa potomna może również rozszerzać klasę bazową poprzez dołożenie nowych pól oraz metod, jak również nadpisać implementację metody zdefiniowanej już w klasie bazowej. Klasy są związane z paradygmatem programowania obiektowego, które jest tematem bardzo szerokim, ale jednocześnie ważnym elementem warsztatu programistycznego.

Informacje o klasach w Pythonie:
https://docs.python.org/3/ttorial/classes.html

11. Deklaracja def get_distance() → Optional[int]: oznacza, że funkcja get_distance powinna:

Poprawna odpowiedź to: Zawsze zwrócić wartość typu int albo wartość None.

Zastosowana powyżej notacja to type hints. Umożliwia ona deklarację oczekiwanych typów dla argumentów funkcji, zwracanych wartości, zmiennych. Python jest i pozostaje językiem dynamicznie typowanym, jednak wprowadzenie coraz szerszego wsparcia dla type hints w nowych wersjach języka, umożliwia zastosowanie dodatkowych mechanizmów ułatwiających czytanie już istniejącego kodu i redukcję liczby błędów. Powyższy zapis nie uniemożliwi implementacji funkcji w następujący sposób:

def get_distance() -> Optional[int]:
  return "This is wrong"

Jednak pozwoli łatwiej wykryć tego typu pomyłkę:

  • Środowiska programistyczne (np. PyCharm) wskażą taką implementację jako potencjalny błąd
  • Informacja o oczekiwanych typach pozwala uzyskać lepsze podpowiadanie składni
  • Stosując tzw. type checker (np. narzędzie mypy) błąd zostanie zwrócony w momencie wykonania sprawdzenia kodu (np. przed dołączeniem zmian do wspólnego repozytorium), a nie w momencie jego wykonania w środowisku testowym lub produkcyjnym

Zapis → int oznacza, że dana funkcja zawsze powinna zwrócić wartość typu int. “Opakowanie” w Optional dopuszcza zwrócenie również wartości None.

Szczegółowe informacje dotyczące możliwości type hints można znaleźć w dokumentacji:
https://docs.python.org/3/library/typing.html

Narzędzie mypy:
https://github.com/python/mypy

Prezentacja Carl’a Meyer’a na temat type hints:
https://youtu.be/pMgmKJyWKn8

12. Jaki typ NIE może być kluczem w słowniku?

Poprawna odpowiedź to: list.

Kluczem w słowniku może być każdy obiekt, który jest “hashable”. Oznacza to obiekty, dla których wartość hash-a nie ulega zmianie i mogą być porównywane z innymi na podstawie posiadanej tożsamości, a nie wartości. Zarówno int, str jak i bool są poprawnymi kluczami dla słownika. Lista, podobnie jak inne wbudowane typy mutable nie jest hashable, w związku z czym nie może być pełnić roli klucza w słowniku.

Informacje o słowniku oraz hashable:
https://docs.python.org/3/library/stdtypes.html#typesmapping
https://docs.python.org/3/glossary.html#term-hashable

13. Jak poprawnie użyć pustej listy jako domyślnego argumentu dla funkcji?

Poprawna odpowiedź to:

def calculate(numbers=None):
  if numbers is None:
    numbers = []

Potrzeba zastosowania tej konstrukcji wynika z faktu, iż lista jest typem mutable oraz ze sposobu w jaki Python przetwarza definicję funkcji i domyślne argumenty. Wartości domyślne są przypisywane do argumentów funkcji gdy interpreter po raz pierwszy “czyta” jej definicję, nie zaś za każdym wywołaniem funkcji. Tak więc dzieje się to tylko jeden raz, podczas całego wykonania programu. Podanie pustej listy bezpośrednio jako argument domyślny spowoduje, że dla każdego wywołania będzie to jedna i ta sama lista. Ponieważ jest to typ mutable, wszystkie zmiany wykonane w ramach pierwszego wywołania będą widoczne w drugim. Dobrze obrazuje to poniższy przykład:

def more_numbers(numbers=[]):
  print(numbers)
  numbers.append(1)

>>> more_numbers()
[]
>>> more_numbers()
[1]
>>> more_numbers()
[1, 1]

W każdym kolejnym wywołaniu funkcja korzysta z tej samej listy, która została już wcześniej zmodyfikowana przez poprzednie wykonania. Zastosowanie konstrukcji z wykorzystaniem None jako domyślnej wartości i przypisanie listy do zmiennej dopiero w ciele funkcji jest przyjętym sposobem na rozwiązanie tego problemu. Analogiczna sytuacja dotyczy wykorzystywania wszystkich typów mutable jako domyślnych argumentów funkcji.

Argumenty domyślne – dokumentacja:
https://docs.python.org/3/tutorial/controlflow.html#default-argument-values

Artykuł o wykorzystaniu wartości mutable jako domyślnych argumentów funkcji:
https://nikos7am.com/posts/mutable-default-arguments/

14. Które z wywołań funkcji def download(url, timeout=5): NIE jest poprawne?

Poprawna odpowiedź to: download(url="www.infoshareacademy.com", 10)

Takie wywołanie spowoduje błąd (SyntaxError) wynikający z przekazania argumentu pozycyjnego po argumencie nazwanym.

Argumenty przekazywane do funkcji lub metody mogą być argumentami pozycyjnymi (bez nazwy argumentu i znaku =) albo argumentami nazwanymi (keyword). Dla funkcji zdefiniowanej tak jak w przykładzie możliwe jest:

  • Przekazanie obydwu argumentów jako argumenty pozycyjne download("www.infoshareacademy.com", 10)
  • Przekazanie tylko pierwszego argumentu jako argument pozycyjny (timeout przyjmie wartość domyślną)download("www.infoshareacademy.com")
  • Przekazanie tylko pierwszego argumentu jako keyword argument (timeout przyjmie wartość domyślną) download(url="www.infoshareacademy.com")
  • Przekazanie obu argumentów jako keyword arguments download(url="www.infoshareacademy.com", timeout=10) albo download(timeout=10, url="www.infoshareacademy.com")

Kolejność keyword arguments nie jest ważna (interpreter wie, jakiemu argumentowi przypisać jaką wartość). Kolejność argumentów pozycyjnych jest ważna, gdyż odpowiada ich przypisaniu do poszczególnych zmiennych. Nie jest dozwolone przekazanie argumentu pozycyjnego po keyword arguments, jak również nie jest dozwolone pominięcie argumentu, który nie posiada wartości domyślnej.

Wprowadzona w PEP 3102 koncepcja Keyword-Only Arguments pozwala wymusić podanie niektórych (albo wszystkich) argumentów w postaci keyword arguments. Ma to na celu zwiększenie czytelności, w sytuacjach gdy wartość przekazywanego argumentu nie jest oczywista, w kontekście wywoływanej funkcji. Aby zastosować tę konwencję, należy przy deklarowaniu argumentów funkcji użyć gwiazdki. Spowoduje to, że wszystkie następne argumenty będą musiały być przekazane jako keywords arguments:

def something(could_be_positional, *, only_keyword):
  pass

Informacje o argumentach funkcji:
https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions

PEP 3102:
https://www.python.org/dev/peps/pep-3102/

15. Jak zwrócić listę zawierającą elementy o indeksach 1, 2 i 3 z listy grades = [5, 3, 2, 5, 6]?

Poprawna odpowiedź to: grades[1:4]

Odwołanie się do listy (lub innej sekwencji) za pomocą indeksu oraz dwukropka umożliwia realizację operacji slice. Operacja ta pozwala otrzymać:

  • listę elementów od początku do podanego indeksu [:index] (bez elementu o indeksie index)
  • listę elementów od podanego indeksu do końca [index:] (z elementem o indeksie index)
  • listę elementów pomiędzy dwoma indeksami [start:end] (z elementem o indeksie start, bez elementu o indeksie end)
  • listę elementów pomiędzy dwoma indeksami, zawierającą co n-ty element [start:end:n] (z elementem o indeksie start, następnie element o indeksie start + n itd., bez elementu o indeksie end)

Operacje slice są wygodnym sposobem na uzyskanie wybranego podzbioru elementów z dłuższej sekwencji. Przy stosowaniu slice’a warto pamiętać:

  • pierwszy element list ma indeks 0
  • ostatni element listy przypisanej do zmiennej numbers ma indeks -1 albo len(numbers) - 1
  • stosowanie skomplikowanych slice-ów (np. wykorzystujących ujemne indeksy oraz wybierających co n-ty element) mocno obniża czytelność kodu. W takiej sytuacji warto planowaną operację przeprowadzić np. w kilku krokach

Listy i operacje na listach:
https://docs.python.org/3/tutorial/introduction.html#lists
https://docs.python.org/3/library/stdtypes.html#common-sequence-operations

Myślisz o rozpoczęciu nauki?
Sprawdź, kurs Python od podstaw w Gdańsku

Autor:

wielki test wiedzy o Pythonia

Mikołaj Lewandowski
Senior Python Developer

Programista, team leader i trener programowania. Entuzjasta Software Craftsmanship i pragmatycznego podejścia do rozwijania systemów. Wierzy w ideę Civic Tech i to, że technologia może zmienić świat. Biegacz, rowerzysta, czasami podróżnik.