Hacky pro Python: vlastní infixové operátory

5. 6. 2025
Doba čtení: 21 minut

Sdílet

Autor: Root.cz s využitím DALL-E
Příručky jazyka Python (zcela správně) doporučují, aby byly zdrojové kódy psány idiomatickým způsobem. To však neznamená, že není možné syntaxi Pythonu „ohnout“ a realizovat tak různé triky.

Obsah

1. Hacky pro Python: vlastní infixové operátory

2. Přetěžování operátorů v Pythonu

3. Speciální metody použité při přetěžování operátorů

4. Ukázka realizace datového typu „komplexní číslo“

5. Realizace vlastního infixového operátoru přetížením dvojice standardních operátorů

6. Způsob implementace

7. První demonstrační příklad: třída HackyMul umožňující realizaci vlastního operátoru součinu

8. Odstranění ladicích informací, výpočet tabulky malé násobilky s využitím nového operátoru součinu

9. Praktičtější příklad: realizace operátoru pro skalární součin

10. Realizace nového operátoru s využitím znaku |

11. Demonstrační příklad: operátor skalární součinu zapisovaný znaky |o|

12. Problematika přetížení operátorů < a >

13. Korektní použití operátorů < a >

14. Univerzální konstruktor vlastních infixových operátorů

15. Sedmý demonstrační příklad: definice a použití vlastních infixových operátorů

16. Osmý demonstrační příklad: variabilní použití operátorů

17. Které znaky je možné použít v definovaných operátorech

18. Možné problémy

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Hacky pro Python: vlastní infixové operátory

V ekosystému programovacího jazyka Python nenajdeme jen samotnou specifikaci jazyka, několik jeho interpretrů a obrovské množství podpůrných nástrojů a knihoven. Součástí tohoto ekosystému jsou i více či méně formální doporučení, jakým způsobem se mají psát zdrojové kódy. Tato doporučení jsou do jisté míry validována různými nástroji typu Ruff, Pylint, PyRight, Pydocstyle atd. Ovšem to neznamená, že všechny zdrojové kódy psané v Pythonu musí být psány naprosto stejným stylem a podle jedné šablony. Python totiž umožňuje tvořit aplikace založené na objektově orientovaném programování, aplikace psané ve funkcionálním stylu atd. A navíc je možné (ve vhodných situacích) programovací jazyk „ohnout“ a využít jeho vlastností například pro vytvoření nových infixových operátorů (což zdánlivě není povoleno). Dnes si právě tento trik ukážeme a pochopitelně se zmíníme i o tom, zda je to vhodný či nevhodný přístup.

Jen pro ukázku, co je naším cílem: například se jedná o definici operátoru pro skalární součin:

x = [1, 2, 3]
y = [3, 4, 5]
 
print(x <<o>> y)

2. Přetěžování operátorů v Pythonu

Nejprve se ve stručnosti seznámíme s problematikou přetěžování standardních operátorů v Pythonu, což je technika, která nám umožní měnit chování operátorů, ovšem již neumožňuje nový operátor přidat. Tento programovací jazyk nabízí (pro různé datové typy) celou řadu operátorů, které jsou podle své priority rozděleny do osmnácti kategorií – od nejvyšší priority směrem k prioritě nejnižší:

Pořadí Operátory
1. (výrazy…), [výrazy…], {klíč: hodnota…}, {výrazy…}
2. x[index], x[index:index], x(argumenty…), x.atribut
3. await x
4. **
5. +x, -x, ~x (unární)
6. *, @, /, //, %
7. +, –
8. <<, >>
9. &
10. ^
11. |
12. in, not in, is, is not, <, <=, >, >=, !=, ==
13. not x
14. and
15. or
16. if – else
17. lambda
18. :=

3. Speciální metody použité při přetěžování operátorů

Většinu operátorů zmíněných v předchozí kapitole je možné přetížit, tj. definovat jejich chování (jaký výpočet se provede) pro různé datové typy. Například je možné si nadefinovat datový typ představující vektory a definovat pro ně základní operátory (součet prvek po prvku atd.), totéž udělat pro matice, komplexní čísla atd. Vlastní předefinování chování operátoru je relativně snadné – pouze postačuje vytvořit speciální metodu, která bude zavolána v případě, že je operátor použit v programovém kódu. Tyto speciální metody mají v některých případech dvojí variantu – podle toho, na které straně operátoru se nachází operand příslušného typu (a toho později využijeme):

Přetížený operátor Název speciální metody Varianta s druhým operandem
+ __add__ __radd__
__sub__ __rsub__
* __mul__ __rmul__
/ __truediv__ __rtruediv__
// __floordiv__ __rfloordiv__
% __mod__ __rmod__
** __pow__ __rpow__
     
+= __iadd__  
-= __isub__  
*= __imul__  
/= __itruediv__  
//= __ifloordiv__  
%= __imod__  
**= __ipow__  
     
– (unární) __neg__  
+ (unární) __pos__  
     
< __lt__  
<= __le__  
== __eq__  
!= __ne__  
>= __ge__  
> __gt__  
     
& __and__ __rand__
| __or__ __ror__
^ __xor__ __rxor__
<< __lshift__ __rlshift__
>> __rshift__ __rrshift__
~ __invert__  
     
&= __iand__  
|= __ior__  
^= __ixor__  
<<= __ilshift__  
>>= __irshift__  
     
in __contains__
Poznámka: některé další operátory, například indexování, nejsou v tabulce uvedeny, i když by mohly být zařazeny mezi operátory binární.

4. Ukázka realizace datového typu „komplexní číslo“

Způsob přetížení některých vybraných unárních i binárních operátorů si ukážeme na příkladu komplexních čísel, přesněji řečeno na třídě Complex. V rámci definice této třídy jsou přetíženy operátory =, – (unární), +, – a +=. Implementace by měla být ze zdrojového kódu snadno pochopitelná:

class Complex:
 
    def __init__(self, real=0, imag=0):
        self._real = real
        self._imag = imag
 
    def __str__(self):
        return "{r} + {i}j".format(r=self._real, i=self._imag)
 
    def __eq__(self, other):
        """Přetížení binárního operátoru ==."""
        return self._real == other._real and self._imag == other._imag
 
    def __neg__(self):
        """Přetížení unárního operátoru -."""
        return Complex(-self._real, -self._imag)
 
    def __add__(self, other):
        """Přetížení binárního operátoru +."""
        r = self._real + other._real
        i = self._imag + other._imag
        return Complex(r, i)
 
    def __sub__(self, other):
        """Přetížení binárního operátoru -."""
        r = self._real - other._real
        i = self._imag - other._imag
        return Complex(r, i)
 
    def __iadd__(self, other):
        """Přetížení binárního operátoru +=."""
        self._real += other._real
        self._imag += other._imag
        return self
 
 
c1 = Complex(1, 2)
c2 = Complex(10, 20)
c3 = Complex(100)
c4 = Complex()
 
print("c1:", c1)
print("c2:", c2)
print("c3:", c3)
print("c4:", c4)
 
print()
 
print("-c1:", -c1)
print("-c2:", -c2)
 
print()
 
print("c1 + c2:", c1+c2)
print("c1 + c3:", c1+c3)
print("c1 - c2:", c1-c2)
print("c1 - c3:", c1-c3)
 
c2 += c3
print("c2 += c3:", c1)
 
print()
 
c5 = Complex(1, 2)
print("c1 == c5:", c1 == c5)
print("c2 == c5:", c2 == c5)
print("c3 == c5:", c3 == c5)

Vypočtené a vypsané výsledky ukazují, že přetížené operátory pracují korektně:

c1: 1 + 2j
c2: 10 + 20j
c3: 100 + 0j
c4: 0 + 0j
 
-c1: -1 + -2j
-c2: -10 + -20j
 
c1 + c2: 11 + 22j
c1 + c3: 101 + 2j
c1 - c2: -9 + -18j
c1 - c3: -99 + 2j
c2 += c3: 1 + 2j
 
c1 == c5: True
c2 == c5: False
c3 == c5: False

5. Realizace vlastního infixového operátoru přetížením dvojice standardních operátorů

Přetížení operátorů samo o sobě neumožňuje vytvoření nového operátoru. Co ovšem můžeme udělat, je vhodným způsobem přetížit dvojici existujících operátorů, například << a >>, a to takovým způsobem, že jejich kombinací (s vhodným „mezioperandem“) jakoby vznikne nový operátor. Příkladem je zápis uvedený už v první kapitole:

x = [1, 2, 3]
y = [3, 4, 5]
 
print(x <<o>> y)

Co ale vlastně zápis:

x <<o>> y

znamená? Dekódovat ho můžeme následovně:

  1. Provedení operace x << o, kde x je seznam a o je vhodným způsobem připravený objekt. Výsledkem je nějaká hodnota h.
  2. Provedení operace h >> y, kde h je hodnota z předchozího kroku a y je seznamem. Výsledek je současně i výsledkem celého zápisu x <<o>> y.

6. Způsob implementace

Musíme tedy vytvořit objekt o, jehož třída má přetížený operátor <<. Výsledná hodnota h musí být instancí třídy s přetíženým operátorem >>. Ovšem situaci si můžeme zjednodušit – pokud bude výsledkem první operace x << o opět instance stejné třídy, bude nám postačovat implementovat jen jedinou třídu, ve které budou přetíženy oba operátory << i >>. První přetížený operátor si zapamatuje svůj levý operand a po zavolání druhého operátoru se vyhodnotí výsledek celé operace. Budeme tedy muset definovat speciální metody __rlshift__ (operátor << s prohozenými operandy) a __rshift__ (operátor >>):

class NewOperator:
 
    def __rlshift__(self, other):
        self.value = other
        return self
 
    def __rshift__(self, other):
        # provedení výpočtu se self.value a other
        # vrácení výsledku tohoto výpočtu

7. První demonstrační příklad: třída HackyMul umožňující realizaci vlastního operátoru součinu

V dnešním prvním demonstračním příkladu je ukázán výše popsaný postup, který umožňuje realizaci vlastního operátoru násobení (resp. přesněji řečeno součinu), který se bude zapisovat pomocí symbolů <<x>> (namísto x bude ovšem možné použít libovolný jiný validní identifikátor). Celý trik, na kterém je tento příklad postaven, je založen na třídě pojmenované HackyMul, která vhodným způsobem přetíží operátory << a >> tak, aby se při aplikaci prvního operátoru zapamatoval levý operand (pravým operandem je instance této třídy) a aplikací druhého operátoru se vypočte součin, jehož hodnota se následně vrátí. Ve skutečnosti tedy zápis <<x>> obsahuje dva operátory. Navíc se v obou speciálních metodách vždy vypíše předaný parametr, aby bylo zřejmé, jak výpočet interně probíhá:

# Vlastní infixové operátory v Pythonu
#
# - třída HackyMul umožňující definici operátoru typu <<x>> apod.
# - při volání speciálních metod se vypisují ladicí informace
 
 
class HackyMul:
 
    def __rlshift__(self, other):
        print("rlshift:", other)
        self.value = other
        return self
 
    def __rshift__(self, other):
        print("rshift: ", other)
        return self.value * other
 
 
x = HackyMul()
print(2 <<x>> 3)
 
y = HackyMul()
print(3 <<y>> 3)
 
z = HackyMul()
i = 10
j = 20
print(i <<z>> j)

Po spuštění skriptu se můžeme přesvědčit, že výsledky skutečně odpovídají běžnému součinu:

rlshift: 2
rshift:  3
6
 
rlshift: 3
rshift:  3
9
 
rlshift: 10
rshift:  20
200
Poznámka: jak ovšem uvidíme v dalším textu, může v praxi nastat hned celá řada problémů, před kterými nás neochrání ani interpret Pythonu, ani žádné další pomocné nástroje (lintery atd.).

8. Odstranění ladicích informací, výpočet tabulky malé násobilky s využitím nového operátoru součinu

Funkce print byly ve speciálních metodách __rlshift__ a __rshift__ volány pouze z toho důvodu, abychom si ujasnili, za jakých okolností a s jakými argumenty jsou tyto metody volány. V praxi se pochopitelně bez těchto pomocných výpisů většinou obejdeme a proto je ve druhém příkladu z definice třídy HackyMul odstraníme. Aby bylo zřejmé, že součin je počítán korektně, necháme si v dnešním druhém demonstračním příkladu vypočítat tabulku malé násobilky. Implementace tohoto demonstračního příkladu bude vypadat následovně:

# Vlastní infixové operátory v Pythonu
#
# - třída HackyMul umožňující definici operátoru typu <<x>> apod.
# - bez výpisu ladicích informací
# - výpočet tabulky malé násobilky přes nový "operátor"
 
 
class HackyMul:
 
    def __rlshift__(self, other):
        self.value = other
        return self
 
    def __rshift__(self, other):
        return self.value * other
 
 
x = HackyMul()
 
for j in range(1, 11):
    for i in range(1, 11):
        r = i <<x>> j
        print(f"{r:3d}", end=" ")
    print()

Z tabulky zobrazené níže je patrné, že výpočty s využitím našeho operátoru <<x>> probíhají korektně:

  1   2   3   4   5   6   7   8   9  10
  2   4   6   8  10  12  14  16  18  20
  3   6   9  12  15  18  21  24  27  30
  4   8  12  16  20  24  28  32  36  40
  5  10  15  20  25  30  35  40  45  50
  6  12  18  24  30  36  42  48  54  60
  7  14  21  28  35  42  49  56  63  70
  8  16  24  32  40  48  56  64  72  80
  9  18  27  36  45  54  63  72  81  90
 10  20  30  40  50  60  70  80  90 100

9. Praktičtější příklad: realizace operátoru pro skalární součin

Realizace vlastního operátoru pro provedení běžného součinu je pochopitelně pouze ukázka toho, jakým směrem je možné do jisté míry „ohnout“ syntaxi programovacího jazyka Python. Ukažme si tedy poněkud praktičtější příklad. Tím bude realizace operátoru pro výpočet skalárního součinu (dot product). Náš nový speciální operátor bude předpokládat, že na jeho levé i pravé straně budou jako operandy předány n-tice nebo seznamy mající stejný počet prvků. Operátor vrátí výsledek skalárního součinu obou operandů (ty jsou považovány za vektory délky n):

# Vlastní infixové operátory v Pythonu
#
# - třída DotProduct umožňující definici operátoru pro skalární součin
# - definice nového operátoru
# - otestování nového operátoru
 
 
class DotProduct:
 
    def __rlshift__(self, other):
        self.value = other
        return self
 
    def __rshift__(self, other):
        dot_product = 0
        for a, b in zip(self.value, other):
            dot_product += a * b
        return dot_product
 
 
o = DotProduct()
x = [1, 2, 3]
y = [3, 4, 5]
 
print(x <<o>> y)

Po spuštění tohoto skriptu si ověříme, zda je výpočet korektní či nikoli. Měla by se vypsat hodnota 26, která je získána výpočtem 1×3+2×4+3×5=3+8+15:

26
Poznámka: operátor pro výpočet skalárního součinu je skutečně poměrně praktický, zejména v době velkých jazykových modelů, neuronových sítí atd. Pokud se ho ovšem chystáte skutečně realizovat, může být výhodnější pro tento účel přetížit operátor @, který je v jazyce Python sice rezervován (je to skutečný a plnohodnotný operátor), ale není mu přiřazena žádná funkce. Například v knihovně NumPy je použit pro maticový součin atd.

Způsob přetížení operátoru @ a popř. @= může vypadat následovně:

# Operátor maticového násobení:
# - přetížení operátoru maticového násobení @
# - přetížení operátoru @= kombinujícího maticové násobení s přiřazením
 
 
class Test:
    """Třída s přetíženými operátory @ a @=."""
 
    def __init__(self, x):
        """Konstruktor."""
        self.value = str(x)
 
    def __str__(self):
        """Metoda volaná při tisku objektu."""
        return self.value
 
    def __matmul__(self, other):
        """Metoda přetěžující operátor @."""
        return Test(f"({self.value} @ {other.value})")
 
    def __imatmul__(self, other):
        """Metoda přetěžující operátor @=."""
        return Test(f"({self.value} @ {other.value})")
 
 
# konstrukce několika instancí třídy Test
t1 = Test(1)
print("t1:", t1)
 
t2 = Test(2)
print("t2:", t2)
 
t3 = Test(3)
print("t3:", t3)
 
# použití obou přetížených operátorů ve stejném příkazu
t3 @= t1 @ t2
print("t3 @= t1 @ t2:", t3)

10. Realizace nového operátoru s využitím znaku |

Samozřejmě nám nic nebrání v tom, aby nový operátor nebyl vytvořen ze znaků << a >>. Můžeme využít jakékoli jiné existující operátory. Například se může jednat o operátor |. Zde ovšem vyvstává otázka, jak v zápisu:

x |o| y

rozlišit první a druhý operátor, které se zapisují stejným znakem. Ve skutečnosti je to v tomto případě jednoduché, protože operátor | přetížíme pro náš objekt o (resp. pro třídu, které je o instancí) a v ní již můžeme snadno rozlišit „pravou“ a „levou“ variantu operátoru, tedy situaci, kdy se o nachází v pozici pravého nebo levého operandu. Definovat tedy budeme muset metody __ror__ (pravá varianta) a __or__ (levá varianta):

class NewOperator:
 
    def __ror__(self, other):
        self.value = other
        return self
 
    def __or__(self, other):
        # provedení výpočtu se self.value a other
        # vrácení výsledku tohoto výpočtu

11. Demonstrační příklad: operátor skalární součinu zapisovaný znaky |o|

V dalším demonstračním příkladu je ukázána realizace nového infixového operátoru, který bude zapisován s využitím znaků |o|. Od předchozích skriptů se tento příklad odlišuje tím, že jsou definovány speciální metody __ror__ a __or__. Musí se přitom definovat obě zmíněné metody, protože první z těchto metod je volána tehdy, pokud je instance třídy DotProduct pravým operandem a druhá metoda je volána naopak tehdy, pokud je instance DotProduct operandem levým:

# Vlastní infixové operátory v Pythonu
#
# - třída DotProduct umožňující definici operátoru pro skalární součin
# - odlišné symboly operátoru
# - definice nového operátoru
# - otestování nového operátoru
 
 
class DotProduct:
 
    def __ror__(self, other):
        self.value = other
        return self
 
    def __or__(self, other):
        dot_product = 0
        for a, b in zip(self.value, other):
            dot_product += a * b
        return dot_product
 
 
o = DotProduct()
x = [1, 2, 3]
y = [3, 4, 5]
 
print(x |o| y)

Spuštěním tohoto příkladu si opět ověříme, jestli jsou provedeny korektní výpočty (mělo by tomu tak být):

26

12. Problematika přetížení operátorů < a >

Prozatím jsme pro nové „operátory“ využívali existující operátory <<, >> a taktéž operátor |. Tento výběr nebyl náhodný, protože výsledkem byly vizuálně odlišné (de facto) nové prvky jazyka. Ovšem nic nám nebrání ani v tom, abychom pro stejný účel použili jiné standardní operátory. Řekněme, že si vybereme dvojici operátorů < a > (tedy nezdvojených variant). Mohlo by se zdát, že v tomto případě nám postačuje přetížit skutečně oba dva operátory, tj. definovat metody __lt__ a __gt__. Ve skutečnosti nebude toto řešení funkční, o čemž se můžeme velmi snadno přesvědčit:

# Vlastní infixové operátory v Pythonu
#
# - třída DotProduct umožňující definici operátoru pro skalární součin
# - pokus o redefinici speciálních metod pro operátory < a >
# - nekorektní řešení!
 
 
class DotProduct:
 
    def __lt__(self, other):
        self.value = other
        return self
 
    def __gt__(self, other):
        dot_product = 0
        for a, b in zip(self.value, other):
            dot_product += a * b
        return dot_product
 
 
o = DotProduct()
x = [1, 2, 3]
y = [3, 4, 5]
 
print(x <o> y)

Výsledkem bude chyba:

Traceback (most recent call last):
  File "/home/ptisnovs/src/most-popular-python-libs/custom_operators/infix_5.py", line 25, in <module>
    print(x <o> y)
          ^^^^^^^
  File "/home/ptisnovs/src/most-popular-python-libs/custom_operators/infix_5.py", line 16, in __gt__
    for a, b in zip(self.value, other):
                     ^^^^^^^^^^
AttributeError: 'DotProduct' object has no attribute 'value'

Tato chyba vznikla tím, že se vlastně vůbec nezavolala metoda __lt__! Je tomu tak z toho důvodu, že x < o sice používá operátor <, ovšem Python nemá přetíženou metodu __lt__ pro seznam (levý operand). Proto zavolá metodu __gt__ objektu o (nemáme totiž k dispozici variantu __rlt__).

13. Korektní použití operátorů < a >

Jedno z možných řešení problému zmíněného v předchozí kapitole spočívá v tom, že se přetíží pouze metoda __gt__ (ta je volaná dvakrát), ovšem interně se musíme rozhodnout, zda se jedná o první volání nebo o volání druhé. Na základě tohoto rozhodnutí si buď zapamatujeme levý operand, nebo provedeme výpočet skalárního součinu. Jedno z možných (nutno říci, že poměrně škaredých) řešení je ukázáno v dalším příkladu:

# Vlastní infixové operátory v Pythonu
#
# - třída DotProduct umožňující definici operátoru pro skalární součin
# - redefinice speciální metody pro operátor >
# - korektní řešení
 
 
class DotProduct:
 
    def __init__(self):
        self.value = None
 
    def __gt__(self, other):
        if self.value is None:
            print("<", other)
            self.value = other
            return self
 
        print(">", other)
        dot_product = 0
        for a, b in zip(self.value, other):
            dot_product += a * b
        return dot_product
 
 
o = DotProduct()
 
x = [1, 2, 3]
y = [3, 4, 5]
 
print(x <o> y)

Nyní by již vše mělo být funkční:

< [1, 2, 3]
> [3, 4, 5]
26

14. Univerzální konstruktor vlastních infixových operátorů

Ve všech předchozích příkladech jsme museli pro každý nový „operátor“ definovat novou třídu. Nabízí se ovšem otázka, zda není možné napsat třídu univerzální, tj. takovou třídu, do které by se pouze předala nějaká funkce určená pro implementaci požadované operace. Toto řešení je pochopitelně možné realizovat a jedno z možných řešení je ukázáno níže (pro operátor složený ze znaků << a >>). První polovina operátoru vrací novou instanci třídy s uzávěrem, který si kromě jiného pamatuje i předaný levý operand other. A druhá polovina operátoru provede vlastní výpočet zavoláním uzávěru:

class Infix:
 
    def __init__(self, function):
        self.function = function
 
    def __rlshift__(self, other):
        # uzávěr
        return Infix(lambda x, self=self, other=other: self.function(other, x))
 
    def __rshift__(self, other):
        # levá hodnota je již dostupná v uzávěru
        return self.function(other)

Příklad použití (tedy definice nového operátoru s jeho zavoláním):

x=Infix(lambda x,y: x*y)
print(2 <<x>> 4)

15. Sedmý demonstrační příklad: definice a použití vlastních infixových operátorů

Výše uvedený koncept si můžeme otestovat a to hned pro několik různých uživatelem definovaných operátorů:

# Vlastní infixové operátory v Pythonu
#
# - třída Infix umožňující definici jakéhokoli binárního operátoru
# - definice několika operátorů
# - otestování nových operátorů
 
 
class Infix:
 
    def __init__(self, function):
        self.function = function
 
    def __rlshift__(self, other):
        # uzávěr
        return Infix(lambda x, self=self, other=other: self.function(other, x))
 
    def __rshift__(self, other):
        # levá hodnota je již dostupná v uzávěru
        return self.function(other)
 
 
# operátor součinu
print("multiply:")
 
x=Infix(lambda x,y: x*y)
print(2 <<x>> 4)
 
print("-" * 20)
 
# operátor testu typu hodnoty
print("'is a' and 'instance of':")
isa=Infix(lambda x,y: x.__class__==y.__class__)
print([1,2,3] <<isa>> [])
 
# operátor testu, zda je hodnota instancí třídy
instance_of=Infix(lambda x,y: isinstance(x, y))
print(1 <<instance_of>> int)
print(1 <<instance_of>> str)
print("foo" <<instance_of>> str)
 
print(1 <<instance_of>> int and "foo" <<instance_of>> str)
 
print("-" * 20)
 
print("has attr:")
has_attr=Infix(lambda x,y: hasattr(x, y))
print(str <<has_attr>> "tolower")
print(str <<has_attr>> "tolowercase")
print(str <<has_attr>> "lower")
print(str <<has_attr>> "__doc__")
 
print("-" * 20)
 
print("has method:")
has_method=Infix(lambda x,y: hasattr(x, y) and callable(getattr(x, y, None)))
print(str <<has_method>> "tolower")
print(str <<has_method>> "tolowercase")
print(str <<has_method>> "lower")
print(str <<has_method>> "__doc__")
 
print("-" * 20)
 
print("is in:")
is_in=Infix(lambda x,y: x in y)
print(1 <<is_in>> {1:'one'})
print(1 <<is_in>> {1:'one'})
 
print("-" * 20)
 
print("gcd:")
 
import math
gcd=Infix(lambda x,y: math.gcd(x, y))
print(12 <<gcd>> 8)
 
 
print("div:")
 
import operator
div=Infix(operator.floordiv)
print(10 <<div>> (4 <<div>> 2))

Jak je ze zdrojového kódu patrné, vytvořili jsme si hned několik vlastních operátorů, které byly použity. Test, zda vše pracuje tak, jak má:

multiply:
8
--------------------
'is a' and 'instance of':
True
True
False
True
True
--------------------
has attr:
False
False
True
True
--------------------
has method:
False
False
True
False
--------------------
is in:
True
True
--------------------
gcd:
4
div:
5

16. Osmý demonstrační příklad: variabilní použití operátorů

Nic nám pochopitelně nebrání v tom, abychom uživatelům umožnili pro vlastní operátory používat jak dvojice znaků << a >>, tak i například již zmíněný znak |. Pouze rozšíříme třídu Infix o další dvě speciální metody __ror__ a __or__:

# Vlastní infixové operátory v Pythonu
#
# - třída Infix umožňující definici jakéhokoli binárního operátoru
# - definice několika operátorů
# - otestování nových operátorů


class Infix:
 
    def __init__(self, function):
        self.function = function
 
    def __rlshift__(self, other):
        # uzávěr
        return Infix(lambda x, self=self, other=other: self.function(other, x))
 
    def __rshift__(self, other):
        # levá hodnota je již dostupná v uzávěru
        return self.function(other)
 
    def __ror__(self, other):
        return self.__rlshift__(other)
 
    def __or__(self, other):
        return self.__rshift__(other)
 
 
# operátor součinu
print("multiply:")
 
x=Infix(lambda x,y: x*y)
print(2 |x| 4)
 
print("-" * 20)
 
# operátor testu typu hodnoty
print("'is a' and 'instance of':")
isa=Infix(lambda x,y: x.__class__==y.__class__)
print([1,2,3] |isa| [])
 
# operátor testu, zda je hodnota instancí třídy
instance_of=Infix(lambda x,y: isinstance(x, y))
print(1 |instance_of| int)
print(1 |instance_of| str)
print("foo" |instance_of| str)
 
print(1 |instance_of| int and "foo" |instance_of| str)
 
print("-" * 20)
 
print("has attr:")
has_attr=Infix(lambda x,y: hasattr(x, y))
print(str |has_attr| "tolower")
print(str |has_attr| "tolowercase")
print(str |has_attr| "lower")
print(str |has_attr| "__doc__")
 
print("-" * 20)
 
print("has method:")
has_method=Infix(lambda x,y: hasattr(x, y) and callable(getattr(x, y, None)))
print(str |has_method| "tolower")
print(str |has_method| "tolowercase")
print(str |has_method| "lower")
print(str |has_method| "__doc__")
 
print("-" * 20)
 
print("is in:")
is_in=Infix(lambda x,y: x in y)
print(1 |is_in| {1:'one'})
print(1 |is_in| {1:'one'})
 
print("-" * 20)
 
print("gcd:")
 
import math
gcd=Infix(lambda x,y: math.gcd(x, y))
print(12 |gcd| 8)
 
 
print("div:")
 
import operator
div=Infix(operator.floordiv)
print(10 |div| (4 |div| 2))

Výsledky by přitom měly být stejné, jako tomu bylo v předchozím demonstračním příkladu.

17. Které znaky je možné použít v definovaných operátorech

Možná se na tomto místě můžete zeptat, že když už Python „ohýbáme“ do takové míry, že si definujeme vlastní (pseudo)operátory, jestli je možné tyto operátory pojmenovat s využitím speciálních Unicode symbolů, tedy například symbolem ×, ∃, ∊ atd. V původním Pythonu 2 to nebylo vůbec možné (byli jsme omezeni ASCII znaky), ovšem v Pythonu 3 můžeme využít alespoň některé znaky (ovšem většinu z nich nikoli na první pozici identifikátoru):

Z tabulky Unicode znaků lze pro první znak identifikátoru vybírat ze skupin:

Lu - uppercase letters
Ll - lowercase letters
Lt - titlecase letters
Lm - modifier letters
Lo - other letters
Poznámka: zde nalezneme alespoň některé zajímavé znaky, ale zdaleka ne všechny.

Pro druhý a další znak se navíc jedná o skupiny:

Nl - letter numbers
Mn - nonspacing marks
Mc - spacing combining marks
Nd - decimal numbers
Pc - connector punctuations

18. Možné problémy

Řešení nebo možná lépe řečeno hack, který byl v tomto článku popsán, může být v mnoha ohledech problematické, zejména tehdy, pokud nedodržíme přesný zápis operátoru (což je vlastně výraz se dvěma operátory). Problémem jsou zde kryptická chybová hlášení. Taktéž je zapotřebí si dát velký pozor na prioritu operátorů, protože náš „složený operátor“ může mít menší prioritu, než další části výrazu:

bitcoin_smenarna

o = DotProduct()
x = [1, 2, 3]
y = [3, 4, 5]
 
print(x + y |o| y + x)

Tento skript vypíše hodnotu 52, protože se provede:

print((x + y) |o| (y + x))

19. Repositář s demonstračními příklady

Všechny demonstrační příklady, které jsme si dnes ukázali, jsou uloženy 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 infix1.py třída HackyMul umožňující definici operátoru typu <<x>> apod. s výpisem ladicích informací https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix1.py
2 infix2.py třída HackyMul umožňující definici operátoru typu <<x>> apod. bez výpisu ladicích informací https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix2.py
3 infix3.py realizace nového operátoru pro skalární součin, založeno na operátorech << a >> https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix3.py
4 infix4.py realizace nového operátoru pro skalární součin, založeno na operátoru | https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix4.py
5 infix5.py realizace nového operátoru pro skalární součin, založeno na operátorech < a > (nekorektní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix5.py
6 infix6.py realizace nového operátoru pro skalární součin, založeno na operátorech < a > (korektní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix6.py
7 infix7.py implementace třídy pro konstrukci vlastních operátorů (jednodušší varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix7.py
8 infix8.py implementace třídy pro konstrukci vlastních operátorů (nepatrně složitější varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/infix8.py
9 complex.py implementace komplexních čísel i s přetížením vybraných operátorů https://github.com/tisnik/most-popular-python-libs/blob/master/custom_o­perators/complex.py

20. Odkazy na Internetu

  1. Python: defining my own operators?
    https://stackoverflow.com/qu­estions/932328/python-defining-my-own-operators
  2. Infix operators (Python recipe)
    https://web.archive.org/web/20220528202902/htt­ps://code.activestate.com/re­cipes/384122/
  3. Sage: decorators
    https://doc.sagemath.org/html/en/re­ference/misc/sage/misc/de­corators.html
  4. make a custom infix operator decorator
    https://github.com/sagemat­h/sage/issues/6245
  5. Creating Custom Operators in Python 3
    https://dnmtechs.com/creating-custom-operators-in-python-3/
  6. Allowed characters in Python function names
    https://stackoverflow.com/qu­estions/19482730/allowed-characters-in-python-function-names
  7. List of Unicode Characters of Category “Other Letter”
    https://www.compart.com/en/u­nicode/category/Lo
  8. Python Unicode Variable Names
    https://www.asmeurer.com/python-unicode-variable-names/
  9. Start characters
    https://www.asmeurer.com/python-unicode-variable-names/start-characters.md
  10. Continue characters
    https://www.asmeurer.com/python-unicode-variable-names/continue-characters.md
  11. Special Methods
    https://www.pythonlikeyou­meanit.com/Module4_OOP/Spe­cial_Methods.html
  12. 3. Data model
    https://docs.python.org/3/re­ference/datamodel.html
  13. Special Method Names
    https://diveintopython3.net/special-method-names.html
  14. Full list of Python special method names?
    https://stackoverflow.com/qu­estions/51562492/full-list-of-python-special-method-names
  15. Python special methods
    https://pythonforstarters­.solomonmarvel.com/object-oriented-programming-in-python/python-special-methods
  16. Python's Magic Methods: Leverage Their Power in Your Classes
    https://realpython.com/python-magic-methods/
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.