It is a new selection of tips and tricks about Python and programming from my Telegram-channel @pythonetc.

< Previous publications


You can’t mutate closure variables by simply assigning them. Python treats assignment as a definition inside a function body and doesn’t make closure at all.

Works fine, prints 2:

def make_closure(x):
    def closure():
        print(x)

    return closure

make_closure(2)

Throws UnboundLocalError: local variable 'x' referenced before assignment:

def make_closure(x):
    def closure():
        print(x)
        x *= 2
        print(x)

    return closure

make_closure(2)()


To make it work you should use nonlocal. It explicitly tells the interpreter not to treat assignment as a definition:

def make_closure(x):
    def closure():
        nonlocal x
        print(x)
        x *= 2
        print(x)

    return closure

make_closure(2)()


Sometimes during iteration you may want to know whether it’s the first or the last element step of the iteration. Simple way to handle this is to use explicit flag:

def sparse_list(iterable, num_of_zeros=1):
    result = []
    zeros = [0 for _ in range(num_of_zeros)]

    first = True
    for x in iterable:
        if not first:
            result += zeros
        result.append(x)

        first = False

    return result

assert sparse_list([1, 2, 3], 2) == [
    1,
    0, 0,
    2,
    0, 0,
    3,
]

You also could process the first element outside of the loop, that may seem more clear but leads to code duplication to the certain extent. It is also not a simple thing to do while working with abstract iterables:

def sparse_list(iterable, num_of_zeros=1):
    result = []
    zeros = [0 for _ in range(num_of_zeros)]

    iterator = iter(iterable)
    try:
        result.append(next(iterator))
    except StopIteration:
        return []

    for x in iterator:
       result += zeros
       result.append(x)

    return result

You also could use enumerate and check for the i == 0 (works only for the detection of the first element, not the last one), but the ultimate solution might be a generator that returns first and last flags along with the element of an iterable:

def first_last_iter(iterable):
    iterator = iter(iterable)

    first = True
    last = False
    while not last:
    if first:
        try:
            current = next(iterator)
            except StopIteration:
                return
    else:
        current = next_one

    try:
        next_one = next(iterator)
    except StopIteration:
        last = True

    yield (first, last, current)

    first = False

The initial function now may look like this:

def sparse_list(iterable, num_of_zeros=1):
    result = []
    zeros = [0 for _ in range(num_of_zeros)]

    for first, last, x in first_last_iter(iterable):
        if not first:
            result += zeros
        result.append(x)

    return result


If you want to measure time between two events you should use time.monotonic() instead of time.time(). time.monotonic() never goes backwards even if system clock is updated:

from contextlib import contextmanager
import time


@contextmanager
def timeit():
    start = time.monotonic()
    yield
    print(time.monotonic() - start)

def main():
    with timeit():
           time.sleep(2)

main()


Nested context managers normally don’t know that they are nested. You can make them know by spawning inner context managers by the outer one:

from contextlib import AbstractContextManager
import time


class TimeItContextManager(AbstractContextManager):
    def __init__(self, name, parent=None):
        super().__init__()

        self._name = name
        self._parent = parent
        self._start = None
        self._substracted = 0

    def __enter__(self):
        self._start = time.monotonic()
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        delta = time.monotonic() - self._start
        if self._parent is not None:
            self._parent.substract(delta)

    print(self._name, 'total', delta)
    print(self._name, 'outer', delta - self._substracted)

    return False

    def child(self, name):
        return type(self)(name, parent=self)

    def substract(self, n):
        self._substracted += n


timeit = TimeItContextManager


def main():
    with timeit('large') as large_t:
        with large_t.child('medium') as medium_t:
            with medium_t.child('small-1'):
                time.sleep(1)
            with medium_t.child('small-2'):
                time.sleep(1)
        time.sleep(1)
    time.sleep(1)


main()


If you want to pass some information down the call chain, you usually use the most straightforward way possible: you pass it as functions arguments.

However, in some cases, it may be highly inconvenient to modify all functions in the chain to propagate some new piece of data. Instead, you may want to set up some kind of context to be used by all functions down the chain. How can this context be technically done?

The simplest solution is a global variable. In Python, you also may use modules and classes as context holders since they are, strictly speaking, global variables too. You probably do it on a daily basis for things like loggers.

If your application is multi-threaded, a bare global variable won't work for you since they are not thread-safe. You may have more than one call chain running at the same time, and each of them needs its own context. The threading module gets you covered, it provides the threading.local() object that is thread-safe. Store there any data by simply accessing attributes: threading.local().symbol = '@'.

Still, both of that approaches are concurrency-unsafe meaning they won't work for coroutine call-chain where functions are not only called but can be awaited too. Once a coroutine does await, an event loop may run a completely different coroutine from a completely different chain. That won't work:

import asyncio
import sys

global_symbol = '.'

async def indication(timeout):
    while True:
        print(global_symbol, end='')
        sys.stdout.flush()
        await asyncio.sleep(timeout)

async def sleep(t, indication_t, symbol='.'):
    loop = asyncio.get_event_loop()

    global global_symbol
    global_symbol = symbol
    task = loop.create_task(
            indication(indication_t)
    )
    await asyncio.sleep(t)
    task.cancel()

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
    sleep(1, 0.1, '0'),
    sleep(1, 0.1, 'a'),
    sleep(1, 0.1, 'b'),
    sleep(1, 0.1, 'c'),
))

You can fix that by having the loop set and restore the context every time it switches between coroutines. You can do it with the contextvars module since Python 3.7.

import asyncio
import sys
import contextvars

global_symbol = contextvars.ContextVar('symbol')

async def indication(timeout):
    while True:
        print(global_symbol.get(), end='')
        sys.stdout.flush()
        await asyncio.sleep(timeout)

async def sleep(t, indication_t, symbol='.'):
    loop = asyncio.get_event_loop()

    global_symbol.set(symbol)
    task = loop.create_task(indication(indication_t))
    await asyncio.sleep(t)
    task.cancel()

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
    sleep(1, 0.1, '0'),
    sleep(1, 0.1, 'a'),
    sleep(1, 0.1, 'b'),
    sleep(1, 0.1, 'c'),
))

Комментарии (1)