@Pythonetc compilation, May 2019
This is the eleventh selection of Python tips and programming from my @pythonetc feed.
← Previous collections
The expression
break
blocks the exception if applied in a block finally
, even if there is no block except
:for i in range(10):
try:
1 / i
finally:
print('finally')
break
print('after try')
print('after while')
Result:
finally
after while
The same is true for
continue
, however, this expression can finally
only be applied before Python version 3.8:SyntaxError: 'continue'not supported inside 'finally' clause
You can add Unicode characters to string literals not only by their indexes, but also by name.
>>> '\N{EM DASH}''—'>>> '\u2014''—'
This method is also compatible with f-lines:
>>> width = 800>>> f'Width \N{EM DASH}{width}''Width — 800'
There are six “magic” methods for Python objects that define the rules for comparison:
__lt__
for<
__gt__
for>
__le__
for<=
__ge__
for>=
__eq__
for==
__ne__
for!=
If any of these methods are not defined or returned
NotImplemented
, then the following rules apply:a.__lt__(b)
same asb.__gt__(a)
a.__le__(b)
same asb.__ge__(a)
a.__eq__(b)
the same thingnot a.__ne__(b)
(note that in this case,a
andb
not swapped)
However, the conditions
a >= b
, and a != b
does not automatically mean that a > b
. Decorator functools.total_ordering
creates all six methods based on __eq__
and one of these: __lt__
, __gt__
, __le__
or __ge__
.from functools import total_ordering
@total_ordering classUser: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 need to use both the decorated and non-decorated versions of the function. The easiest way to achieve this is if you do not use a special decorating syntax (with a symbol
@
) and create a decorating function manually:import json
defensure_list(f):defdecorated(*args, **kwargs):
result = f(*args, **kwargs)
if isinstance(result, list):
return result
else:
return [result]
return decorated
defload_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
Or you can write a decorator that decorates the function, while preserving the
orig
original version in its attribute :import json
defsaving_orig(another_decorator):defdecorator(f):
decorated = another_decorator(f)
decorated.orig = f
return decorated
return decorator
defensure_list(f):
...
@saving_orig(ensure_list)defload_data(string):return json.loads(string)
print(load_data('3')) # [3]
print(load_data.orig('4')) # 4
If all your decorators are created through
functools.wraps
, then you can use the attribute __wrapped__
to access an undecorated function:import json
from functools import wraps
defensure_list(f): @wraps(f)defdecorated(*args, **kwargs):
result = f(*args, **kwargs)
if isinstance(result, list):
return result
else:
return [result]
return decorated
@ensure_listdefload_data(string):return json.loads(string)
print(load_data('3')) # [3]
print(load_data.__wrapped__('4')) # 4
But remember that this approach does not work for functions that are decorated with more than one decorator: you will have to refer to
__wrapped__
each of the applied decorators:defensure_list(f):
...
defensure_ints(f): @wraps(f)defdecorated(*args, **kwargs):
result = f(*args, **kwargs)
return [int(x) for x in result]
return decorated
@ensure_ints@ensure_listdefload_data(string):return json.loads(string)
for f in (
load_data,
load_data.__wrapped__,
load_data.__wrapped__.__wrapped__,
):
print(repr(f('"4"')))
Result:
[4]
['4']
'4'
The decorator mentioned above
@saving_orig
takes another decorator as an argument. And if it will be parameterized? Since a parameterized decorator is a function that returns a real decorator, this situation is processed automatically:import json
from functools import wraps
defsaving_orig(another_decorator):defdecorator(f):
decorated = another_decorator(f)
decorated.orig = f
return decorated
return decorator
defensure_ints(*, default=None):defdecorator(f): @wraps(f)defdecorated(*args, **kwargs):
result = f(*args, **kwargs)
ints = []
for x in result:
try:
x_int = int(x)
except ValueError:
if default isNone:
raiseelse:
x_int = default
ints.append(x_int)
return ints
return decorated
return decorator
@saving_orig(ensure_ints(default=0))defload_data(string):return json.loads(string)
print(repr(load_data('["2", "3", "A"]')))
print(repr(load_data.orig('["2", "3", "A"]')))
The decorator
@saving_orig
will not do what we want if several decorators are applied to the function. Then for each of them you have to call orig
:import json
from functools import wraps
defsaving_orig(another_decorator):defdecorator(f):
decorated = another_decorator(f)
decorated.orig = f
return decorated
return decorator
defensure_list(f):
...
defensure_ints(*, default=None):
...
@saving_orig(ensure_ints(default=42))@saving_orig(ensure_list)defload_data(string):return json.loads(string)
for f in (
load_data,
load_data.orig,
load_data.orig.orig,
):
print(repr(f('"X"')))
Result:
[42]
['X']
'X'
This can be fixed by supporting an arbitrary number of decorators as arguments
saving_orig
:defsaving_orig(*decorators):defdecorator(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,
)
defload_data(string):return json.loads(string)
for f in (
load_data,
load_data.orig,
):
print(repr(f('"X"')))
Result:
[42]
'X'
There is another solution: make it
saving_orig
pass orig
from one decorated function to another:defsaving_orig(another_decorator):defdecorator(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)defload_data(string):return json.loads(string)
When the decorator gets too complicated, it is best to convert it from a function to a class with a method
__call__
:classSavingOrig: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 to name the class in the Camel case and save the decorator name in the Snake case.
Instead of converting the decorated function, you can create another called class, and return instances of the function instead of the function:
classCallableWithOrig: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)
@propertydeforig(self):if isinstance(self._orig, type(self)):
return self._orig.orig
else:
return self._orig
classSavingOrig:def__init__(self, another_decorator):
self._another = another_decorator
def__call__(self, f):return CallableWithOrig(self._another(f), f)
saving_orig = SavingOrig
All code is available here.