… v Pythonu: to není lenost, ale produkční kód
Co se dozvíte v článku
- … v Pythonu: to není lenost, ale produkční kód
- Objektový systém jazyka Python a literál …
- Je tedy … pouze jmenným aliasem pro Ellipsis?
- Způsoby využití … v Pythonu
- Definice funkce s dočasným tělem
- Využití … jako výchozí hodnoty volitelného parametru funkce
- Specifikace datového typu n-tice s neznámým počtem prvků
- Specifikace typu a dalších vlastností atributu bez uvedení výchozí hodnoty (Pydantic)
- Příklad složitějšího modelu, ve kterém jsou definovány atributy bez výchozí hodnoty
- Typová informace: funkce akceptující neznámý počet a typ parametrů
- Využití Callable[…, ]
- Praktické příklady využití typu Callable[…, ]
- Indexování využívané v knihovně Numpy (n-rozměrná pole)
- Další příklady využití … při indexování n-rozměrných polí
- Využití … při komunikaci mezi procesy nebo vlákny
- Příklad využití … v komunikaci mezi vlákny s využitím sdílené fronty
- Upozornění: … není to samé jako _
- Trojice teček v dalších programovacích jazycích
- Repositář s demonstračními příklady
- Odkazy na Internetu
V případě, že na prezentaci či v nějakém repositáři naleznete následující definice funkcí (zapsané v různých programovacích jazycích), pravděpodobně budete předpokládat, že tyto funkce nejsou dokončeny a že zdrojové kódy s těmito funkcemi nebudou bez dalšího rozšiřování přeložitelné nebo spustitelné:
void foo() {
...
}
fn foo() {
...
}
func foo() {
...
}
fn void foo()
{
...;
}
def foo():
...
V prvních čtyřech případech (což jsou konkrétně příklady napsané v C, Rustu, Go a C3) budete mít naprostou pravdu – takto zapsané funkce nejenom že neobsahují tělo, ale navíc jsou v nich zapsány „trojtečky“ (ellipsis), které nejsou korektním příkazem ani výrazem.
Ovšem v případě posledním je tomu jinak. Poslední funkce je totiž zapsána v Pythonu a tento jazyk má trojtečky … definovány jako literál. Pochopitelně jsou stanovena i syntaktická a sémantická pravidla použití tohoto literálu. A právě s těmito pravidly a způsoby použití se setkáme v dnešním článku. Znaky … se totiž v Pythonu mohou použít na více místech, mnohdy tam, kde bychom je vlastně ani nečekali. A navíc můžeme pomocí … vyřešit některé zapeklitější sémantické problémy, například schopnost zjistit, zda byl do funkce předán parametr.
Objektový systém jazyka Python a literál …
Při spuštění interpretru programovacího jazyka Python je (kromě dalších hodnot) inicializována i hodnota uložená do proměnné nazvané Ellipsis. Tato hodnota je jedináčkem (singleton) a je typu ellipsis (pozor na rozdíl velké/malé písmeno na začátku).
Přímo v REPLu máme pochopitelně přístup k nápovědě o Ellipsis i ellipsis:
help(Ellipsis)
Po zadání tohoto příkazu by se měla zobrazit nápověda ke třídě ellipsis:
class ellipsis(object) | The type of the Ellipsis singleton. | | Methods defined here: | | __reduce__(self, /) | Helper for pickle. | | __repr__(self, /) | Return repr(self). | | ---------------------------------------------------------------------- | Static methods defined here: | | __new__(*args, **kwargs) | Create and return a new object. See help(type) for accurate signature.
O existenci proměnné Ellipsis (a hodnoty do ní přiřazené) se taktéž můžeme velmi snadno přesvědčit:
print(Ellipsis)
Mělo by se vypsat:
Ellipsis
Důležité je, že literál … je při inicializaci interpretru nastaven na stejnou hodnotu, jaká je uložena v proměnné Ellipsis:
print(Ellipsis) print(...)
V tomto případě se vypíšou dva naprosto stejné řádky:
Ellipsis Ellipsis
To ovšem ještě vůbec nic neříká o tom, že jsou obě hodnoty stejného typu. Ověření typů tedy musí proběhnout odlišně:
print(type(Ellipsis)) print(type(...))
<class 'ellipsis'> <class 'ellipsis'>
Navíc se snadno přesvědčíme o tom, že se jedná nejenom o stejné hodnoty, ale i o shodný objekt ležící ve stejném místě operační paměti. K tomuto účelu využijeme standardní funkci id:
print(id(Ellipsis)) print(id(...)) print(id(Ellipsis) == id(...))
9918048 9918048 True
Je tedy … pouze jmenným aliasem pro Ellipsis?
Z demonstračních příkladů uvedených v předchozích kapitolách by se mohlo zdát, že trojice teček … je pouze jmenným aliasem pro proměnnou Ellipsis. Ovšem to není úplně přesné, protože s Ellipsis je možné skutečně pracovat jako s běžnou proměnnou, což mj. znamená, že je do ní možné přiřadit novou hodnotu. Ostatně si to můžeme velmi snadno ukázat. Následující příklad je zcela korektní:
print(Ellipsis) print(Ellipsis == ...) print() Ellipsis=42 print(Ellipsis) print(Ellipsis == ...)
Po spuštění výše uvedeného skriptu se nejdříve vypíše původní hodnota přiřazená do proměnné Ellipsis i to, zda je tato hodnota totožná s hodnotou reprezentovanou literálem …:
Ellipsis True
Ovšem další dva řádky vypsané skriptem prozradí, že nám prakticky nic nebrání v tom, abychom hodnotu uloženou do proměnné Ellipsis přepsali jinou hodnotou (interpret nás na chybu neupozorní, protože se jedná o zcela korektní operaci):
42 False
Ovšem literál … se chová jinak než běžná proměnná. To především znamená, že ho není možné použít na levé straně přiřazovacího příkazu, což si opět snadno ověříme:
print(Ellipsis) print(Ellipsis == ...) print() ...=42 print(Ellipsis) print(Ellipsis == ...)
Interpret programovacího jazyka Python v tomto případě vypíše chybu (dokonce chybu na úrovni syntaxe) již při pokusu o načtení zdrojového kódu:
File "/home/ptisnovs/src/most-popular-python-libs/ellipsis/ellipsis_assignment_2.py", line 5
...=42
^^^
SyntaxError: cannot assign to ellipsis here. Maybe you meant '==' instead of '='?
Způsoby využití … v Pythonu
V programovacím jazyku Python se zápis tří teček používá v několika oblastech. Přitom ovšem platí, že každé použití … má poněkud odlišný sémantický význam, který bude popsán (včetně demonstračních příkladů) v navazujících kapitolách:
- Trojtečku je možné použít v těle funkce jako jediný výraz (ne příkaz). Takové funkce lze zavolat, ovšem z pohledu sémantiky se jedná o funkce, které se ještě budou měnit.
- Trojtečka se taktéž může využít jako výchozí hodnota volitelného parametru funkce. Poté lze v čase běhu detekovat, jestli byl do funkce parametr předán či nikoli (což je možná lepší, než definovat None jako výchozí hodnotu).
- Ve FastAPI či při použití knihovny Pydantic se trojtečka používá v definicích modelů resp. atributů modelů.
- Trojtečku lze taktéž využít při definicích typů, konkrétně při definici typu funkce s libovolným počtem či typy parametrů.
- V knihovně Numpy se trojtečka zapisuje resp. může zapisovat při indexaci prvků n-rozměrných polí.
- Trojtečku je taktéž možné použít při komunikaci mezi vlákny nebo procesy, přičemž trojtečka sémanticky reprezentuje žádost o ukončení vlákna/procesu či naopak informaci o tom, že se vlákno/proces po odeslání trojtečky ukončí.
Definice funkce s dočasným tělem
Připomeňme si, že programovací jazyk Python je poměrně unikátní v tom, že se v něm nepoužívají klasické příkazové bloky známé z jiných programovacích jazyků. Namísto toho blok začíná dvojtečkou a kód bloku musí být vhodným způsobem odsazen (a typicky ukončen prázdným řádkem). Jak se ovšem v takovém případě zapíše „prázdný blok“? Většinou se setkáme s použitím (rezervovaného) klíčového slova pass, které bylo do jazyka přidáno právě z tohoto důvodu:
def foo():
pass
Ovšem naprosto stejným postupem můžeme do těla funkce zapsat jakoukoli hodnotu (libovolného typu), například:
def foo():
42
print(foo())
Po spuštění je ověřeno, že 42 má význam pouze ze syntaktického hlediska. Tato hodnota je vyhodnocena, ale není vrácena (chybí příkaz return):
None
Klíčové slovo pass se používá tehdy, pokud funkci již nechceme dále modifikovat – je to její finální podoba (některé funkce nebo metody skutečně pouze musí existovat, ale mohou mít prázdné tělo). Pokud ovšem budete chtít naznačit, že se funkce ještě v budoucnu rozšíří, je lepší použít tento způsob zápisu:
def foo(): ...
Samozřejmě si můžeme uvést i nepatrně složitější příklad, ve kterém se kombinuje hned několik (mírně pokročilejších) vlastností Pythonu:
from typing import Union, overload
@overload
def add(a: int, b: int) -> int:
...
@overload
def add(a: str, b: str) -> str:
...
def add(a: Union[int, str], b: Union[int, str]) -> Union[int, str]:
"""Funkce s typovými anotacemi."""
return a + b
# zavolání funkce add s argumenty různých typů
print(add(1, 2))
print(add("foo", "bar"))
Využití … jako výchozí hodnoty volitelného parametru funkce
Poměrně často se v praxi můžeme setkat s funkcemi s volitelnými parametry. V případě, že tento parametr nebo tyto parametry nejsou předány při volání funkce, zapíše se do nich nějaká předem zvolená výchozí hodnota. U plně volitelných parametrů se mnohdy jako výchozí hodnota používá None, což má ovšem jednu nevýhodu – tímto způsobem totiž nelze odlišit explicitní předání None (to je totiž mnohdy zcela validní hodnota) od neuvedení parametru:
def bar(arg = None):
if arg is None:
print("No value provided!")
else:
print("Value provided", arg)
bar(42)
bar(None)
bar()
Povšimněte si, že volání funkce s předáním None skutečně nebylo odlišeno od volání bez předání parametru:
Value provided 42 No value provided! No value provided!
Tento problém lze do jisté míry vyřešit tak, že výchozí hodnotou parametru budou právě tři tečky:
def bar(arg = ...):
if arg is ...:
print("No value provided!")
else:
print("Value provided", arg)
bar(42)
bar(None)
bar()
Nyní lze snadno rozlišit volání s předáním None od volání bez uvedení parametru:
Value provided 42 Value provided None No value provided!
def bar(arg = ...):
if arg is ...:
print("No value provided!")
else:
print("Value provided", arg)
bar(42)
bar(Ellipsis)
bar(...)
bar()
Nyní se od sebe neodliší poslední tři volání:
Value provided 42 No value provided! No value provided! No value provided!
Specifikace datového typu n-tice s neznámým počtem prvků
Se zápisem tří teček se setkáme i u specifikace datového typu n-tice s neznámým počtem prvků. U n-tic je totiž nutné uvádět typ každého z prvků zvlášť (na rozdíl od seznamů). Ovšem pokud mají mít prvky stejný typ a současně neznáme jejich přesný počet, může to být problém. Právě ten je řešený následujícím způsobem:
x: tuple[str, ...] = ("foo", "bar", "baz")
print(x)
x = ("x", "y", "z")
print(x)
Pochopitelně nám nic nebrání v deklaraci (de facto) konstanty:
from typing import Final
x: Final[tuple[str, ...]] = ("foo", "bar", "baz")
print(x)
V tomto článku se setkáme i s několika příklady z praxe. V případě n-tic s uvedením plné typové informace (type hints):
# Routes excluded from Sentry trace sampling (health checks, metrics, root).
# Note: health and metrics routers are mounted WITHOUT a /v1 prefix
# (see the setup_routers function in src/app/routers.py), so ASGI paths are
# /readiness, /liveness, /metrics.
SENTRY_EXCLUDED_ROUTES: Final[tuple[str, ...]] = (
"/readiness",
"/liveness",
"/metrics",
"/",
)
Popř. definice typu n-tice s proměnným počtem prvků typu FieldSpec nebo ListFieldSpec:
LIGHTSPEED_STACK_FIELDS: tuple[FieldSpec | ListFieldSpec, ...] = (
# Operational
FieldSpec("name", MaskingType.PASSTHROUGH),
# Core Service Configuration
FieldSpec("service.workers", MaskingType.PASSTHROUGH),
Specifikace typu a dalších vlastností atributu bez uvedení výchozí hodnoty (Pydantic)
S použitím tří teček se poměrně často můžeme setkat i v případě, že se v aplikaci využívají modely, což jsou například v knihovně Pydantic třídy odvozené od BaseModel. S knihovnou Pydantic i se základním způsobem jejího využití jsme se již na stránkách Roota setkali, takže si jen krátce ukažme právě použití tří teček. V mnoha datových třídách totiž potřebujeme popsat vlastnosti některého atributu (řekněme jména uživatele). To se provádí inicializací atributu s přiřazením hodnoty typu Field:
class User(BaseModel):
"""Model reprezentující uživatele."""
name: str = Field("", max_length=10)
surname: str = Field("", max_length=10)
age: PositiveInt | None
registered: bool = False
kde prázdné uvozovky značí výchozí hodnotu jména, příjmení atd. Mnohdy ovšem naopak nechceme specifikovat žádnou výchozí hodnotu – tím pádem vlastně „donutíme“ vývojáře, kteří třídu používají, aby ji při konstrukci třídy zapsali. To lze provést následovně:
class User(BaseModel):
"""Model reprezentující uživatele."""
name: str = Field(max_length=10)
surname: str = Field(max_length=10)
age: PositiveInt | None
registered: bool = False
Ovšem mnohdy je výhodnější, a to i z dokumentačních důvodů, explicitně naznačit, že je nutné atributy při konstrukci třídy nastavit. A právě tehdy se používají tři tečky:
class User(BaseModel):
"""Model reprezentující uživatele."""
name: str = Field(..., max_length=10)
surname: str = Field(..., max_length=10)
age: PositiveInt | None
registered: bool = False
Využití datového modelu může vypadat takto:
from pydantic import BaseModel, Field, PositiveInt, field_validator
class User(BaseModel):
"""Model reprezentující uživatele."""
name: str = Field(..., max_length=10)
surname: str = Field(..., max_length=10)
age: PositiveInt | None
registered: bool = False
@field_validator("age")
@classmethod
def check_age(cls, value):
"""Kontrola, jestli je uživatel dospělý."""
if value < 18:
raise ValueError("You are too young to register")
return value
user1 = User(name="Nabuchodonozor", surname="II", age=18)
print(user1)
Příklad složitějšího modelu, ve kterém jsou definovány atributy bez výchozí hodnoty
Pro zajímavost se podíváme ještě na jeden Pydantic model s atributy, které sice mají definován typ a další vlastnosti (maximální délka jména atd.), ovšem nikoli výchozí hodnotu. Takové atributy je nutné při konstrukci modelu explicitně naplnit:
from pydantic import BaseModel, Field, PositiveInt, field_validator
class Address(BaseModel):
street: str
house_number: PositiveInt | str
city: str
class User(BaseModel):
name: str = Field(..., max_length=10)
surname: str = Field(..., max_length=10)
age: PositiveInt | None
registered: bool = False
@field_validator("age")
@classmethod
def check_age(cls, value):
if value < 18:
raise ValueError("You are too young to register")
return value
class Character(BaseModel):
role: str
user: User
address: Address
character = Character(
role="Detective",
user=User(name="Sherlock", surname="Holmes", age=42),
address=Address(street="Baker", house_number="221B", city="London"),
)
as_json = character.model_dump_json(indent=4)
print(as_json)
Typová informace: funkce akceptující neznámý počet a typ parametrů
Další oblastí, ve které se můžeme se třemi tečkami ve zdrojových kódech Pythonu setkat, je definice typu „funkce“, konkrétně funkce, která akceptuje neznámý počet a typ parametrů. Nejprve si však pro připomenutí vysvětlíme, jak vypadá definice typu „funkce s konkrétními parametry“, například funkce akceptující dvě celá čísla a vracející (jiné) celé číslo:
Callable[[int, int], int]
Ukažme si příklad použití takového datového typu pro specifikaci typu parametru jiné funkce nazvané calc, která provede vybranou aritmetickou binární operaci (parametr operator) s hodnotami x a y:
def calc(operator: Callable[[int, int], int], x: int, y: int) -> int:
return operator(x, y)
Funkci calc můžeme při jejím volání v prvním parametru předat například tyto funkce, které přesně odpovídají Callable[[int, int], int]:
def add(x: int, y: int) -> int:
return x + y
def mul(x: int, y: int) -> int:
return x * y
def less_than(x: int, y: int) -> bool:
return x < y
Následuje ucelený příklad, který tyto „operátory“ využívá:
from typing import Callable
def get_operator(symbol: str) -> Callable[[int, int], int]:
operators = {
"+": add,
"*": mul,
}
return operators[symbol]
def calc(operator: Callable[[int, int], int], x: int, y: int) -> int:
return operator(x, y)
def add(x: int, y: int) -> int:
return x + y
def mul(x: int, y: int) -> int:
return x * y
def less_than(x: int, y: int) -> bool:
return x < y
z = calc(get_operator("+"), 10, 20)
print(z)
z = calc(get_operator("*"), 10, 20)
print(z)
z = calc(less_than, 10, 20)
print(z)
Výsledky získané po spuštění tohoto příkladu:
30 200 True
Využití Callable[…, ]
Vyzkoušejme si nyní naprogramovat funkci nazvanou log_calc, které je možné předat (jinou) funkci s libovolným počtem a typy parametrů. Reference na tuto funkci je předána v parametru nazvaném function. Tato funkce se v rámci log_calc zavolá, pochopitelně i s předáním parametrů a vypočtená (vrácená) hodnota se vypíše. Vzhledem k tomu, že neznáme ani počet ani typ parametrů předané funkce function, bude výsledný programový obsahovat typové informace i s uvedením …:
def log_calc(function: Callable[..., Any], *args: Any) -> Any:
function_name = getattr(function, '__name__', repr(function))
print()
print(f"Called with {function_name} with args={args}")
result = function(*args)
print(f"Evaluated result: {result}")
return result
Příklad použití:
from typing import Any, Callable
def log_calc(function: Callable[..., Any], *args: Any) -> Any:
function_name = getattr(function, '__name__', repr(function))
print()
print(f"Called with {function_name} with args={args}")
result = function(*args)
print(f"Evaluated result: {result}")
return result
def add(x: int, y: int) -> int:
return x + y
def mul(x: int, y: int) -> int:
return x * y
def less_than(x: int, y: int) -> bool:
return x < y
print(log_calc(min, 10, 20))
print(log_calc(max, 10, 20))
print(log_calc(id, 42))
print(log_calc(add, 10, 20))
print(log_calc(mul, 6, 7))
print(log_calc(less_than, 10, 20))
Výsledky ukazují, že vše pracuje podle očekávání:
Called with min with args=(10, 20) Evaluated result: 10 10 Called with max with args=(10, 20) Evaluated result: 20 20 Called with id with args=(42,) Evaluated result: 10093224 10093224 Called with add with args=(10, 20) Evaluated result: 30 30 Called with mul with args=(6, 7) Evaluated result: 42 42 Called with less_than with args=(10, 20) Evaluated result: True True
Praktické příklady využití typu Callable[…, ]
S typem Callable[…, ] se relativně často setkáme i v praxi. Ukažme si dva podobné příklady z praxe. První příklad obsahuje definici dekorátoru @connection, který je možné zapsat před libovolnou metodu. Ještě před zavoláním takové metody se voláním connected zjistí, jestli existuje aktivní připojení k databázi (resp. lze pochopitelně použít jakékoli další sémanticky podobné připojení – realizace metody záleží na programátorovi). Pokud connected vrátí hodnotu False, zavolá se metoda connect a následně i metoda předaná do dekorátoru:
"""Decorator that makes sure the object is 'connected' according to it's connected predicate."""
from collections.abc import Callable
from typing import (
Concatenate,
ParamSpec,
Protocol,
TypeVar,
runtime_checkable,
)
P = ParamSpec("P")
R = TypeVar("R")
S = TypeVar("S", bound="Connectable") # the method's self type
@runtime_checkable
class Connectable(Protocol):
"""Any class that implements methods connected and connect."""
def connected(self) -> bool:
"""Check if DB is connected."""
return False
def connect(self) -> None:
"""Connect or reconnect the database."""
def connection(
f: Callable[Concatenate[S, P], R],
) -> Callable[..., R]:
"""
Ensure a connectable object is connected before invoking the wrapped method.
The returned wrapper calls `connectable.connected()` and, if that returns
`False`, calls `connectable.connect()` prior to delegating to the original
method.
Parameters:
----------
f (Callable): The method to wrap. The wrapped method is
expected to accept a `connectable` first argument.
Returns:
-------
Callable: A wrapper method with signature `(connectable,
*args, **kwargs)` that ensures `connectable` is connected
before calling `f`.
Example:
```python
@connection
def list_history(self) -> list[str]:
pass
```
"""
def wrapper(self: S, *args: P.args, **kwargs: P.kwargs) -> R:
"""
Ensure the provided connectable is connected, then call the wrapped with the same arguments.
Parameters:
----------
connectable (Any): Object that implements `connected()` -> bool and
`connect()` -> None; will be connected if not already.
*args (Any): Positional arguments forwarded to the wrapped callable.
**kwargs (Any): Keyword arguments forwarded to the wrapped callable.
Returns:
-------
Any: The value returned by the wrapped callable.
"""
if not self.connected():
self.connect()
return f(self, *args, **kwargs)
return wrapper
Druhý příklad použití Callable[…, ] zajišťuje, že je předaná asynchronní funkce zavolána pouze jedenkrát:
def run_once_async(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Ensure that an async function is executed only once.
On the first invocation the wrapped coroutine is scheduled as an
asyncio.Task on the current running event loop and its Task is cached.
Later invocations return/await the same Task, receiving the same result or
propagated exception. Requires an active running event loop when the
wrapped function is first called.
Returns:
Any: The result produced by the wrapped coroutine, or the exception it
raised propagated to callers.
"""
task = None
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
"""
Run the wrapped async function exactly once and return its (awaited) result on every call.
On the first invocation this schedules the underlying coroutine as an
asyncio.Task on the current running event loop and caches that task.
Subsequent calls return the same awaited task result. Exceptions raised
by the task propagate to callers. Requires an active running event loop
when first called.
Returns:
The awaited result of the wrapped coroutine.
"""
nonlocal task
if task is None:
loop = asyncio.get_running_loop()
task = loop.create_task(func(*args, **kwargs))
return await task
return wrapper
Indexování využívané v knihovně Numpy (n-rozměrná pole)
Pravděpodobně nejpraktičtější využití trojtečky … nalezneme ve známé knihovně Numpy při indexování vícerozměrných polí. V mnoha případech je totiž vhodné provést výběr prvků pole pouze na základě některých dimenzí (indexů). A právě takovou operaci je možné s využitím trojtečky provést.
Ukažme si příklad s dvourozměrným polem (maticí) 5×5 prvků. Provedeme tři různé výběry z tohoto pole, přičemž vždy jeden z indexů bude roven dvojce:
import numpy as np a=np.arange(25) print(a) print() b=a.reshape((5, 5)) print(b) print() print(b[2]) print() print(b[2, ...]) print() print(b[..., 2]) print()
Po spuštění příkladu se nejdříve vypíše původní vektor i dvourozměrné pole (matice) vytvořené z tohoto vektoru:
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24] [[ 0 1 2 3 4] [ 5 6 7 8 9] [10 11 12 13 14] [15 16 17 18 19] [20 21 22 23 24]]
Dále se vypíše prvek s indexem 2 (v nejvyšší dimenzi), což je třetí řádek matice:
[10 11 12 13 14]
Další dva výběry z matice jsou založeny na použití trojtečky:
[10 11 12 13 14] [ 2 7 12 17 22]
Ve druhém případě jsme získali sloupec s indexem 2 (tedy v pořadí třetí sloupec). To je poměrně elegantní řešení, že?
Další příklady využití … při indexování n-rozměrných polí
Výběr prvků, který jsme si ukázali pro dvourozměrné pole, lze pochopitelně rozšířit i pro vícerozměrná pole. Ukažme si to na poli trojrozměrném; jeho konkrétní tvar (shape) je 3×4×2 prvky:
import numpy as np a=np.arange(24) print(a) print() b=a.reshape((3, 4, 2)) print(b) print() print(b[1]) print() print(b[..., 1]) print() print(b[..., 1, 1]) print()
Nejdříve se opět vypíše původní jednorozměrný vektor a z něho vytvořené pole 3×4×2 prvky:
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23] [[[ 0 1] [ 2 3] [ 4 5] [ 6 7]] [[ 8 9] [10 11] [12 13] [14 15]] [[16 17] [18 19] [20 21] [22 23]]]
Výsledkem operace b[1] je dvourozměrné pole (matice):
[[ 8 9] [10 11] [12 13] [14 15]]
Druhou výběrovou operací je b[…, 1], kterou získáme matici (specifikovali jsme jen druhý index):
[[ 1 3 5 7] [ 9 11 13 15] [17 19 21 23]]
Tento výběr ještě můžeme zúžit operací b[…, 1, 1], která z 2D matice získá druhý sloupec (s indexem 1):
[ 3 11 19]
Tři tečky mohou být použity i mezi explicitně zapsanými indexy:
import numpy as np a=np.arange(24) print(a) print() b=a.reshape((3, 4, 2)) print(b) print() print(b[1, ..., 1]) print() print(b[2, ..., 0]) print()
Výsledky:
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23] [[[ 0 1] [ 2 3] [ 4 5] [ 6 7]] [[ 8 9] [10 11] [12 13] [14 15]] [[16 17] [18 19] [20 21] [22 23]]]
Výběr b[1, …, 1] vrátí druhý sloupec druhé matice, ze které se 3D pole skládá:
[ 9 11 13 15]
Výběr b[2, …, 0] vrátí první sloupec z matice třetí:
[16 18 20 22]
Ovšem trojtečku není možné použít vícekrát:
import numpy as np a=np.arange(24) print(a) print() b=a.reshape((3, 4, 2)) print(b) print() print(b[..., 1, ...]) print()
Poslední výběr není možné realizovat:
File "/home/ptisnovs/src/most-popular-python-libs/ellipsis/numpy_4.py", line 11, in <module>
print(b[..., 1, ...])
~^^^^^^^^^^^^^
IndexError: an index can only have a single ellipsis ('...')
Využití … při komunikaci mezi procesy nebo vlákny
Poslední příklad, ve kterém použijeme trojtečku …, se teprve dostává do podvědomí, ovšem do značné míry odpovídá filozofii testu na nepředaný nepovinný parametr funkce atd. Trojtečka se totiž začíná používat i při komunikaci mezi několika procesy nebo vlákny. Opět se jedná o téma, kterému jsme se již věnovali (viz odkazy uvedené na konci článku). Připomeňme si, že pro komunikaci mezi procesy nebo vlákny lze mj. využít i fronty (queue), které se do jisté míry chovají jako kanály v programovacím jazyku Go. Ovšem v případě, že má jeden proces či vlákno oznámit ukončení své činnosti popř. naopak požádat další proces nebo vlákno o ukončení činnosti, je většinou nutné přes frontu předat vhodnou hodnotu, která sémanticky znamená „ukonči se“ nebo „ukončuji se“. Samozřejmě je k tomuto účelu možné použít jakoukoli hodnotu, například None (pokud nemá jiný význam), řetězec „quit“, instanci nějaké třídy atd. A nebo se může použít Ellipsis neboli tři tečky…
Příklad využití … v komunikaci mezi vlákny s využitím sdílené fronty
V dalším demonstračním příkladu se spustí několik vláken, které mezi sebou sdílí frontu, přes kterou spolu komunikují. Pokud vlákno z fronty přečte příkaz „quit“ (řetězec), je ihned ukončeno. Fronta tedy slouží i pro řízení výpočtů:
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových vláknech
# - komunikace mezi vlákny s využitím fronty
CONCURRENCY_LEVEL = 5
TASKS = 20
WAIT_FOR_KEY = True
SLEEP_AMOUNT = 1
from queue import Queue
from threading import Thread
import time
def worker(name, q):
"""Worker spuštěný několikrát v samostatných vláknech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Thread '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Thread '{name}' is about to quit")
return
if SLEEP_AMOUNT > 0:
time.sleep(SLEEP_AMOUNT)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi vlákny
q = Queue()
ts = []
# vytvoření procesů
for i in range(CONCURRENCY_LEVEL):
name = f"Thread #{i}"
ts.append(Thread(target=worker, daemon=True, name=name, args=[name, q]))
# spuštění vláken
for t in ts:
t.start()
print("Sending data to other threads")
# komunikace s vlákny přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
if WAIT_FOR_KEY:
print("Press Enter to force all threads to finish")
input()
print("Asking other threads to finish")
# příkaz pro ukončení vláken
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
print("Waiting for other threads")
# čekání na zpracování všech zpráv ve frontě
for t in ts:
t.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
Úprava pro použití trojtečky je snadná:
# příkaz pro ukončení vláken
for i in range(CONCURRENCY_LEVEL):
q.put(...)
a:
if cmd is ...:
print(f"Thread '{name}' is about to quit")
return
Úplný zdrojový kód takto upraveného příkladu vypadá následovně:
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových vláknech
# - komunikace mezi vlákny s využitím fronty
CONCURRENCY_LEVEL = 5
TASKS = 20
WAIT_FOR_KEY = True
SLEEP_AMOUNT = 1
from queue import Queue
from threading import Thread
import time
def worker(name, q):
"""Worker spuštěný několikrát v samostatných vláknech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Thread '{name}' received command '{cmd}'")
if cmd is ...:
print(f"Thread '{name}' is about to quit")
return
if SLEEP_AMOUNT > 0:
time.sleep(SLEEP_AMOUNT)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi vlákny
q = Queue()
ts = []
# vytvoření procesů
for i in range(CONCURRENCY_LEVEL):
name = f"Thread #{i}"
ts.append(Thread(target=worker, daemon=True, name=name, args=[name, q]))
# spuštění vláken
for t in ts:
t.start()
print("Sending data to other threads")
# komunikace s vlákny přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
if WAIT_FOR_KEY:
print("Press Enter to force all threads to finish")
input()
print("Asking other threads to finish")
# příkaz pro ukončení vláken
for i in range(CONCURRENCY_LEVEL):
q.put(...)
print("Waiting for other threads")
# čekání na zpracování všech zpráv ve frontě
for t in ts:
t.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
Upozornění: … není to samé jako _
V závěrečné části dnešního článku je nutné upozornit na to, že tři tečky … nemají ani vzdáleně stejný význam, jako má podtržítko _. To je totiž využíváno jako takzvaný placeholder v místech, kde je očekáván nějaký identifikátor, ovšem nezajímá nás, jaká hodnota do něj bude přiřazena. Příkladem je funkce vracející tři hodnoty, přičemž nás zajímají jen hodnoty dvě:
_, y, z = get_xyz()
Podobným způsobem se setkáme se znakem _ v jazykové konstrukci match-case, konkrétně (například) v poslední větvi zachytávající všechny ostatní možnosti, které nebyly zachyceny větvemi předchozími:
def perform_command():
response = input("> ")
match response:
case "quit":
return "Quit"
case "list employees":
return "List employees"
case "list departments":
return "List departments"
case "list rooms":
return "List rooms"
case _:
return "Wrong command"
print(perform_command())
Použití trojtečky je v tomto případě nekorektní:
def perform_command():
response = input("> ")
match response:
case "quit":
return "Quit"
case "list employees":
return "List employees"
case "list departments":
return "List departments"
case "list rooms":
return "List rooms"
case ...:
return "Wrong command"
print(perform_command())
Na nekorektní použití nás upozorní interpret Pythonu:
File "/home/ptisnovs/src/most-popular-python-libs/ellipsis/multiword_commands_ellipsis.py", line 13
case ...:
^^^
SyntaxError: invalid syntax
Trojice teček v dalších programovacích jazycích
„Trojtečky“ ve skutečnosti nejsou jen specialitou Pythonu, protože tento zápis můžeme najít i v dalších programovacích jazycích. Například v GNU C (jako rozšíření) nebo v nových standardech C lze použít trojtečku pro zápis intervalu:
void writeUnicode(char32_t c) {
switch (c) {
// matches any value between [0, 0x7F] inclusive
case 0 ... 0x7F:
break;
// matches any value between [0x80, 0x7FF] inclusive
case 0x80 ... 0x7FF:
break;
// matches any value between [0x800, 0xFFFF] inclusive
case 0x800 ... 0xFFFF:
break;
default:
unreachable();
}
}
Opět v GNU C lze realizovat inicializaci části pole:
int values[1000] = { [0...10] = 1 };
Pravděpodobně nejčastěji se ovšem trojtečka používá pro zápis hlavičky funkce, která akceptuje proměnný počet parametrů stejného typu. Ukažme si hlavičky takových funkcí zapsané v C, Javě (metoda), Go a taktéž v jazyku C3:
void baz(...);
public void baz(int... values);
func baz(values ...int) {
fn void baz(int... values)
Repositář s demonstračními příklady
Všechny demonstrační příklady popsané v tomto článku naleznete i v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady:
Odkazy na Internetu
- Python Ellipsis (triple dots): What is it, How to Use
https://python.land/python-ellipsis - What is Three dots(…) or Ellipsis in Python3
https://www.geeksforgeeks.org/python/what-is-three-dots-or-ellipsis-in-python3/ - When Do You Use an Ellipsis in Python?
https://realpython.com/python-ellipsis/ - All about Ellipsis (…) in Python
https://www.youtube.com/watch?v=2f0nAi7JJCk - python: Ellipsis (…) and typing (beginner – intermediate) anthony explains #067
https://www.youtube.com/watch?v=yLwvOwTO068 - Python dataclasses will save you HOURS, also featuring attrs
https://www.youtube.com/watch?v=vBH6GRJ1REM - It's Pointless! Or Isn't It? Python's
EllipsisHas Three …
https://www.thepythoncodingstack.com/p/pythons-ellipsis-pointless-or-useful - Three Dots in Python, What is the Ellipsis Object?
https://pakstech.com/blog/python-ellipsis/ - Ellipsis (Wikipedia)
https://en.wikipedia.org/wiki/Ellipsis - Výpustka (Wikipedia)
https://cs.wikipedia.org/wiki/V%C3%BDpustka - Seriál Programovací jazyk Rust
https://www.root.cz/serialy/programovaci-jazyk-rust/ - Seriál Programovací jazyk Go
https://www.root.cz/serialy/programovaci-jazyk-go/ - Seriál Programovací jazyk C3
https://www.root.cz/serialy/programovaci-jazyk-c3/ - Ellipsis (computer programming)
https://en.wikipedia.org/wiki/Ellipsis_(computer_programming) - Validace dat v Pythonu s využitím knihovny Pydantic
https://www.root.cz/clanky/validace-dat-v-pythonu-s-vyuzitim-knihovny-pydantic/ - Validace dat v Pythonu s využitím knihovny Pydantic (2. část)
https://www.root.cz/clanky/validace-dat-v-pythonu-s-vyuzitim-knihovny-pydantic-2-cast/ - Validace dat v Pythonu s využitím knihovny Pydantic (3. část – dokumentace)
https://www.root.cz/clanky/validace-dat-v-pythonu-s-vyuzitim-knihovny-pydantic-3-cast-dokumentace/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu (2)
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-2/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – závěrečné zhodnocení
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-zaverecne-zhodnoceni/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – knihovna Trio
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-knihovna-trio/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – knihovna Trio (2)
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-knihovna-trio-2/
