1. Konvence PEP8, paramtery funkcí, balíčky

PEP8

PEP8 je zkratka pro "Python Enhancement Proposal 8". Jedná se o oficiální doporučený styl psaní kódu v jazyce Python. PEP8 poskytuje pravidla a konvence pro formátování kódu, které usnadňují čitelnost a srozumitelnost kódu pro vývojáře.

Mezi pravidla PEP8 patří například doporučení ohledně odsazování kódu, používání mezer, délky řádků, pojmenovávání proměnných a funkcí atd. Dodržování PEP8 přispívá ke konzistenci a profesionalitě vývoje v Pythonu a usnadňuje spolupráci mezi vývojáři.

# PEP8 - mezery okolo operátorů
# správně
7 + 4
# špatně
7+4
# PEP8 - mezery okolo závorek
# správě
(7 + 4)
# špatně
( 7 + 4 )
# PEP8 - mezery a slicing
# správně
"ahoj svete"[0:2]
# špatně
"ahoj svete"[0 : 2]
# PEP8 - mezery
# správně
x = 10
y = 20
# špatně
x=10
y    = 10

# PEP8 - pojmenování proměnných
# správně (pojmenování souřadnic bodu)
x = 10
y = 20
# špatně
a = 10
g = 20

# PEP8 - pojmenování víceslovných proměnných
# správně
odd_number
# špatně
oddNumber

# PEP8 - pojmenování konstant
CENTER_X, CENTER_Y = 0, 0
user_integer = int(input("Please enter an integer: "))

# PEP8 - pořadí podmínek (dle četnosti výskytu)
# správně
if user_integer > 0:
    print(user_integer, "is larger than 0")
elif user_integer < 0:
    print(user_integer, "is smaller than 0")
else:
    print(user_integer, "is 0")

# špatně
if user_integer == 0:
    print(user_integer, "is 0")
elif user_integer < 0:
    print(user_integer, "is smaller than 0")
else:
    print(user_integer, "is larger than 0")
# PEP8 - Neporovnávat boolean hodnoty s True nebo False pomocí ==
boolean_value = False

# správně
if boolean_value:
    print("It is true!")

# špatně
if boolean_value == True:
    print("It is true!")
# PEP8 - Neporovnávat číselné hodnoty pokud testujeme na nulu
number_value = 42

# správně
if number_value:
    print("Not equal to zero!")

# špatně
if number_value != 0:
    print("Not equal to zero!")
# PEP8 - mezery# správně
point["z"] = 40
# špatně
point ["z"] = 40
# PEP8 - v seznamech se na poslední pozici čárka nepíše
# správně
numbers = [10, 20, 5, 10]
# špatně
numbers = [10, 20, 5, 10,]
# PEP8 - pojmenování nepoužité hodnoty
for x, _ in ((10, 20), (5, 2), (20, 30)):
    print(x)
# PEP8 - využívejte faktu, že prázdné sekvence jsou vyhodnoceny jako False 
# správně
if not numbers:
    pass 

if numbers:
    pass 

# špatně
if not len(numbers):
    pass 

if len(numbers):
    pass 

# PEP8 - pořadí psaní podmínek
numbers = [1, 20, 5, 1]

# správně
if 10 not in numbers:
    print("There is no 10!")

# špatně
if not 10 in numbers:
    print("There is no 10!")
# iterování přes seznam - není součástí PEP8, ale je dobré to používat
numbers = [1, 2, 3, 4]

# správně
for number in numbers:
    print(number)

# špatně
for i in range(len(numbers)):
    print(numbers[i])

Automatické formátování kódu

Autopep8 je nástroj pro automatickou úpravu kódu v jazyce Python tak, aby odpovídal konvencím a pravidlům definovaným v PEP8. Tento nástroj analyzuje zdrojový kód a automaticky provede některé potřebné změny (například Vám ale nezmění pojmenování proměnných). Pomocí rozšíření jej lze plně integrovat do Visual Studio Code.

Black je další nástroj pro automatickou úpravu kódu, který ale vynucuje jiný standard než PEP8. Hlavní rysy Blacku zahrnují:

  1. Automatické formátování: Black automaticky upravuje kód podle svých interních pravidel, což eliminuje potřebu ručního formátování.
  2. Jednotný styl: Black má pevně stanovené konvence formátování, což zajišťuje konzistentní vzhled kódu napříč projektem.
  3. Bezkonfigurační přístup: Na rozdíl od jiných nástrojů umožňuje Black minimalizovat konfiguraci, což znamená, že vývojář nemusí strávit čas nastavováním pravidel formátování.
  4. Naprostá jistota: Black je navržen tak, aby poskytoval "one true way" formátování, což znamená, že vždy dává stejný výstup pro stejný vstup bez ohledu na kontext nebo konfiguraci.

Pokročilá práce s parametry funkcí

Více informací zde.

Předpis pro definování funkce.

# definice funkce
def <function_name>([<parameters>]):
    <statement(s)>
# PEP8 - názvy funkcí podobně jako proměnné, rovněž dbáme na vhodné pojmenování parametrů
# správně
def multiply_point(point, by):
    x, y = point
    return x * by, y * by


# špatně
def multiplyPoint(a, b):
    x, y = a
    return x * b, y * b

Předávání argumentů (vstup funkce).

def subtract(a, b):
    # vrácení hodnoty z funkce
    return a - bsubtract(a=2, b=3)

    
subtract(b=3, a=2)

subtract(3)
subtract(3, b=2)

# chyba, nejprve musí být povinné parametry
subtract(b=2, 3)
# již správně
subtract(3, b=2)

Výchozí parametr.

def subtract(a, b=1):
    return a - b


subtract(3)
subtract(3, 2)
subtract(3, b=2)
Pozor na mutovatelné výchozí parametry.
def my_list(list_=[]):
    list_.append("123")
    return list_


# problém
my_list([1, 2, 3])
my_list([1, 2, 3])
my_list()
my_list()  # výsledek ['123', '123']

# jak poznáme, že se jedná o stejný objekt?
my_list() is my_list()


# řešení problému
def my_list(list_=None):
    if list_ is None:
        list_ = []
    list_.append("123")
    return list_


# vše se chová jak očekáváme
my_list([1, 2, 3])
my_list([1, 2, 3])
my_list()
my_list()  # výsledek ['123']

my_list() is my_list()

Předání argumentu je v Pythonu řešeno hybridním způsobem mezi "předání hodnotou" a "předání odkazem". Funkci je předán odkaz na objekt, odkaz je ale předáván hodnotou.

name = "Lukáš Novák"

def f(name):
    name = "Petr Novák"

f(name)

print(name)  # vytiskne se Lukáš Novák

To však neznamená, že se obsah argumentu nikdy nemůže měnit v lokální funkci, stačí použít mutovatelné struktury.

names = ["Lukáš Novák", "Karel Novák"]

def change_name(names):
    names[0] = "Petr Novák"

change_name(names)

print(names) # vytiskne ['Petr Novák', 'Karel Novák']

Příkaz return podrobněji

Příkaz return okamžitě ukončí veškeré vykonávání funkce (včetně cyklů) a vrátí uvedenou hodnotu.

def member(target, iterable):
    for idx, item in enumerate(iterable):
        if item == target:
            return idx

    return False


member(4, [1, 2, 3, 4, 5, 6, 7])
member(10, [1, 2, 3, 4, 5, 6, 7])

Pokud return není uveden, vrací se automaticky hodnota None.

def nothing():
    pass

nothing() is None

def nothing():
    return

nothing() is None

Příkaz return můžeme v kombinaci se sekvencí tuple použít na vrácení několika hodnot.

def multiply_point(point, by):
    x, y = point
    # konstruktor tuple
    return x * by, y * by


multiply_point((10, 5), 2)

# tuple unpacking
new_x, new_y = multiply_point((10, 5), 2)

Tuple unpacking je užitečný pro předání seznamu/tuple argumentů.

def multiply_point(x, y, by):
    return x * by, y * by


point = (10, 5)

# chyba
multiply_point(point, 2)

# správně, dojde k unpackingu pomocí operátoru *
multiply_point(*point, 2)

# příklad z praxe, transformace matice
zip(*[[1, 2, 3], [4, 5, 6]])

# slovník lze použít pro pojmenované argumenty
point = {"x": 10, "y": 5}

# unpacking pomoci operatoru **
multiply_point(**point, by=2)

Volitelný počet argumentů

Volitelný počet argumentu pomocí tuple.

def average(*args):
    return sum(args) / len(args)


average(1, 2, 3, 4)

Volitelný počet argumentů pomocí slovníku.

def print_name(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")


print_name(title="Ing.", firstname="Lukáš", lastname="Novák")
print_name("Novák", title="Ing.", firstname="Lukáš")

def print_name(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

person = {"title": "Ing.", "firname": "Lukáš", "lastname": "Novák", "job": "programmer"}

print_name(**person)

Kombinace předchozích možností.

def func(a, b, *args, **kwargs):
    print(a)
    print(b)
    print(args)
    print(kwargs)

func(1, 2, "ahoj", "svete", x=11, y=22)
# vytiskne postupně:
# 1
# 2
# ('ahoj', 'svete')
# {'x': 11, 'y': 22}

Docstrings

Každá definice funkce může (a měla by) obsahovat takzvaný docstring. Jedná se o řetězec s krátkou dokumentací funkce v anglickém jazyce.

def subtract(a, b):
    """Subtract b from a."""
    return a - b # V případě jednořádkového docstringu zde nevkládáme prázdný řádek.

def subtract(a, b):
    """Subtract b from a.

    Args:
        a: Number to subtract from.
        b: Number being subtracted.

    Returns:
        Result of subtraction.
    """

    return a - b # V případě víceřádkového docstringu zde vkládáme prázdný řádek.

subtract.__doc__
help(subtract)

Příkaz assert

Příkaz assert je vhodným nástrojem defenzivního programování, umožňuje nám za běhu programu zaručit, že jsou uvedené výrazy platné.

a = 10
assert a == 10 

def subtract(a, b):
    """Subtract b from a."""
    return a - b

assert subtract(10, 5) == 5

Je to jednoduchý nástroj jak ověřovat základní funkčnost funkcí (k pokročilejšímu testování později). Je však nutné zmínit, že assert do finálního kódu nepatří.

Moduly a balíčky

Doteď jsme náš kód organizovali do jednotlivých skriptů (samostatně fungující soubor). S příchodem funkcí je vhodné dělit definice funkcí na různé soubory a z těchto souborů potom uživatelsky definované funkce importovat do dalších skriptů. Tímto je možné sdílet funkcionalitu mezi více projektů a vytvářet knihovny.

Modul je tedy soubor obsahující Python definice a příkazy. Název modulu je název souboru s příponou .py. V rámci modulu můžeme název modulu získat pomoci speciální proměné __name__.

Ukázka importování vestavěného modulu math pomoci příkazu import.

import math


math.sqrt(6)

math.__name__

Vlastní modul pak můžeme vytvořit jednoduše. Vytvořme soubor list_operations.py s následujícím obsahem:

def subtract_lists(list1, list2):
    """Subtract two list piecewise."""
    result = []

    for a, b in zip(list1, list2):
        result.append(a - b)
    
    return result


def sum_lists(list1, list2):
    """Sum two list piecewise."""
    result = []

    for a, b in zip(list1, list2):
        result.append(a + b)
    
    return result

V jiném skriptu umístěném v téže složce (nebo v interpretu spuštěném z téže složky - vhodné pro rychlé testování) můžeme modul list_operations importovat.

import list_operations


list_operations.subtract_lists([1, 2], [4, 3])

Modul nemusí obsahovat pouze definice ale i příkazy, tyto příkazy jsou zamýšleny jako inicializace modulu a jsou provedeny pouze jednou při načtení modulu.

Každý modul má pak odpovídající jmenný rozsah, proto můžeme používat dvě funkce z rozdílných modulů se stejným názvem.

import math
import my_math


math.sqrt
my_math.sqrt
# PEP8 - jednotlivé importy musí být na samostatném řádku
# správně
import os
import sys

# špatně
import sys, os

# PEP8 - pořadí importů
import sys # systémová knihovna

import numpy # externí knihovna

import my_math # lokální modul

Existuje varianta import příkazu, který přímo importuje jména z jmenného prostoru modulu do aktuálního jmenného prostoru.

from list_operations import subtract_lists, sum_lists


subtract_lists([1, 2], [4, 3])
# PEP8 - jednotlivé importy musí být na samostatném řádku
# toto je výjimka z pravidla
from list_operations import subtract_lists, sum_lists

Variantu, která importuje kompletní jmenný prostor modulu do toho aktuálního, je lepší nepoužívat.

from list_operations import *

Přejmenování modulu při importu je však užitečné.

import list_operations as loperations


loperations.subtract_lists([1, 2], [4, 3])

Rovněž je možné přejmenovat importované funkce.

from list_operations import subtract_lists as lsubtract

Pokud do modulu umístíme následující podmínku, obsah bude vykonán pouze při přímem spuštění zdrojového kódu, nikoli při jeho importování.

if __name__ == "__main__":
    print("Only when script is launched.")

Odkud se moduly importují?

Při importu modulu my_math hledá interpret nejdříve modulu vestavěné. Pokud nebyl žádný vestavěný modul nalezen, hledá soubor s názvem my_math.py v seznamu adresářích uložném v proměnné sys.path. Seznam obsahuje adresář odkud byl skript spuštěn, PYTHONPATH s cestami k instalovaným modulů a další.

Jak a kde používat odřádkování

Při psaní kódu je žádoucí kód strukturovat pro jeho lepší čitelnost. Bloky kódu oddělujeme na vhodných místech jedním nebo vícero prázdnými řádky.

import sys # systémová knihovna, prázdný řádek

import numpy
import pandas # externí knihovna, prázdný řádek

import my_math
from . import calculator # lokální modul, prázdný řádek


# prazdný řádek nad prvním řádkem pod importy
def subtract_lists(list1, list2):
    """Subtract two list piecewise.""" # pod docstring neumisťujeme žádný řádek
    result = [] # zde budeme agregovat vysledek, prazdny řádek

    for a, b in zip(list1, list2):
        result.append(a - b) # ukončení bloku s cyklem, prázdný řádek
    
    return result


# dva prazdné řádky mezi funkcemi
def sum_lists(list1, list2):
    """Sum two list piecewise."""
    result = []

    for a, b in zip(list1, list2):
        result.append(a + b)
    
    return result

Balíčky

Balíčky jsou způsob jak strukturovat jmenný prostor Python modulu. Jmenný prostor je potom přístupný skrze tečkovou notaci.

# z modulu A importujeme submodul B
import A.B

Příklad struktury balíčku:

# z modulu A importujeme submodul B
sound/                          # Top-level package
      __init__.py               # Initialize the sound package
      formats/                  # Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  # Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  # Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

Balíček sound má podřazené balíčky formats, effects a další.

Jakmile balíček importujeme, Python automaticky dohledá veškeré podsložky balíčků.

Speciální soubor __init__.py je potřebný k tomu, aby Python považoval složku za balíček. Pro základní funkcionalitu stačí, aby se jednalo o prázdný soubor, může zde však být provedena inicializace balíčku.

Z takové struktury může uživatel importovat následujícím způsobem:

import sound.effects.echo


# aby mohl být modul použít, musí být napsáno jeho plné jméno
sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

Alternativně však můžeme použít následující:

from sound.effects import echo


echo.echofilter(input, output, delay=0.7, atten=4)

Posledním způsobem je potom importování samostatné funkce echofilter.

from sound.effects.echo import echofilter


echofilter(input, output, delay=0.7, atten=4)

Reference v rámci balíčku

Často je potřeba provádět relativní import v rámci balíčku. Relativní import je možné provést následovně:

# v rámci složky
from . import echo
# nadřazená složka
from .. import formats
from ..filters import equalizer

Pokud jste například v souboru echo.py pak můžete importovat z karaoke.py následovně:

from ..filters.karaoke import funkce1, funkce2, ...

Instalace externích balíčku

V budoucnu rozebereme detailněji, aktuálně pro nás bude relevantní, že balíčky v drtivé většině instalujeme z Python Package Index (PyPI) pomocí nástroje pip.

pip je to standardní správce balíčků pro jazyk Python. Pomocí nástroje pip můžete snadno instalovat, aktualizovat a odinstalovat balíčky Pythonu a jejich závislosti z Python Package Index (PyPI) a dalších repozitářů balíčků. PyPI je rozsáhlý repozitář, který obsahuje tisíce balíčků Pythonu, které lze použít pro různé účely, od webového vývoje po vědecké výpočty.

Detailnější popis nalezneme v dokumentaci.

Budeme používat externí knihovnou pytest, kterou je tedy možné instalovat příkazy:

pip install pytest
pip install pytest-console-scripts

Posléze ve složce s repozitářem úkolu L03E01 stačí spustit příkaz (kde soubor tests.py obsahuje testy):

pytest tests.py

A v případě úkolu obsahující balíček (například L01E05) je nutné aby složka tests obsahovala soubor __init__.py nebo musíte balíček nejprve lokálně nainstalovat a poté testy spustit:

pip install -e .
pytest

Zatím není nutné předchozím krokům rozumět, o publikování a instalovaní balíčků si více řekneme na konci tohoto kurzu.

Dobrovolné úkoly

Nevíte si rady? Přečtěte si "Jak pracovat s Github Classroom?".

Při řešení úloh dodržujte konvence PEP8 (nebo Black) a pište dokumentaci ve formě docstringů u všech funkcí, které se v domácích úkolech vyskytnou.