company_banner

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



    Это третья подборка советов про 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 "<stdin>", line 1, in <module>
    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 <module>
        print(list(one_two_repeat(3)))
    RuntimeError: generator raised StopIteration

    Вы можете с помощью __future__ import generator_stop включить такое же поведение начиная с Python 3.5.
    • +34
    • 8,1k
    • 4

    Mail.Ru Group

    831,72

    Строим Интернет

    Поделиться публикацией

    Похожие публикации

    Комментарии 4
      –2
      Какой смысл у этой всей писанины? Вы тут обсуждаете очень тонкие и специализированные вопросы. Ну ок, я понял, согласен, умно. Но я через пару дней уже забуду про это.
      Это к тому, что рефакторинг это такая штука которая должна приходить с опытом. Ей невозможно сразу научиться. Можно только каким-то общим методам. Но разные задачи требудут применения разных приемов. Более того может быть несколько оптимальных решений. Еще более того можно оптимизировать до бесконечности и всеравно найдется вариант еще более оптимальный (колмогоровская сложность).
        +1
        Даже не пытайтесь гордиться своей узколобостью. В этом нет ничего восхитительного.
        Это далеко не «очень тонкие и специализированные вопросы», Вы сможете найти примеры использования этих концепций в любой широко используемой python библиотеке.

        P.S. Мне показалось, статья написана с учетом скорее Python 3? Стоило бы об этом упомянуть.
        0
        Подобные фабричные методы можно превращать в асинхронные функции, что абсолютно невозможно провернуть с __init__.

        Все еще хуже. Возможно. Буквально недавно тут даже было видео Юрия Селиванова с PyCon Russia. Как минимум наследование уже будет адом, особенно множественное.
          0
          Да, это преувеличение. В одном из наших проектов даже есть класс с асинхронным конструктором, но мы быстро об этом пожалели :).

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое