… v Pythonu: to není lenost, ale produkční kód

13. 5. 2026
Doba čtení: 27 minut

Sdílet

Python tři tečky
Autor: Duck.ai
Jeden známý okomentoval můj kód psaný v Pythonu slovy: „kdy to dokončíš, aby to šlo spustit?“ Vysvětlilo se, že myslí výpustky („trojtečky“, …), které jsem použil. Ve skutečnosti hrají v Pythonu důležité role.

… v Pythonu: to není lenost, ale produkční kód

Co se dozvíte v článku
  1. … v Pythonu: to není lenost, ale produkční kód
  2. Objektový systém jazyka Python a literál …
  3. Je tedy … pouze jmenným aliasem pro Ellipsis?
  4. Způsoby využití … v Pythonu
  5. Definice funkce s dočasným tělem
  6. Využití … jako výchozí hodnoty volitelného parametru funkce
  7. Specifikace datového typu n-tice s neznámým počtem prvků
  8. Specifikace typu a dalších vlastností atributu bez uvedení výchozí hodnoty (Pydantic)
  9. Příklad složitějšího modelu, ve kterém jsou definovány atributy bez výchozí hodnoty
  10. Typová informace: funkce akceptující neznámý počet a typ parametrů
  11. Využití Callable[…, ]
  12. Praktické příklady využití typu Callable[…, ]
  13. Indexování využívané v knihovně Numpy (n-rozměrná pole)
  14. Další příklady využití … při indexování n-rozměrných polí
  15. Využití … při komunikaci mezi procesy nebo vlákny
  16. Příklad využití … v komunikaci mezi vlákny s využitím sdílené fronty
  17. Upozornění: … není to samé jako _
  18. Trojice teček v dalších programovacích jazycích
  19. Repositář s demonstračními příklady
  20. 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 Ellipsisellipsis:

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
Poznámka: vypsané numerické hodnoty se velmi pravděpodobně budou ve vašem případě odlišovat, ale oba první řádky budou totožné a poslední řádek bude obsahovat hodnotu 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
Poznámka: proměnné ve skutečnosti obsahují reference na hodnoty, takže (jediná) instance třídy ellipsis nemusí být přiřazením dotčena.

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 '='?
Poznámka: v podmínce se … vyhodnotí jako pravda (True). Víte, proč tomu tak je?

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:

  1. 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.
  2. 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).
  3. Ve FastAPI či při použití knihovny Pydantic se trojtečka používá v definicích modelů resp. atributů modelů.
  4. 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ů.
  5. V knihovně Numpy se trojtečka zapisuje resp. může zapisovat při indexaci prvků n-rozměrných polí.
  6. 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
Poznámka: jedinou „speciální hodnotou“ je v tomto kontextu dokumentační řetězec.

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():
   ...
Poznámka: v Pythonu se jedná o zcela korektní funkci, kterou je možné zavolat. Literál je zde použit stejně tak, jako jsme dříve použili hodnotu 42. Žádný jiný speciální význam v tomto kontextu nemá, pouze čtenářům kódu naznačuje, že tato funkce bude později upravena.

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!
Poznámka: pochopitelně i toto řešení má problém, a to při předání samotných tří teček:
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
Poznámka: poslední způsob zápisu je většinou nejlepší a zamezí většině nejasností.

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
Poznámka: v tomto případě se tedy jedná o určitou obdobu cache s výsledky volání funkce.

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:

Školení Kubernetes

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)
Poznámka: sémantika je tedy značně odlišná v porovnání s Pythonem.

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:

# Příklad Stručný popis Adresa příkladu
1 ellipsis_help.py zobrazení nápovědy ke třídě ellipsis a jejímu jedináčkovi Ellipsis https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_help.py
2 ellipsis_type.py zobrazení typu hodnoty Ellipsis a literálu … https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_type.py
3 ellipsis_id.py zobrazení id hodnoty Ellipsis a literálu … https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_id.py
       
4 ellipsis_print1.py zobrazení hodnoty Ellipsis https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_print1.py
5 ellipsis_print2.py zobrazení hodnoty literálu … https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_print2.py
6 ellipsis_assignment1.py přiřazení nové hodnoty do proměnné Ellipsis https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_assignment1.py
7 ellipsis_assignment2.py pokus o přiřazení nové hodnoty k literálu … https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_assignment2.py
       
8 ellipsis_function.py literál … použitý v definici funkce https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_function.py
9 ellipsis_sentinel.py literál … použitý jako výchozí hodnota nepovinného parametru funkce https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_sentinel.py
10 ellipsis_sentinel2.py literál … použitý jako výchozí hodnota nepovinného parametru funkce https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/e­llipsis_sentinel2.py
11 none_sentinel.py hodnot None použitá jako výchozí hodnota nepovinného parametru funkce https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/no­ne_sentinel.py
       
12 adder.py kombinace dekorátoru @overload a funkcí obsahujících jen … https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/adder.py
13 connection_decorator.py dekorátor, ve kterém se používá typ Callable https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/con­nection_decorator.py
       
14 model_character.py využití … v deklaraci modelu knihovny Pydantic https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/mo­del_character.py
15 model_user.py využití … v deklaraci modelu knihovny Pydantic https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/mo­del_user.py
       
16 multiple_threads1.py komunikace mezi vlákny s využitím fronty https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/mul­tiple_threads1.py
17 multiple_threads2.py komunikace mezi vlákny s využitím fronty, využití hodnoty … pro ukončení spojení https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/mul­tiple_threads2.py
18 multiword_commands.py ukázka, že … není _ (placeholder) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/mul­tiword_commands.py
       
19 numpy1.py indexování vícerozměrných polí a literál … (dvourozměrné pole) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/numpy1.py
20 numpy2.py indexování vícerozměrných polí a literál … (trojrozměrné pole) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/numpy2.py
21 numpy3.py indexování vícerozměrných polí a literál … (složitější indexování) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/numpy3.py
22 numpy4.py indexování vícerozměrných polí a literál … (nekorektní použití) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/numpy4.py
       
23 run_once_async.py literál … a typ Callable https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/run_on­ce_async.py
       
24 tuple1.py literál … a specifikace typu prvků v n-tici (základní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/tuple1.py
25 tuple2.py literál … a specifikace typu prvků v n-tici (složitější varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/tuple2.py
26 tuple3.py literál … a specifikace typu prvků v n-tici (reálný kód) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/tuple3.py
27 tuple4.py literál … a specifikace typu prvků v n-tici (reálný kód) https://github.com/tisnik/most-popular-python-libs/blob/master/ellipsis/tuple4.py

Odkazy na Internetu

  1. Python Ellipsis (triple dots): What is it, How to Use
    https://python.land/python-ellipsis
  2. What is Three dots(…) or Ellipsis in Python3
    https://www.geeksforgeeks­.org/python/what-is-three-dots-or-ellipsis-in-python3/
  3. When Do You Use an Ellipsis in Python?
    https://realpython.com/python-ellipsis/
  4. All about Ellipsis (…) in Python
    https://www.youtube.com/wat­ch?v=2f0nAi7JJCk
  5. python: Ellipsis (…) and typing (beginner – intermediate) anthony explains #067
    https://www.youtube.com/wat­ch?v=yLwvOwTO068
  6. Python dataclasses will save you HOURS, also featuring attrs
    https://www.youtube.com/wat­ch?v=vBH6GRJ1REM
  7. It's Pointless! Or Isn't It? Python's Ellipsis Has Three …
    https://www.thepythoncodin­gstack.com/p/pythons-ellipsis-pointless-or-useful
  8. Three Dots in Python, What is the Ellipsis Object?
    https://pakstech.com/blog/python-ellipsis/
  9. Ellipsis (Wikipedia)
    https://en.wikipedia.org/wi­ki/Ellipsis
  10. Výpustka (Wikipedia)
    https://cs.wikipedia.org/wi­ki/V%C3%BDpustka
  11. Seriál Programovací jazyk Rust
    https://www.root.cz/seria­ly/programovaci-jazyk-rust/
  12. Seriál Programovací jazyk Go
    https://www.root.cz/seria­ly/programovaci-jazyk-go/
  13. Seriál Programovací jazyk C3
    https://www.root.cz/seria­ly/programovaci-jazyk-c3/
  14. Ellipsis (computer programming)
    https://en.wikipedia.org/wi­ki/Ellipsis_(computer_pro­gramming)
  15. Validace dat v Pythonu s využitím knihovny Pydantic
    https://www.root.cz/clanky/validace-dat-v-pythonu-s-vyuzitim-knihovny-pydantic/
  16. 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/
  17. 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/
  18. Souběžné a paralelně běžící úlohy naprogramované v Pythonu
    https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu/
  19. 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/
  20. 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/
  21. 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/
  22. 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/
Neutrální ikona do widgetu na odběr článků ze seriálů

Zajímá vás toto téma? Chcete se o něm dozvědět víc?

Objednejte si upozornění na nově vydané články do vašeho mailu. Žádný článek vám tak neuteče.


Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.