📜 ⬆️ ⬇️

Selection @pythonetc, May 2019



This is the eleventh collection of Python tips and programming from my author's @pythonetc channel.

Previous collections


The break expression blocks an exception if it is used in a finally block, even in the absence of the except block:
')
 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 , but this expression can be used in finally only up to 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-strings:

 >>> width = 800 >>> f'Width \N{EM DASH} {width}' 'Width — 800' 


For Python objects, there are six "magic" methods that define comparison rules:


If some of these methods are not defined or return NotImplemented , then the following rules apply:


However, the conditions a >= b and a != b do not automatically mean that a > b . The functools.total_ordering decorator creates all six methods based on __eq__ and one of these: __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 need to use both a decorated and non-decorated version of a function. The easiest way to achieve this is if you do not use a special decorating syntax (with the @ symbol) and create the decorating 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 

Or you can write a decorator who decorates a function, while maintaining the original version in its orig attribute:

 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 your decorators are created via functools.wraps , then you can use the __wrapped__ attribute to access the non-decorated 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 

But remember that this approach does not work for functions that are decorated by more than one decorator: you will have to contact __wrapped__ each of the decorators used:

 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"'))) 

Result:

 [4] ['4'] '4' 

The above decorator @saving_orig takes another decorator as an argument. And if it is parameterized? Since the parameterized decorator is a function that returns a real decorator, this situation is automatically handled:

 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 @saving_orig 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 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"'))) 

Result:

 [42] ['X'] 'X' 

You can saving_orig this by supporting an arbitrary number of decorators as arguments for saving_orig :

 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"'))) 

Result:

 [42] 'X' 

There is another solution: make saving_orig transfer 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) 

When the decorator becomes too complicated, it is better to convert it from a function to a class with the __call__ method:

 class 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 to name the class in Camel case and save the decorator name in Snake case.

Instead of converting a decorated function, you can create another callable class, and return instances of it 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 

All code is available here.

Source: https://habr.com/ru/post/454646/


All Articles