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

    Also popular now: