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