@Pythonetc compilation, May 2019



    This is the eleventh selection of Python tips and programming from my @pythonetc feed.

    Previous collections


    The expression breakblocks 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 finallyonly 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 as b.__gt__(a)
    • a.__le__(b) same as b.__ge__(a)
    • a.__eq__(b)the same thing not a.__ne__(b)(note that in this case, aand bnot swapped)

    However, the conditions a >= b, and a != bdoes not automatically mean that a > b. Decorator functools.total_orderingcreates 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 origoriginal 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_origtakes 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_origwill 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_origpass origfrom 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.

    Also popular now: