Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
308 Глава 6. Объектно/ориентированное программирование приложения, предназначенные для работы с графикой. Изображение .xpm , которое воспроизводит данный фрагмент программного кода, приводится на рис. 6.6. Теперь приступим к знакомству с методами класса Image, начав с инст рукции class и метода инициализации: class Image: def __init__(self, width, height, filename="", background="#FFFFFF"): self.filename = filename self.__background = background self.__data = {} self.__width = width self.__height = height self.__colors = {self.__background} При создании экземпляра класса Image пользователь (то есть програм мист – пользователь класса) должен указать ширину и высоту изобра жения, а имя файла и цвет фона являются необязательными, потому что эти параметры имеют значения по умолчанию. Ключами словаря self.__data являются координаты (x, y), а его значениями – строки, обозначающие цвет. Множество self.__colors инициализируется зна чением цвета фона – в нем будут храниться все уникальные значения цвета, присутствующие в изображении. Все атрибуты класса, за исключением filename, являются частными, поэтому нам необходимо реализовать средства доступа к ним. Это лег ко можно сделать с помощью свойств. 1 @property def background(self): return self.__background @property def width(self): return self.__width @property 1 В главе 8 будет представлен совершенно иной подход к обеспечению досту па к атрибутам – с использованием таких методов, как __getattr__() и __setattr__(), что в некоторых случаях может быть очень удобно. Рис. 6.6. Изображение square_eye.xpm Собственные классы коллекций 309 def height(self): return self.__height @property def colors(self): return set(self.__colors) При возвращении атрибута объектом нам необходимо понимать разницу между изменяемыми и неизменяемы ми типами. Всегда безопасно возвращать атрибуты неиз меняемых типов, поскольку их невозможно изменить, но в случае с изменяемыми атрибутами нам следует рас смотреть некоторые их особенности. Возврат ссылки на изменяемый атрибут выполняется очень быстро, потому что при этом не происходит его копирование, но это так же означает, что вызывающий программный код полу чает доступ к внутреннему состоянию объекта и может изменить его так, что объект окажется испорчен. Для предотвращения такой возможности можно взять за правило всегда возвращать копии атрибутов изменяе мых типов данных, если это не оказывает существенного влияния на производительность. (В этом случае вместо хранения множества уникальных цветов можно было бы возвращать результат выражения set(self.__data.val ues()) | {self.__background}, когда вызывающей програм ме потребуется множество уникальных цветов.) def __getitem__(self, coordinate): assert len(coordinate) == 2, "coordinate should be a 2tuple" if (not (0 <= coordinate[0] < self.width) or not (0 <= coordinate[1] < self.height)): raise CoordinateError(str(coordinate)) return self.__data.get(tuple(coordinate), self.__background) Этот метод возвращает цвет пикселя с заданными координатами, ко гда вызывающая программа использует оператор доступа к элементам ([]). Специальный метод реализации оператора доступа к элементам и некоторые другие специальные методы, имеющие отношение к кол лекциям, перечислены в табл. 6.4. Мы будем применять две тактики для организации доступа к элемен там. Первая тактика состоит в том, чтобы предварительно проверить, является ли аргумент coordinate последовательностью из двух элемен тов (обычно кортеж из двух элементов), для чего используется инст рукция assert. Вторая тактика состоит в том, что принимаются любые координаты, но если они выходят за пределы изображения, возбужда ется наше собственное исключение. Для получения значения цвета из указанных координат мы использо вали метод dict.get() со значением по умолчанию, равным цвету фона. Это гарантирует, что если для данной пары координат цвет никогда не Копирование коллекций, стр. 173 310 Глава 6. Объектно/ориентированное программирование устанавливался, вместо возбуждения исключения KeyError будет воз вращен цвет фона. def __setitem__(self, coordinate, color): assert len(coordinate) == 2, "coordinate should be a 2tuple" if (not (0 <= coordinate[0] < self.width) or not (0 <= coordinate[1] < self.height)): raise CoordinateError(str(coordinate)) if color == self.__background: self.__data.pop(tuple(coordinate), None) else: self.__data[tuple(coordinate)] = color self.__colors.add(color) Если пользователь устанавливает значение цвета, равное значению цвета фона, мы просто удаляем соответствующий элемент словаря, по скольку отсутствие тех или иных координат в словаре означает, что пиксели с этими координатами имеют цвет фона. Вместо инструкции del следует использовать метод dict.pop() и передавать ему фиктив ный второй аргумент, чтобы избежать возбуждения исключения Key Error , если указанный ключ (координаты) отсутствует в словаре. Таблица 6.4. Специальные методы коллекций Специальный метод Пример использования Описание __contains__(self, x) x in y Возвращает True, если x присутст вует в последовательности y или x является ключом отображения y __delitem__(self, k) del y[k] Удаляет kй элемент последова тельности y или элемент с ключом k в отображении y __getitem__(self, k) y[k] Возвращает kй элемент последова тельности y или значение элемента с ключом k в отображении y __iter__(self) for x in y: pass Возвращает итератор по элемен там последовательности y или по ключам отображения y __len__(self) len(y) Возвращает число элементов в y __reversed__(self) reversed(y) Возвращает итератор, выполняю щий обход элементов последова тельности y или ключей отображе ния y в обратном порядке __setitem__(self, k, v) y[k] = v Устанавливает kй элемент после довательности y или значение эле мента с ключом k в отображении y Собственные классы коллекций 311 Если цвет отличается от цвета фона, мы устанавливаем его как значе ние элемента с заданными координатами и добавляем его в множество уникальных цветов, используемых в изображении. def __delitem__(self, coordinate): assert len(coordinate) == 2, "coordinate should be a 2tuple" if (not (0 <= coordinate[0] < self.width) or not (0 <= coordinate[1] < self.height)): raise CoordinateError(str(coordinate)) self.__data.pop(tuple(coordinate), None) Когда удаляется значение цвета для заданных координат, фактически происходит назначение цвета фона для этих координат. Здесь для уда ления элемента также используется метод dict.pop(), потому что он корректно работает, даже когда в словаре отсутствует элемент с ука занными координатами. Мы не предусматриваем реализацию метода __len__(), поскольку он не имеет смысла для двухмерного объекта. Кроме того, мы не предусмат риваем реализацию метода получения репрезентативной формы, по скольку объект Image невозможно сформировать полностью единствен ным вызовом Image(); в связи с этим в классе отсутствует реализация метода __repr__() (и __str__()). Если пользователь вызовет функцию repr() или str() для объекта Image, метод object.__repr__() базового класса вернет строку вида ' Нам необходимо предоставить пользователям класса Image возмож ность сохранять изображения на диск и загружать их с диска, поэтому мы предусмотрели реализацию двух методов – save() и load(), выпол няющие эти действия. Чтобы сохранить данные на диск, мы будем выполнять их консервиро вание . На языке Python под консервированием понимается способ се риализации (преобразования в последовательность байтов или в стро ку) объектов. В консервировании особенно ценно то, что имеется воз можность консервировать коллекции, такие как списки или словари, и даже объекты, внутри которых имеются другие объекты (включая коллекции, которые в свою очередь могут включать другие коллек ции, и т. д.) – весь объем данных будет законсервирован, причем без дублирования повторяющихся объектов. Законсервированный объект можно прочитать прямо в пере менную Python – нам не потребуется выполнять тот или иной вид парсинга. То есть консервирование объектов идеально под ходит для сохранения и загрузки специализированных кол лекций данных, особенно в маленьких программах и в про граммах, разрабатываемых для личного пользования. Однако 312 Глава 6. Объектно/ориентированное программирование при консервировании не используются механизмы обеспечения безо пасности (данные не шифруются и электронная подпись не добавляет ся), поэтому загрузка законсервированных объектов из непроверенных источников может быть опасным делом. Ввиду этого в программах, предназначенных не только для личного пользования, лучше вырабо тать собственный формат файлов, – специфичный для программы. В главе 7 будет показано, как читать и писать файлы с двоичными дан ными, с текстом и с данными в формате XML. def save(self, filename=None): if filename is not None: self.filename = filename if not self.filename: raise NoFilenameError() fh = None try: data = [self.width, self.height, self.__background, self.__data] fh = open(self.filename, "wb") pickle.dump(data, fh, pickle.HIGHEST_PROTOCOL) except (EnvironmentError, pickle.PicklingError) as err: raise SaveError(str(err)) finally: if fh is not None: fh.close() Первая часть функции просто проверяет наличие имени файла. Если объект Image был создан без указания имени файла и после этого имя файла не было установлено, то при вызове метода save() необходимо явно указывать имя файла (в этом случае он выполняет операцию «со хранить как» и устанавливает значение атрибута filename). Если имя файла не было указано при вызове метода, то используется текущее значение атрибута filename, а если текущее имя файла не задано, то возбуждается исключение. Затем создается список, в который добавляются данные объекта для сохранения, включая словарь self.__data с элементами координаты цвет, но исключая множество уникальных цветов, поскольку эти дан ные легко реконструируются. Далее открывается файл для записи в двоичном режиме и вызывается функция pickle.dump(), которая за писывает данные объекта в файл. Вот и все! Модуль pickle может сериализовать данные с использованием разных форматов (в документации они называются протоколами). Формат оп ределяется третьим аргументом функции pickle.dump(). Протокол 0 – это ASCII, его удобно использовать во время отладки. Мы использова ли протокол 3 (pickle.HIGHEST_PROTOCOL) – компактный двоичный фор мат, именно поэтому файл был открыт в режиме записи двоичных дан ных. При чтении файлов с законсервированными объектами протокол Собственные классы коллекций 313 не указывается – функция pickle.load() автоматически определяет ис пользуемый протокол. def load(self, filename=None): if filename is not None: self.filename = filename if not self.filename: raise NoFilenameError() fh = None try: fh = open(self.filename, "rb") data = pickle.load(fh) (self.__width, self.__height, self.__background, self.__data) = data self.__colors = (set(self.__data.values()) | {self.__background}) except (EnvironmentError, pickle.UnpicklingError) as err: raise LoadError(str(err)) finally: if fh is not None: fh.close() Эта функция, так же как и функция save(), начинается с определения имени файла, который требуется загрузить. Файл должен быть открыт для чтения в двоичном режиме, а сама операция чтения выполняется единственной инструкцией data = pickle.load(fh). Объект data – это точная реконструкция сохранявшегося объекта. То есть в данном слу чае это список, содержащий целочисленные значения ширины и высо ты, строку с цветом фона и словарь с элементами координатыцвет. Для присваивания каждого элемента списка data соответствующей пе ременной выполняется операция распаковывания кортежа, поэтому любые имевшиеся данные, хранившиеся в объекте Image, будут (кор ректно) потеряны. Множество уникальных цветов реконструируется посредством созда ния множества всех цветов, хранящихся в словаре, после чего в мно жество добавляется цвет фона. def export(self, filename): if filename.lower().endswith(".xpm"): self.__export_xpm(filename) else: raise ExportError("unsupported export format: " + os.path.splitext(filename)[1]) Мы реализовали один универсальный метод экспорта, где по расшире нию файла определяется имя вызываемого метода или возбуждается исключение, если запрошенный формат экспорта не поддерживается. В данном случае поддерживается экспорт только в файлы .xpm (и толь ко для изображений, насчитывающих не более 8 930 цветов). Мы не приводим реализацию метода __export_xpm(), потому что он никак не 314 Глава 6. Объектно/ориентированное программирование связан с темой главы, но в исходных текстах примеров к этой книге он, конечно же, присутствует. На этом мы завершаем рассмотрение класса Image. Этот класс является типичным представителем типов данных, используемых для хране ния специфических данных в программах, обеспечивая возможность доступа к элементам содержащихся в нем данных, возможность сохра нения на диск и загрузки с диска своих данных и предоставляя только необходимые методы. В следующих двух подразделах мы увидим, как создать два типа коллекций, обладающие полным API. Создание классов коллекций посредством агрегирования В этом подразделе мы создадим полный тип коллекции SortedList, ко торый представляет собой список, хранящий свои элементы в порядке сортировки. Сортировка выполняется с помощью оператора «меньше чем» (<), действие которого реализуется специальным методом __lt__() , или с помощью функции, которая передается в виде аргумен та key. Класс стремится соответствовать API встроенного класса list, чтобы максимально упростить его освоение, но некоторые методы не могут быть реализованы по вполне объяснимым причинам, например, использование оператора конкатенации (+) может привести к наруше нию порядка сортировки элементов, поэтому мы не реализуем его. Как всегда, когда создается свой собственный класс, у нас есть выбор – наследовать класс, подобный тому, что создается, или создать новый класс с нуля, агрегируя в него экземпляры других, необходимых клас сов. Для класса SortedList, рассматриваемого в этом подразделе, мы будем использовать подход на основе агрегирования элементов дан ных, а в следующем подразделе, где рассматривается создание класса SortedDict , мы будем использовать не только агрегирование, но и на следование. В главе 8 мы узнаем, что классы могут брать на себя обязательства поддерживать определенные типы API. Например, класс list поддер живает API MutableSequence, то есть поддерживает оператор in, встро енные функции iter() и len(), оператор доступа к элементам ([]) для получения, установки и удаления элементов, а также метод insert(). Класс SortedList, представленный здесь, не поддерживает возмож ность изменения значений элементов и не имеет метода insert(), по этому он не поддерживает API MutableSequence. Если бы мы создавали класс SortedList, наследуя класс list, окончательная реализация заяв ляла бы о себе как об изменяемой последовательности, но не имела бы полного API. Поэтому класс SortedList не наследует класс list и не де лает никаких утверждений о своем API. С другой стороны, класс SortedDict , рассматриваемый в следующем подразделе, реализует пол ный API MutableMapping, который поддерживается классом dict, поэто му мы смогли создать его как подкласс класса dict. Собственные классы коллекций 315 Ниже приводятся несколько типичных примеров использования клас са SortedList: letters = SortedList.SortedList(("H", "c", "B", "G", "e"), str.lower) # str(letters) == "['B', 'c', 'e', 'G', 'H']" letters.add("G") letters.add("f") letters.add("A") # str(letters) == "['A', 'B', 'c', 'e', 'f', 'G', 'G', 'H']" letters[2] # вернет: 'c' Объект класса SortedList представляет собой агрегат (композицию) из двух частных атрибутов – функции self.__key() (ссылка на объект self.__key ) и списка self.__list. Ключевая функция передается во втором аргументе (или в виде именованного аргумента key, если начальная по следовательность не задана). Если ключевая функция задана, используется частная функция модуля: _identity = lambda x: x Это функция тождественности: она просто возвращает свой аргумент, не изменяя его, поэтому, когда она используется в качестве ключевой функции, это означает, что ключом сортировки объектов в списке яв ляются сами объекты. Тип SortedList не поддерживает возможность изменения элементов с по мощью оператора доступа к элементам ([]) (и поэтому не реализует специальный метод __setitem__()), а также не имеет методов append() или extend(), поскольку они могут нарушать порядок сортировки. Единственный способ добавить элементы в сортированный список со стоит в том, чтобы передать последовательность при создании объекта SortedList , или добавлять их позднее, с помощью метода Sorted List.add() . С другой стороны, мы безопасно можем использовать опе ратор доступа к элементам для получения значения или удаления эле мента в указанной позиции, потому что ни одна из этих операций не оказывает влияния на порядок сортировки, поэтому были реализова ны оба специальных метода __getitem__() и __delitem__(). Теперь мы перейдем к последовательному рассмотрению методов клас са и, как обычно, начнем с инструкции class и метода инициализации: class SortedList: def __init__(self, sequence=None, key=None): self.__key = key or _identity assert hasattr(self.__key, "__call__") if sequence is None: self.__list = [] elif (isinstance(sequence, SortedList) and sequence.key == self.__key): Лямбда функции, стр. 215 |