
Это шестая подборка советов про Python и программирование из моего авторского канала @pythonetc.
Предыдущие подборки:
Нетипичные декораторы
Декораторы функций не обязаны возвращать только новые функции, они могут возвращать любое другое значение:
def call(*args, **kwargs): def decorator(func): return func(*args, **kwargs) return decorator @call(15) def sqr_15(x): return x * x assert sqr_15 == 225
Это бывает полезно для создания простых классов всего лишь с одним переопределяемым методом:
from abc import ABCMeta, abstractmethod class BinaryOperation(metaclass=ABCMeta): def __init__(self, left, right): self._left = left self._right = right def __repr__(self): klass = type(self).__name__ left = self._left right = self._right return f'{klass}({left}, {right})' @abstractmethod def do(self): pass @classmethod def make(cls, do_function): return type( do_function.__name__, (BinaryOperation,), dict(do=do_function), ) class Addition(BinaryOperation): def do(self): return self._left + self._right @BinaryOperation.make def Subtraction(self): return self._left - self._right
__length_hint__
PEP 424 позволяет генераторам и прочим итерируемым объектам, у которых нет конкретного заранее определённого размера, возвращает свою примерную длину. Например, этот генератор наверняка вернёт около 50 элементов:
(x for x in range(100) if random() > 0.5)
Если вы пишете что-то итерируемое и хотите возвращать примерную длину, то определяйте метод
__length_hint__. А если вам точно известна длина, то используйте __len__. Если же используете итерируемый объект и хотите знать, какой он может быть длины, используйте operator.length_hint.in с генератором
Оператор
in можно использовать с генераторами: x in g. В этом случае Python будет итерироваться по g, пока не найдётся x или пока не закончится g.>>> def g(): ... print(1) ... yield 1 ... print(2) ... yield 2 ... print(3) ... yield 3 ... >>> 2 in g() 1 2 True
range(), однако, работает несколько лучше. У него есть волшебный переопределённый метод __contains__, благодаря которому вычислительная сложность in становится равна O(1):In [1]: %timeit 10**20 in range(10**30) 375 ns ± 10.7 ns per loop
Обратите внимание, что с функцией
xrange() из Python 2 это работать не будет.Операторы += и +
В Python есть два разных оператора:
+= и +. За их поведение отвечают методы __iadd__ и __add__ соответственно.class A: def __init__(self, x): self.x = x def __iadd__(self, another): self.x += another.x return self def __add__(self, another): return type(self)(self.x + another.x)
Если
__iadd__ не определён, то a += b будет работать как a = a + b.Семантическая разница между
+= и + заключается в том, что первый изменяет объект, а второй — создаёт новый:>>> a = [1, 2, 3] >>> b = a >>> a += [4] >>> a [1, 2, 3, 4] >>> b [1, 2, 3, 4] >>> a = a + [5] >>> a [1, 2, 3, 4, 5] >>> b [1, 2, 3, 4]
Функция как атрибут класса
Вы не можете хранить функцию в виде атрибута класса, потому что она будет автоматически преобразована в метод, если к ней обратятся через инстанс:
>>> class A: ... CALLBACK = lambda x: x ** x ... >>> A.CALLBACK <function A.<lambda> at 0x7f68b01ab6a8> >>> A().CALLBACK <bound method A.<lambda> of <__main__.A object at 0x7f68b01aea20>> >>> A().CALLBACK(4) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: <lambda>() takes 1 positional argument but 2 were given
Можно схитрить и обернуть функцию в тривиальный дескриптор:
>>> class FunctionHolder: ... def __init__(self, f): ... self._f = f ... def __get__(self, obj, objtype): ... return self._f ... >>> class A: ... CALLBACK = FunctionHolder(lambda x: x ** x) ... >>> A().CALLBACK <function A.<lambda> at 0x7f68b01ab950>
Также можно выйти из ситуации, воспользовавшись методом класса вместо атрибута.
class A: @classmethod def _get_callback(cls): return lambda x: x ** x
