## Konteksti

Pythonov modul `contextlib` - ki ima, kot bi bilo bistremu človeku jasno že iz imena, nekaj opraviti s konteksti - ima funkcijo `chdir`, s katero lahko začasno zamenjamo trenutni delovni direktorij.

    import os
    import contextlib

    print(os.getcwd())

    with contextlib.chdir("/Users/janez/Downloads"):
        print(os.getcwd())

    print(os.getcwd())

    /Users/janez/Desktop/predavanja/p1/drobnarije
    /Users/janez/Downloads
    /Users/janez/Desktop/predavanja/p1/drobnarije

To je uporabno. Če tega ne bi bilo, bi se morali narediti sami. (Eni smo si že: `contextlib.chdir` obstaja šele od Pythona 3.11.)

Da naredimo upravljalec konteksta (context manager) moramo poznati bodisi razrede bodisi generatorje in dekoratorje. Razrede nekateri že poznate, o generatorjih govorimo v ločenem zapisku. V teh zapiskih pokažimo oba načina. Prvi je poučnejši, drugi prikladnejši.

## Konteksti z razredom

Upravljalec konteksta je razred, ki ima metodi `__enter__` in `__exit__`. `with` pokliče pred vstopom v blok in drugo po izhodu. Prva ima le en argument, `self` (ekvivalent `this`-a v nekaterih drugih jezikih; v Pythonu ga je potrebno eksplicitno navesti med argumenti), druga pa ima tri argumente, ki povedo, ali je bila znotraj bloka sprožena izjema (exception) ter kakšna in kje.

    class NotInVen:
        def __enter__(self):
            print("vstopamo")

        def __exit__(self, exc_type, exc_val, exc_tb):
            print("izstopamo")

    print("začetek")

    with NotInVen():
        print("v bloku")

    print("konec")

    začetek
    vstopamo
    v bloku
    izstopamo
    konec

To je to. To je že (skoraj) vsa znanost. Ostane le še, da lahko `__enter__` vrne kako vrednost; v `with` jo z `as` priredimo imenu. Takole.

    class NotInVen:
        def __enter__(self):
            print("vstopamo")
            return 42

        def __exit__(self, exc_type, exc_val, exc_tb):
            print("izstopamo")

    print("začetek")

    with NotInVen() as x:
        print("v bloku")
        print("x =", x)

    print("konec")

    začetek
    vstopamo
    v bloku
    x = 42
    izstopamo
    konec

### Začasna menjava direktorija

Upravljalec konteksta, ki zamenja trenutni delovni direktorij znotraj bloka, je trivialna zadeva. Če znamo napisati razred v Pythonu, seve.

    import os

    class MojChDir:
        def __init__(self, dir):
            self.dir = dir
            self.saved = None

        def __enter__(self):
            self.saved = os.getcwd()
            os.chdir(self.dir)

        def __exit__(self, exc_type, exc_val, exc_tb):
            os.chdir(self.saved)

    print(os.getcwd())

    with MojChDir("/Users/janez/Downloads"):
        print(os.getcwd())

    print(os.getcwd())

    /Users/janez/Desktop/predavanja/p1/drobnarije
    /Users/janez/Downloads
    /Users/janez/Desktop/predavanja/p1/drobnarije

### Merjenje časa

Za še en primer napišimo kontekst, ki pove, koliko časa se izvaja blok.

    import time

    class Timer:
        def __enter__(self):
            self.start = time.time()

        def __exit__(self, exc_type, exc_val, exc_tb):
            print("Elapsed time: ", time.time() - self.start)

Pa pomerimo, če Python prav spi.

    with Timer():
        time.sleep(1.5)

    Elapsed time:  1.505854845046997

Kar v redu.

### Začasno spreminjanje funkcij

    class SupressPrint:
        def __enter__(self):
            global print
            self.old_print = print

            def print(*args, **kwargs):
                pass

        def __exit__(self, *_):
            global print
            print = self.old_print

    print("Pišem")

    with SupressPrint():
        print("Ne pišem.")

    print("Spet pišem.")

    Pišem
    Spet pišem.

To ne bo delovalo vedno; če ta razred uvozite iz modula, ne bo imel učinka, ker `global` ne deluje čisto tako, kot si (najbrž) predstavljate, da deluje.

Kaj pa tole?

    class TimePrint:
        def __enter__(self):
            global print
            self.old_print = print

            def print(*args, **kwargs):
                self.old_print(f"{time.time()}:", *args, **kwargs)

        def __exit__(self, *_):
            global print
            print = self.old_print

    import time
    import random

    with TimePrint():
        print("Začetek")
        x = 1 + random.random()
        time.sleep(x)
        print("in čez", round(x, 2), "sekund")

    print("Zdaj sem pa spet normalen.")

    1734281983.302903: Začetek
    1734281984.607671: in čez 1.3 sekund
    Zdaj sem pa spet normalen.

### Kako se zapirajo datoteke?

Čisto preprosto. Datoteke imajo poleg vseh `read`-ov in `readline`-ov in `write`-ov še metodi `__enter__` in `__exit__`, ki delata približno (ali celo točno) tole:

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            self.close()

Elegantno, ni?

## Konteksti z generatorjem

Da nam ne bi bilo potrebno pisati razredov, lahko kontekste pišemo tudi s funkcijami. Točneje, generatorskimi funkcijami, ki morajo vsebovati natančno en `yield`. Kar je pred `yield`-om se zgodi pred vstopom v blok, kar po njem, ob izhodu iz bloka. Vrednost, ki jo generiramo, pa je rezultat metode `__enter__`. Tako zapisan kontekst je potrebno dekorirati s `contextlib.contextmanager`, tako da pred definicijo funkcije dodamo vrstico `@contextlib.contextmanager`.

Napišimo, recimo, začasno spremembo direktorija.

    import contextlib

    @contextlib.contextmanager
    def moj_chdir(dir):
        old_dir = os.getcwd()
        os.chdir(dir)
        yield
        os.chdir(old_dir)

    print(os.getcwd())

    with moj_chdir("/Users/janez/Downloads"):
        print(os.getcwd())

    print(os.getcwd())

    /Users/janez/Desktop/predavanja/p1/drobnarije
    /Users/janez/Downloads
    /Users/janez/Desktop/predavanja/p1/drobnarije

Tako je še veliko preprosteje, ni?

Če delamo tako reč čisto zares, je prav, da poskrbimo še za izjeme (exception). Detajle preberite v dokumentaciji.

### Črna magija

Pythonovi konteksti vedno uporabljajo razrede. `contextlib.contextmanager` je samo funkcija, ki sama sestavi razred namesto nas. Ljubitelji črne magije naj raziščejo, zakaj tale funkcija dela isto kot `contextlib.contextmanager`.

    def moj_contextmanager(f):
        class cm:
            def __init__(self, *args, **kwargs):
                self.g = f(*args, **kwargs)

            def __enter__(self):
                return next(self.g)

            def __exit__(self, *_):
                try:
                    next(self.g)
                except StopIteration:
                    pass

        return cm

    import contextlib

    @moj_contextmanager
    def moj_chdir(dir):
        old_dir = os.getcwd()
        os.chdir(dir)
        yield
        os.chdir(old_dir)

    print(os.getcwd())

    with moj_chdir("/Users/janez/Downloads"):
        print(os.getcwd())

    print(os.getcwd())

    /Users/janez/Desktop/predavanja/p1/drobnarije
    /Users/janez/Downloads
    /Users/janez/Desktop/predavanja/p1/drobnarije