Подборка @pythonetc, ноябрь 2018
Это шестая подборка советов про 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
at 0x7f68b01ab6a8>
>>> A().CALLBACK
of <__main__.A object at 0x7f68b01aea20>>
>>> A().CALLBACK(4)
Traceback (most recent call last):
File "", line 1, in
TypeError: () 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
at 0x7f68b01ab950>
Также можно выйти из ситуации, воспользовавшись методом класса вместо атрибута.
class A:
@classmethod
def _get_callback(cls):
return lambda x: x ** x