Tips and tricks from my Telegram-channel @pythonetc, May 2019
It is a new selection of tips and tricks about Python and programming from my Telegram-channel @pythonetc.
← Previous publications
break
statement suppresses exception if used in the finally
clause even when the except
block is not presented:for i in range(10):
try:
1 / i
finally:
print('finally')
break
print('after try')
print('after while')
Output:
finally
after while
The same is true for
continue
, however it can’t be used in finally
until Python 3.8:SyntaxError: 'continue' not supported inside 'finally' clause
You can add Unicode characters in a string literal not only by its number, but by also by its name.
>>> '\N{EM DASH}'
'—'
>>> '\u2014'
'—'
It’s also compatible with f-strings:
>>> width = 800
>>> f'Width \N{EM DASH} {width}'
'Width — 800'
There are six magic methods for Python objects that define comparison rules:
__lt__
for<
__gt__
for>
__le__
for<=
__ge__
for>=
__eq__
for==
__ne__
for!=
If some of these methods are not defined or return
NotImplemented
, the following rules applied:a.__lt__(b)
is the same asb.__gt__(a)
a.__le__(b)
is the same asb.__ge__(a)
a.__eq__(b)
is the same asnot a.__ne__(b)
(mind thata
andb
are not swapped in this case)
However,
a >= b
and a != b
don’t automatically imply a > b
. The functools.total_ordering
decorator create all six methods based on __eq__
and one of the following: __lt__
, __gt__
, __le__
, or __ge__
.from functools import total_ordering
@total_ordering
class User:
def __init__(self, pk, name):
self.pk = pk
self.name = name
def __le__(self, other):
return self.pk <= other.pk
def __eq__(self, other):
return self.pk == other.pk
assert User(2, 'Vadim') < User(13, 'Catherine')
Sometimes you want to use both decorated and undecorated versions of a function. The easiest way to achieve that is to forgo the special decorator syntax (the one with
@
) and create the decorated function manually:import json
def ensure_list(f):
def decorated(*args, **kwargs):
result = f(*args, **kwargs)
if isinstance(result, list):
return result
else:
return [result]
return decorated
def load_data_orig(string):
return json.loads(string)
load_data = ensure_list(load_data_orig)
print(load_data('3')) # [3]
print(load_data_orig('4')) 4
Alternatively, you can write another decorator, that decorate a function while preserving its original version in the
orig
attribute of the new one:import json
def saving_orig(another_decorator):
def decorator(f):
decorated = another_decorator(f)
decorated.orig = f
return decorated
return decorator
def ensure_list(f):
...
@saving_orig(ensure_list)
def load_data(string):
return json.loads(string)
print(load_data('3')) # [3]
print(load_data.orig('4')) # 4
If all decorators you are working with are created via
functools.wraps
you can use the __wrapped__
attribute to access the undecorated function:import json
from functools import wraps
def ensure_list(f):
@wraps(f)
def decorated(*args, **kwargs):
result = f(*args, **kwargs)
if isinstance(result, list):
return result
else:
return [result]
return decorated
@ensure_list
def load_data(string):
return json.loads(string)
print(load_data('3')) # [3]
print(load_data.__wrapped__('4')) # 4
Mind, however, that it doesn’t work for functions that are decorated by more than one decorator: you have to access
__wrapped__
for each decorator applied:def ensure_list(f):
...
def ensure_ints(f):
@wraps(f)
def decorated(*args, **kwargs):
result = f(*args, **kwargs)
return [int(x) for x in result]
return decorated
@ensure_ints
@ensure_list
def load_data(string):
return json.loads(string)
for f in (
load_data,
load_data.__wrapped__,
load_data.__wrapped__.__wrapped__,
):
print(repr(f('"4"')))
Output:
[4]
['4']
'4'
The
@saving_orig
mentioned above accepts another decorator as an argument. What if that decorator can be parametrized? Well, since parameterized decorator is a function that returns an actual decorator, this case is handled automatically:import json
from functools import wraps
def saving_orig(another_decorator):
def decorator(f):
decorated = another_decorator(f)
decorated.orig = f
return decorated
return decorator
def ensure_ints(*, default=None):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
result = f(*args, **kwargs)
ints = []
for x in result:
try:
x_int = int(x)
except ValueError:
if default is None:
raise
else:
x_int = default
ints.append(x_int)
return ints
return decorated
return decorator
@saving_orig(ensure_ints(default=0))
def load_data(string):
return json.loads(string)
print(repr(load_data('["2", "3", "A"]')))
print(repr(load_data.orig('["2", "3", "A"]')))
The
@saving_orig
decorator doesn’t really do what we want if there are more than one decorator applied to a function. We have to call orig
for each such decorator:import json
from functools import wraps
def saving_orig(another_decorator):
def decorator(f):
decorated = another_decorator(f)
decorated.orig = f
return decorated
return decorator
def ensure_list(f):
...
def ensure_ints(*, default=None):
...
@saving_orig(ensure_ints(default=42))
@saving_orig(ensure_list)
def load_data(string):
return json.loads(string)
for f in (
load_data,
load_data.orig,
load_data.orig.orig,
):
print(repr(f('"X"')))
Output:
[42]
['X']
'X'
We can fix it by supporting arbitrary number of decorators as
saving_orig
arguments:def saving_orig(*decorators):
def decorator(f):
decorated = f
for d in reversed(decorators):
decorated = d(decorated)
decorated.orig = f
return decorated
return decorator
...
@saving_orig(
ensure_ints(default=42),
ensure_list,
)
def load_data(string):
return json.loads(string)
for f in (
load_data,
load_data.orig,
):
print(repr(f('"X"')))
Output:
[42]
'X'
Another solution is to make
saving_orig
smart enough to pass orig
from one decorated function to another:def saving_orig(another_decorator):
def decorator(f):
decorated = another_decorator(f)
if hasattr(f, 'orig'):
decorated.orig = f.orig
else:
decorated.orig = f
return decorated
return decorator
@saving_orig(ensure_ints(default=42))
@saving_orig(ensure_list)
def load_data(string):
return json.loads(string)
If a decorator you are writing becomes too complicated, it may be reasonable to transform it from a function to a class with the
__call__
methodclass SavingOrig:
def __init__(self, another_decorator):
self._another = another_decorator
def __call__(self, f):
decorated = self._another(f)
if hasattr(f, 'orig'):
decorated.orig = f.orig
else:
decorated.orig = f
return decorated
saving_orig = SavingOrig
The last line allows you both to name class with camel case and keep the decorator name in snake case.
Instead of modifying the decorated function you can create another callable class to return its instances instead of a function:
class CallableWithOrig:
def __init__(self, to_call, orig):
self._to_call = to_call
self._orig = orig
def __call__(self, *args, **kwargs):
return self._to_call(*args, **kwargs)
@property
def orig(self):
if isinstance(self._orig, type(self)):
return self._orig.orig
else:
return self._orig
class SavingOrig:
def __init__(self, another_decorator):
self._another = another_decorator
def __call__(self, f):
return CallableWithOrig(self._another(f), f)
saving_orig = SavingOrig
View the whole code here