Это одиннадцатая подборка советов про Python и программирование из моего авторского канала @pythonetc.

< Предыдущие подборки


Выражение break блокирует исключение, если применяется в блоке finally, даже при отсутствии блока except:

for i in range(10):
    try:
        1 / i
    finally:
        print('finally')
        break
    print('after try')

print('after while')

Результат:

finally
after while

То же самое верно и для continue, однако это выражение может применяться в finally только до версии Python 3.8:

SyntaxError: 'continue' not supported inside 'finally' clause


Вы можете добавлять Unicode-символы в строковые литералы не только по их индексам, но и по имени.

>>> '\N{EM DASH}'
'—'
>>> '\u2014'
'—'

Этот способ совместим и с f-строками:

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


Для Python-объектов есть шесть «волшебных» методов, которые определяют правила сравнения:

  • __lt__ для <
  • __gt__ для >
  • __le__ для <=
  • __ge__ для >=
  • __eq__ для ==
  • __ne__ для !=

Если какие-то из этих методов не определены или возвращают NotImplemented, то применяются такие правила:

  • a.__lt__(b) то же самое, что b.__gt__(a)
  • a.__le__(b) то же самое, что b.__ge__(a)
  • a.__eq__(b) то же самое, что not a.__ne__(b) (обратите внимание, что в этом случае a и b не поменялись местами)

Однако, условия a >= b и a != b не означают автоматически, что a > b. Декоратор functools.total_ordering создаёт все шесть методов на основе __eq__ и одного из этих: __lt__, __gt__, __le__ или __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')


Иногда нужно использовать и декорированную, и не декорированную версию функции. Проще всего будет этого добиться, если не использовать специальный декорирующий синтаксис (с символом @) и создать декорирующую функцию вручную:

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

Или можно написать декоратор, который декорирует функцию, при этом сохраняя в её атрибуте orig исходную версию:

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

Если все ваши декораторы созданы через functools.wraps, то можете с помощью атрибута __wrapped__ обращаться к не декорированной функции:

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

Но помните, что этот подход не работает для функций, которые декорированы более чем одним декоратором: вам придётся обращаться к __wrapped__ каждого из применённых декораторов:

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

Результат:

[4]
['4']
'4'

Упомянутый выше декоратор @saving_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_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"]')))

Декоратор @saving_orig не будет делать то, что мы хотим, если к функции применено несколько декораторов. Тогда для каждого из них придётся вызывать 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"')))

Результат:

[42]
['X']
'X'

Исправить это можно с помощью поддержки произвольного количества декораторов в качестве аргументов 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"')))

Результат:

[42]
'X'

Есть и другое решение: сделать так, чтобы saving_orig передавал orig из одной декорированной функции в другую:

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)

Когда декоратор становится слишком сложным, то лучше преобразовать его из функции в класс с методом __call__:

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

Последняя строка позволяет вам именовать класс в Camel-кейсе и сохранить имя декоратора в Snake-кейсе.

Вместо преобразования декорированной функции вы можете создать другой вызываемый класс, и возвращать вместо функции его экземпляры:

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

Весь код доступен здесь

Комментарии (2)


  1. Sovetnikov
    05.06.2019 00:18

    Выражение break блокирует исключение, если применяется в блоке finally, даже при отсутствии блока except

    Круто! Но зачем они это сделали!? :)

    Кстати жалею, что я узнал когда-то о for… else ..., конструкция оказалась удобной на практике и я начал её применять! Но читать потом этот код…

    Пойду начну список того «что нельзя делать» у нас на проектах :)


    1. setflag
      06.06.2019 00:40

      Согласен. Буквально на днях, открываю код, а IDE открыла в месте, где в функции цикл for с вложенными блоками и заканчивается все блоком else с одной единственной строкой. И выглядит этот else как будто в воздухе висит. Сначала даже непонятно стало, что это.