Кеннет Рейтц – Автостопом по Python (страница 29)
Один и тот же инструмент используется при создании API для импорта и экспорта данных в Tablib для разных форматов (Tablib не хранит строку для каждого формата). Вместо этого применяются Dataset-атрибуты csv, json и yaml, они похожи на свойства Dataset.height и Dataset.width, показанные в предыдущем примере: вызывают функцию, которая генерирует результат из сохраненных данных или преобразует входной формат и затем заменяет основные данные. Но существует только один набор данных.
Когда свойство data.csv находится с левой стороны знака «равно», вызывается функция-сеттер для этого свойства, которая преобразует dataset из формата CSV. Когда свойство data.yaml находится с правой стороны знака «равно» или стоит отдельно, вызывается функция-геттер для создания строки в заданном формате на основе внутреннего набора данных. Рассмотрим пример.
Функции для получения, установки и удаления данных могут быть привязаны к единому атрибуту с помощью property. Его сигнатура выглядит так: property(fget=None, fset=None, fdel=None, doc=None), fget определяет функцию-геттер (formats.csv.import_set()), fset — функцию-сеттер (formats.csv.export_set()), а fdel — функцию удаления данных (оставлена пустой). Далее мы увидим код, в котором программно устанавливаются свойства форматирования.
Tablib помещает все подпрограммы для форматирования в подпакет formats. Это делает чище основной модуль core.py — и целый пакет становится модульным; добавлять новые форматы файлов будет нетрудно. Несмотря на то что можно копировать фрагменты практически идентичного кода и импортировать поведение при импорте и экспорте для каждого формата отдельно, все форматы
В следующем примере кода мы выводим все содержимое файла formats/__init__.py, поскольку файл не так велик и мы хотим показать, как определяется formats.available.
В файле core.py вместо примерно 20 (безобразных и сложных для поддержки) повторяющихся описаний функции для каждого формата код импортирует каждый формат программно, вызывая функцию self._register_formats() в конце метода __init__() класса Dataset. Рассмотрим фрагмент кода, в котором приводится метод Dataset._register_formats().
Эти способы использования декоратора @property
Зависимости Tablib в данный момент поставляются с кодом — в каталоге packages, но могут в будущем быть перемещены в систему надстроек. Каталог packages содержит сторонние пакеты, используемые внутри Tablib, чтобы гарантировать совместимость; другой вариант — указание версий в файле setup.py, который будет загружен и установлен в момент установки Tablib. Этот прием рассматривается в разделе «Зависимости, получаемые от третьей стороны» раздела «Структурируем проект» главы 4. Для Tablib был выбран вариант поведения, позволяющий снизить количество зависимостей, который нужно загружать пользователям, и поскольку иногда для Python 2 и Python 3 требуются разные пакеты, в этом случае включаются оба пакета. (Соответствующий пакет импортируется, функции вызываются с помощью их обычного имени в файле tablib/compat.py.) Таким образом, Tablib может иметь одну базу кода вместо двух — по одной для каждой версии Python. Раз каждая из зависимостей имеет собственную лицензию, на верхний уровень каталога проекта был добавлен документ NOTICE, в котором перечисляются лицензии каждой зависимости.
Скорости Python предпочитает читаемость. Его дизайн, афоризмы, из которых состоит его дзен, и раннее влияние, которое на него оказали языки вроде ABC (http://bit.ly/abc-to-python), — все это заставляет ставить дружелюбие к пользователю над производительностью (более подробно об оптимизации мы поговорим в разделе «Скорость» главы 8).
Использование свойства __slots__ в Tablib — этот тот случай, когда оптимизация имеет значение. Данная ссылка выглядит несколько странно, она доступна только для новых классов (они описаны через несколько страниц), но мы хотим показать, что при необходимости вы можете оптимизировать Python.
Подобная оптимизация полезна только в том случае, если у вас имеется много небольших объектов, поскольку она сократит отпечаток каждого объекта класса на размер одного словаря (крупные объекты сделают такую небольшую экономию нерелевантной, а для малого количества объектов такая экономия не стоит затраченных усилий).
Рассмотрим фрагмент из документации __slots__ (http://bit.ly/__slots__-doc).
По умолчанию объекты классов имеют словарь для хранения атрибутов. Он занимает слишком много места, если объекты имеют малое количество переменных объекта. Использование места может стать особенно заметным при создании большого количества объектов.
Ситуацию можно изменить путем объявления __slots__ в описании класса. Объявление __slots__ принимает последовательность переменных объекта и резервирует достаточный объем памяти для каждой переменной. Место экономится, поскольку для каждой переменной теперь не создается __dict__.
Обычно вам не следует об этом беспокоиться: обратите внимание, что свойство __slots__ не появляется в классах Dataset или Databook — только в классе Row, но поскольку рядов данных может быть очень много, использование __slots__ выглядит хорошим решением. Класс Row не показан в tablib/__init__.py, поскольку является вспомогательным классом для класса Dataset, для каждой строки создается один объект такого класса.
Рассмотрим, как выглядит определение __slots__ в самом начале определения класса Row:
class Row(object):
····"""Внутренний объект Row. Используется в основном для фильтрации."""
····__slots__ = ['_row', 'tags']
····def __init__(self, row=list(), tags=list()):
········self._row = list(row)
········self.tags = list(tags)
····#
····#… и т. д…
····#
Проблема в том, что больше не существует атрибута __dict__, в котором хранятся объекты класса Row, но функция pickle.dump() (вызывается для сериализации объектов) по умолчанию использует __dict__ для сериализации объектов, если только не определен метод __getstate__(). Аналогично во время десериализации (процесса, который читает сериализованные байты и восстанавливает объект в памяти), если метод __setstate__() не определен, метод pickle.load() загружает данные в атрибут объекта __dict__. Рассмотрим, как это обойти.
class Row(object):
····#
····#… пропускаем другие определения…
····#
····def __getstate__(self):
········slots = dict()
········for slot in self.__slots__:
············attribute = getattr(self, slot)