Mail.ru Group corporate blog
Python
Programming
June 4

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 as b.__gt__(a)
  • a.__le__(b) is the same as b.__ge__(a)
  • a.__eq__(b) is the same as not a.__ne__(b) (mind that a and b 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__ 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 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
+27
828 4
Leave a comment