Подборка @pythonetc, ноябрь 2018

в 8:59, , рубрики: python, Блог компании Mail.Ru Group, Программирование
Подборка @pythonetc, ноябрь 2018 - 1

Это шестая подборка советов про 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

Автор: pushtaev

Источник

* - обязательные к заполнению поля