Подборка @pythonetc, август 2018

8155df0ab68e69c1e5863edec2315486.png

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

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


Фабричный метод


Если вы создаете новые объекты внутри __init__, то целесообразнее передавать их уже готовыми в качестве аргументов, а для создания объекта использовать фабричный метод. Это отделит бизнес-логику от технической реализации создания объектов.

В этом примере __init__ для создания подключения к базе данных принимает в виде аргументов host и port:

class Query:
    def __init__(self, host, port):
        self._connection = Connection(host, port)


Вариант рефакторинга:


class Query:
    def __init__(self, connection):
        self._connection = connection
        
    @classmethod
    def create(cls, host, port):
        return cls(Connection(host, port))


У этого подхода есть как минимум следующие преимущества:

  • Легко внедрять зависимости. В тестах можно будет делать Query(FakeConnection()).
  • Класс может иметь столько фабричных методов, сколько хотите. Создавать подключение можно не только с помощью host и port, но еще и клонируя другое подключение, считывая конфигурационный файл, используя подключение по умолчанию и т. д.
  • Подобные фабричные методы можно превращать в асинхронные функции, что абсолютно невозможно провернуть с __init__.


super или next


Функция super() позволяет ссылаться на базовый класс. Это бывает очень полезно в случаях, когда производный класс хочет добавить что-то в реализацию метода, а не переопределить его полностью.


class BaseTestCase(TestCase):
    def setUp(self):
        self._db = create_db()

class UserTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()
        self._user = create_user()


Имя super не означает ничего «суперского». В данном контексте оно означает «выше по иерархии» (например, как в слове «суперинтендант»). При этом super() не всегда ссылается на базовый класс, она легко может возвращать дочерний класс. Так что правильнее было бы использовать имя next(), поскольку возвращается следующий класс согласно MRO.


class Top:
    def foo(self):
        return 'top'

class Left(Top):
    def foo(self):
        return super().foo()

class Right(Top):
    def foo(self):
        return 'right'

class Bottom(Left, Right):
    pass

# prints 'right'
print(Bottom().foo())


Не забывайте, что super() может выдавать разные результаты в зависимости от того, откуда изначально было вызван метод.


>>> Bottom().foo()
'right'
>>> Left().foo()
'top'

Пользовательское пространство имен для создания класса


Класс создается в два больших этапа. Сначала исполняется тело класса, как тело какой-либо функции. На втором этапе получившееся пространство имен (которое возвращается locals()) используется метаклассом (по умолчанию это type) для создания объекта класса.


class Meta(type):
    def __new__(meta, name, bases, ns):
        print(ns)
        return super().__new__(
            meta, name,
            bases, ns
        )

class Foo(metaclass=Meta):
    B = 2


Этот код выводит на экран {'__module__': '__main__', '__qualname__':'Foo', 'B': 3}.

Очевидно, что если ввести нечто вроде B = 2; B = 3, то метакласс увидит только B = 3, потому что лишь это значение находится в ns. Это ограничение проистекает из того факта, что метакласс начинает работать только после исполнения тела.

Однако можно вмешаться в процедуру исполнения, подсунув собственное пространство имен. По умолчанию используется простой словарь, но вы можете предоставить собственный объект, похожий на словарь, если воспользуетесь методом __prepare__ из метакласса.


class CustomNamespace(dict):
    def __setitem__(self, key, value):
        print(f'{key} -> {value}')
        return super().__setitem__(key, value)


class Meta(type):
    def __new__(meta, name, bases, ns):
        return super().__new__(
            meta, name,
            bases, ns
        )

    @classmethod
    def __prepare__(metacls, cls, bases):
        return CustomNamespace()


class Foo(metaclass=Meta):
    B = 2
    B = 3


Результат выполнения кода:


__module__ -> __main__
__qualname__ -> Foo
B -> 2
B -> 3


Таким образом enum.Enum защищается от дублирования.

matplotlib


matplotlib — сложная и гибкая в применении Python-библиотека для вывода графиков. Ее поддерживают многие продукты, в том числе Jupyter и Pycharm. Вот пример отрисовки простого фрактала с помощью matplotlib: https://repl.it/@VadimPushtaev/myplotlib (см. заглавную картинку этой публикации).

Поддержка временных зон


Python предоставляет мощную библиотеку datetime для работы с датами и временем. Любопытно, что объекты datetime обладают особым интерфейсом для поддержки временных зон (а именно, атрибутом tzinfo), но у этого модуля ограниченная поддержка упомянутого интерфейса, поэтому часть работы возлагается на другие модули.

Самый популярный из них — pytz. Но дело в том, что pytz не полностью соответствует интерфейсу tzinfo. Об этом говорится в самом начале документации pytz: «This library differs from the documented Python API for tzinfo implementations.»

Вы не можете использовать pytz-объекты временных зон в качестве tzinfo-атрибутов. Если попытаетесь это сделать, то рискуете получить совершенно безумный результат:


In : paris = pytz.timezone('Europe/Paris')
In : str(datetime(2017, 1, 1, tzinfo=paris))
Out: '2017-01-01 00:00:00+00:09'


Обратите внимание на смещение +00:09. Pytz надо использовать так:


In : str(paris.localize(datetime(2017, 1, 1)))
Out: '2017-01-01 00:00:00+01:00'


Кроме того, после любых арифметических операций нужно применять normalize к своим datetime-объектам, чтобы избежать изменения смещений (к примеру, на границе DST-периода).


In : new_time = time + timedelta(days=2)
In : str(new_time)
Out: '2018-03-27 00:00:00+01:00'
In : str(paris.normalize(new_time))
Out: '2018-03-27 01:00:00+02:00'


Если у вас Python 3.6, документация рекомендует использовать dateutil.tz вместоpytz. Эта библиотека полностью совместима с tzinfo, ее можно передавать в качестве атрибута и не нужно применять normalize. Правда, и работает помедленнее.

Если хотите знать, почему pytz не поддерживает API datetime, или хотите увидеть больше примеров, то почитайте эту статью.

Магия StopIteration


При каждом вызове next(x) возвращает из итератора x новое значение, пока не будет брошено исключение. Если это окажется StopIteration, значит итератор истощился и больше не может предоставлять значения. Если итерируется генератор, то в конце тела он автоматически бросит StopIteration:


>>> def one_two():
... yield 1
... yield 2
...
>>> i = one_two()
>>> next(i)
1
>>> next(i)
2
>>> next(i)
Traceback (most recent call last):
File "", line 1, in 
StopIteration


StopIteration можно автоматически обрабатывать инструментами, которые вызывают next:


>>> list(one_two())
[1, 2]


Но проблема в том, что любой явно не ожидаемый StopIteration, который возник в теле генератора, будет молча принят за признак окончания генератора, а не за ошибку, как любое другое исключение:


def one_two():
    yield 1
    yield 2

def one_two_repeat(n):
    for _ in range(n):
        i = one_two()
        yield next(i)
        yield next(i)
        yield next(i)

print(list(one_two_repeat(3)))


Здесь последний yield является ошибкой: брошенное исключение StopIteration останавливает итерирование list(...). Мы получаем результат [1, 2]. Однако в Python 3.7 это поведение изменилось. Чужеродное StopIteration заменили на RuntimeError:


Traceback (most recent call last):
  File "test.py", line 10, in one_two_repeat
    yield next(i)
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "test.py", line 12, in 
    print(list(one_two_repeat(3)))
RuntimeError: generator raised StopIteration


Вы можете с помощью __future__ import generator_stop включить такое же поведение начиная с Python 3.5.

© Habrahabr.ru