Подборка @pythonetc, май 2019
Это одиннадцатая подборка советов про 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' '—'
>>> '\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
Весь код доступен здесь