Подборка @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 "", 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.