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

← Previous publications.

The order of except blocks matter: if exceptions can be caught by more than one block, the higher block applies. The following code doesn’t work as intended:

import logging

def get(storage, key, default):
    try:
        return storage[key]
    except LookupError:
        return default
    except IndexError:
        return get(storage, 0, default)
    except TypeError:
        logging.exception('unsupported key')
        return default

print(get([1], 0, 42))  # 1
print(get([1], 10, 42))  # 42
print(get([1], 'x', 42))  # error msg, 42

except IndexError never works since IndexError is a subclass of LookupError. More concrete exception should always be higher:

import logging

def get(storage, key, default):
    try:
        return storage[key]
    except IndexError:
        return get(storage, 0, default)
    except LookupError:
        return default
    except TypeError:
        logging.exception('unsupported key')
    return default

print(get([1], 0, 42))  # 1
print(get([1], 10, 42))  # 1
print(get([1], 'x', 42))  # error msg, 42

Python supports parallel assignment meaning that all variables are modified at once after all expressions are evaluated. Moreover, you can use any expression that supports assignment, not only variables:

def shift_inplace(lst, k):
    size = len(lst)
    lst[k:], lst[0:k] = lst[0:-k], lst[-k:]

lst = list(range(10))

shift_inplace(lst, -3)
print(lst)
# [3, 4, 5, 6, 7, 8, 9, 0, 1, 2]

shift_inplace(lst, 5)
print(lst)
# [8, 9, 0, 1, 2, 3, 4, 5, 6, 7]

Python substitution does not fallback to addition with negative value. Consider the example:

class Velocity:
    SPEED_OF_LIGHT = 299_792_458

    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):
        return type(self)(
            (self.amount + other.amount) /
            (
                1 +
                self.amount * other.amount /
                self.SPEED_OF_LIGHT ** 2
            )
        )

    def __neg__(self):
        return type(self)(-self.amount)

    def __str__(self):
        amount = int(self.amount)
        return f'{amount} m/s'

That doesn’t work:

v1 = Velocity(20_000_000)
v2 = Velocity(10_000_000)

print(v1 - v2)
# TypeError: unsupported operand type(s) for -: 'Velocity' and 'Velocity

Funny enough, that does:

v1 = Velocity(20_000_000)
v2 = Velocity(10_000_000)

print(v1 +- v2)
# 10022302 m/s

Today’s post is written by Telegram-user orsinium.

Function can’t be generator and regular function at the same time. If yield is presented anywhere in the function body, the function turns into generator:

def zeros(*, count: int, lazy: bool):
        if lazy:
            for _ in range(count):
                yield 0
            else:
                return [0] * count

zeros(count=10, lazy=True)
# <generator object zeros at 0x7ff0062f2a98>

zeros(count=10, lazy=False)
# <generator object zeros at 0x7ff0073da570>

list(zeros(count=10, lazy=False))
# []

However, regular function can return another iterator:

def _lazy_zeros(*, count: int):
    for _ in range(count):
        yield 0
    
def zeros(*, count: int, lazy: bool):
    if lazy:
        return _lazy_zeros(count=count)
    return [0] * count

zeros(count=10, lazy=True)
# <generator object _lazy_zeros at 0x7ff0062f2750>

zeros(count=10, lazy=False)
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Also, for simple cases generator expressions could be useful:

def zeros(*, count: int, lazy: bool):
    if lazy:
        return (0 for _ in range(count))
    return [0] * count

Brackets are required to create a generator comprehension:

>>> g = x**x for x in range(10)
    File "<stdin>", line 1
        g = x**x for x in range(10)
            ^
SyntaxError: invalid syntax
>>> g = (x**x for x in range(10))
>>> g
<generator object <genexpr> at 0x7f90ed650258>

However they can be omitted if a generator comprehension is the only argument for the function:

>>> list((x**x for x in range(4)))
[1, 1, 4, 27]
>>> list(x**x for x in range(4))
[1, 1, 4, 27]

That doesn’t work for function with more than one argument:

>>> print((x**x for x in range(4)), end='n')
<generator object <genexpr> at 0x7f90ed650468>
>>>
>>>
>>> print(x**x for x in range(4), end='n')
    File "<stdin>", line 1
SyntaxError: Generator expression must be parenthesized if not sole argument