https://habr.com/company/mailru/blog/422789/- Программирование
- Python
- Блог компании Mail.Ru Group
Это третья подборка советов про 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.