
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 thataandbare 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
