Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
324 Глава 6. Объектно/ориентированное программирование будет передан класс dict, а объекту класса SortedDict будет передан класс SortedDict. Возвращаемое значение – словарь заданного класса. Например: class MyDict(SortedDict.SortedDict): pass d = MyDict.fromkeys("VEINS", 3) str(d) # returns: "{'E': 3, 'I': 3, 'N': 3, 'S': 3, 'V': 3}" d.__class__.__name__ # returns: 'MyDict' То есть при вызове метода дочернего класса в переменной cls будет ус тановлен корректный класс, точно так же, как при вызове обычных методов в переменной self будет передана ссылка на текущий объект. Функциигенераторы Функциягенератор, или методгенератор – это функция, или метод, содержащая выражение yield. В результате обращения к функциигенератору возвращается итератор. Значения из ите ратора извлекаются по одному, с помощью его метода __next__(). При каждом вызове метода __next__() он возвращает результат вычисления выражения yield. (Если выражение отсутствует, возвращается значение None.) Когда функциягенератор завер шается или выполняет инструкцию return, возбуждается исклю чение StopIteration. На практике очень редко приходится вызывать метод __next__() или обрабатывать исключение StopIteration. Обычно функция генератор используется в качестве итерируемого объекта. Ни же приводятся две практически эквивалентные функции. Функ ция слева возвращает список, а функция справа возвращает ге нератор. # Создает и возвращает список def letter_range(a, z): # Возвращает каждое result = [] # значение по требованию while ord(a) < ord(z): def letter_range(a, z): result.append(a) while ord(a) < ord(z): a = chr(ord(a) + 1) yield a return result a = chr(ord(a) + 1) Результаты, воспроизводимые обеими функциями, можно обойти с помощью цикла for, например for letter in letter_range("m", "v"): . Однако когда требуется получить список символов с помо щью функции слева, достаточно просто вызвать ее как let ter_range("m", "v") , а для функции справа необходимо выпол нить преобразование: list(letter_range("m", "v")). Функциигенераторы и методыгенераторы (а также выраже ниягенераторы) более полно рассматриваются в главе 8. Собственные классы коллекций 325 Методы класса отличаются от статических методов и удобнее в исполь зовании, потому что статические методы привязаны к определенному классу и не различают ситуации, когда они вызываются в контексте оригинального класса, а когда – в контексте подкласса. def __setitem__(self, key, value): if key not in self: self.__keys.add(key) return super().__setitem__(key, value) Этот метод обеспечивает поддержку синтаксиса d[key] = value. Если ключ key отсутствует в словаре, он добавляется в список ключей, – с использованием возможностей класса SortedList, чтобы поместить его в позицию с корректным номером. Затем вызывается метод базово го класса, результат которого возвращается вызывающей программе, чтобы обеспечить поддержку объединения операций в цепочку, на пример x = d[key] = value. Обратите внимание, что условная инструкция if проверяет наличие ключа в словаре типа SortedList, используя выражение not in self. Так как класс SortedDict наследует класс dict, объект класса SortedDict мо жет использоваться везде, где ожидается объект класса dict, и в дан ном случае объект self является экземпляром класса SortedDict. При переопределении методов класса dict в классе SortedDict, когда для выполнения какихлибо действий необходимо вызвать реализацию ба зового класса, мы не должны забывать использовать функцию super(), как это сделано в последней инструкции данного метода. Делается это для того, чтобы предотвратить вызов методом самого себя и попадание в бесконечную рекурсию. Мы не стали переопределять метод __getitem__(), так как версия мето да в базовом классе прекрасно справляется с работой и не оказывает влияния на порядок сортировки ключей. def __delitem__(self, key): try: self.__keys.remove(key) except ValueError: raise KeyError(key) return super().__delitem__(key) Этот метод обеспечивает поддержку синтаксиса del d[key]. Если ключ key отсутствует в словаре, вызов метода SortedDict.remove() возбуждает исключение ValueError. Если это происходит, исключение обрабатыва ется и возбуждается исключение KeyError, что соответствует поведе нию класса dict. В противном случае возвращается результат вызова реализации базового класса, которая удаляет элемент с заданным ключом из словаря. def setdefault(self, key, value=None): if key not in self: 326 Глава 6. Объектно/ориентированное программирование self.__keys.add(key) return super().setdefault(key, value) Этот метод возвращает значение для заданного ключа, если этот ключ присутствует в словаре. В противном случае он создает новый элемент с заданным ключом и значением и возвращает значение. В случае со словарем типа SortedDict ключ должен быть добавлен в список клю чей, если этого ключа еще нет в словаре. def pop(self, key, *args): if key not in self: if len(args) == 0: raise KeyError(key) return args[0] self.__keys.remove(key) return super().pop(key, args) Если в словаре имеется указанный ключ, этот метод возвращает соот ветствующее ему значение и удаляет элемент ключзначение из слова ря. Кроме того, ключ также удаляется из списка ключей. Реализация этого не так проста, потому что метод должен поддержи вать два различных типа поведения, чтобы соответствовать реализа ции метода dict.pop(). Вопервых, реализация должна поддерживать синтаксис d.pop(k) и возвращать значение ключа k или, если ключ не существует, возбуждать исключение KeyError. Вовторых, поддержи вать синтаксис d.pop(k, value) и возвращать значение ключа k или, ес ли ключ не существует, возвращать значение value (которое может быть объектом None). В любом случае, если ключ существует, соответ ствующий элемент словаря удаляется. def popitem(self): item = super().popitem() self.__keys.remove(item[0]) return item Метод dict.popitem() удаляет и возвращает случайный элемент ключ значение из словаря. Сначала необходимо вызвать метод базового класса, так как заранее неизвестно, какой элемент будет удален. За тем ключ элемента удаляется из списка ключей, и элемент возвраща ется вызывающей программе. def clear(self): super().clear() self.__keys.clear() Здесь удаляются все элементы словаря и все элементы списка ключей. def values(self): for key in self.__keys: yield self[key] def items(self): Собственные классы коллекций 327 for key in self.__keys: yield (key, self[key]) def __iter__(self): return iter(self.__keys) keys = __iter__ У словарей имеется четыре метода, возвращающие итераторы: dict.va lues() – для значений, dict.items() – для элементов ключзначение, dict.keys() – для ключей и специальный метод __iter__(), обеспечи вающий поддержку функции iter(), возвращающей итератор по клю чам словаря. (В действительности версии методов базового класса воз вращают представления словаря, но итераторы, реализованные здесь, во многом обладают тем же поведением.) Поскольку методы __iter__() и keys() обладают идентичным поведени ем, то вместо того чтобы создавать отдельную реализацию метода keys() , мы просто создали переменную с именем keys и записали в нее ссылку на метод __iter__(). Теперь пользователи класса SortedDict смогут вызывать метод d.keys() или функцию iter(d), чтобы получить итератор, позволяющий выполнить обход ключей словаря. Точно так же они смогут вызывать метод d.values(), чтобы получить итератор, позволяющий выполнить обход значений словаря. Методы values() и items() являются методамигенерато рами (краткое описание методовгенераторов приводит ся во врезке «Функциигенераторы» на стр. 324). В обо их случаях они выполняют итерации по сортированному списку ключей, благодаря чему всегда возвращают ите раторы, выполняющие итерации в порядке сортировки ключей (согласно ключевой функции, которая определя ется на этапе создания словаря). В методах items() и values() значения извлекаются из словаря с использо ванием синтаксиса d[k] (то есть с помощью метода __getitem__() ), благодаря тому, что у нас сохраняется воз можность интерпретировать self как объект класса dict. def __repr__(self): return object.__repr__(self) def __str__(self): return ("{" + ", ".join(["{0!r}: {1!r}".format(k, v) for k, v in self.items()]) + "}") Мы не в состоянии предоставить репрезентативную форму представле ния объекта класса SortedDict, пригодную для передачи функции eval() , потому что мы не можем реализовать репрезентативную форму представления ключевой функции. Поэтому мы переопределяем ме тод __repr__() и вместо метода dict.__repr__() вызываем метод object. __repr__() всеобщего базового класса. Он воспроизводит строку репре Методы генераторы, стр. 397 328 Глава 6. Объектно/ориентированное программирование зентативной формы представления, непригодную для передачи функ ции eval(), например ' Мы предусмотрели собственную реализацию метода __str__(), так как нам необходимо, чтобы элементы выводились в порядке сортировки ключей. Однако этот метод можно было бы реализовать несколько иначе: items = [] for key, value in self.items(): items.append("{0!r}: {1!r}".format(key, value)) return "{" + ", ".join(items) + "}" Однако использование генератора списков позволило получить более короткую реализацию и избавиться от временной переменной items. Методы dict.get() и dict.__getitem__() базового класса (для поддержки синтаксиса v = d[k]), dict.__len__() (для поддержки len(d)) и dict. __contains__() (для поддержки x in d) прекрасно справляются со своей работой и не зависят от порядка сортировки, поэтому нам не требуется переопределять их. Последний метод класса dict, который нам необходимо переопреде лить, – это метод copy(). def copy(self): d = SortedDict() super(SortedDict, d).update(self) d.__keys = self.__keys.copy() return d Этот метод можно было бы реализовать просто: def copy(self): return SortedDict(self) . Но мы выбрали немного более сложное решение, что бы избежать повторной сортировки уже отсортированного списка ключей. Здесь создается пустой отсортированный словарь, затем в не го с помощью метода dict.update() базового класса, с целью избежать вызова переопределенной версии метода SortedDict.update(), перепи сываются элементы оригинального отсортированного словаря, и нако нец список ключей вновь созданного словаря замещается поверхност ной копией списка ключей self.__keys оригинального словаря. Когда функция super() вызывается без аргументов, она работает с объ ектом self и с его базовым классом. Но мы легко можем заставить ее работать с любым классом и с любым объектом, явно передавая ей имя класса и объект. При таком использовании функция super() работает с классом, который является базовым по отношению к указанному, поэтому в данном случае программный код имеет тот же эффект, как (который вполне можно было бы использовать) dict.update(d, self). Благодаря тому, что в языке Python используется весьма эффектив ный алгоритм сортировки, который оптимизирован для работы с час тично отсортированными списками, наша реализация едва ли даст за метный эффект, разве только при работе с огромными словарями. Тем В заключение 329 не менее она демонстрирует, что собственная реализация метода copy() в принципе может быть эффективнее, чем прием copy_of_x = ClassOfX(x) , который используется во встроенных типах языка Py thon. Точно так же, как и в случае с классом SortedList, мы определи ли ссылку __copy__ = copy, чтобы функция copy.copy() могла использо вать нашу реализацию, а не свою собственную. def value_at(self, index): return self[self.__keys[index]] def set_value_at(self, index, value): self[self.__keys[index]] = value Эти два метода расширяют API класса dict. Поскольку в отличие от обычного класса dict ключи в классе SortedDict упорядочены, отсюда следует, что к объектам этого класса применимо понятие номера пози ции. Например, первый элемент словаря имеет номер позиции 0, а по следний элемент – номер позиции len(d) – 1 . Оба эти метода оперируют элементом словаря, ключ которого находится в позиции index внутри отсортированного списка ключей. Благодаря механизму наследования мы можем отыскивать значения в объекте SortedDict с помощью опера тора доступа к элементу ([]), применяя его непосредственно к объекту self , так как в этом отношении self можно считать объектом класса dict . Если указанное значение индекса находится за пределами спи ска, методы возбуждают исключение IndexError. На этом мы завершили реализацию класса SortedDict. Не так часто возникает необходимость создавать универсальные классы коллек ций, подобные этому классу, но когда такая необходимость возникает, специальные методы позволяют полностью интегрировать наш класс в язык Python, чтобы пользователи могли работать с ними, как с лю быми другими встроенными классами или с классами из стандартной библиотеки. В заключение В этой главе были рассмотрены все фундаментальные основы под держки объектноориентированного программирования в языке Py thon. Мы начали с демонстрации некоторых недостатков, присущих применению исключительно процедурного стиля программирования, и описали, как их можно избежать, используя объектноориентиро ванный подход. Затем были описаны некоторые термины, используе мые в объектноориентированном программировании, включая мно жество «совпадающих по значению» терминов, таких как базовый класс и суперкласс. Мы увидели, как можно создавать простые классы с атрибутами дан ных и собственными методами. Кроме того, мы увидели, как наследо вать классы и как добавлять дополнительные атрибуты данных и ме 330 Глава 6. Объектно/ориентированное программирование тоды, а также, как «исключать» реализации нежелательных методов. Исключение методов необходимо при наследовании классов, когда возникает потребность ограничить круг методов, предоставляемых на шим подклассом; однако этот прием следует использовать с большой осторожностью, потому что он не соответствует ожиданиям, что под класс может использоваться везде, где может использоваться базовый класс, то есть он нарушает принцип полиморфизма. Собственные классы могут быть интегрированы в язык Python так, что бы они поддерживали те же синтаксические конструкции, что и встро енные классы Python или классы из стандартной библиотеки. Дости гается это за счет реализации специальных методов. Мы показали, как можно реализовать специальные методы поддержки операций сравнения, как обеспечить представление объектов в репрезентатив ной и строковой формах и как обеспечить преобразование в другие ти пы данных, такие как int и float, когда это имеет смысл. Мы также по казали, как реализовать метод __hash__(), чтобы экземпляры нестан дартных классов могли использоваться в качестве ключей словаря или членов множества. Атрибуты данных сами по себе не обеспечивают механизм, гаранти рующий установку корректных значений. Мы увидели, насколько просто атрибуты данных замещаются свойствами, что позволяет соз давать атрибуты, доступные только для чтения, а для свойств, доступ ных для записи, легко реализовать проверку корректности записывае мых данных. В большинстве случаев классы, которые мы создаем, являются «непол ными», потому что мы стремимся реализовать только те методы, кото рые действительно необходимы. Это вполне оправданный прием, но у нас имеется возможность создавать полностью собственные реализа ции классов, которые предоставляют все соответствующие методы. Мы увидели, как создаются классы, хранящие одиночные значения, путем агрегирования других классов или более компактным способом – за счет использования наследования. Мы также увидели, как создаются классы, хранящие множество значений (коллекции). Нестандартные классы коллекций могут обеспечивать те же возможности, что и встро енные классы коллекций, включая поддержку оператора in, функций len() , iter(), reversed() и оператор доступа к элементам ([]). Мы узнали, что создание объекта и его инициализация – это разные операции и что язык Python позволяет контролировать выполнение обеих операций, хотя практически всегда нам требуется предусматри вать собственную реализацию только для операции инициализации. Мы также узнали, что можно безопасно возвращать объекты неизме няемых атрибутов данных, но в случае изменяемых атрибутов данных почти всегда желательно возвращать их копии, чтобы избежать не преднамеренного изменения объекта и приведения его в недопустимое состояние. В заключение 331 В языке Python имеются обычные методы, статические методы, мето ды классов и функции модуля. Мы узнали, что в большинстве своем методы относятся к категории обычных методов. Иногда бывает по лезно реализовать методы класса. Статические же методы использу ются редко, так как методы класса или функции модуля представля ют собой более привлекательную альтернативу. Встроенная функция repr() вызывает специальный метод __repr__() объекта. Желательно, чтобы выполнялось eval(repr(x)) == x, и мы увидели, как реализовать поддержку такой возможности. Когда от сутствует возможность создать строку репрезентативной формы, кото рую можно передавать функции eval(), мы используем метод ob ject.__repr__() базового класса для воспроизведения репрезентатив ной формы в стандартном формате, несовместимом с функцией eval(). Проверка типа объекта достаточно эффективно может быть выполнена с помощью встроенной функции isinstance(), хотя пуристы объектно ориентированного стиля почти наверняка предпочли бы избегать ее использования. Доступ к методам базового класса выполняется по средством вызова встроенной функции super(), которая является ос новным способом избежать попадания в бесконечную рекурсию, когда возникает необходимость вызвать метод базового класса в переопреде ленной версии этого же метода в подклассе. Функциигенераторы и методыгенераторы обеспечивают средство вы полнения отложенных вычислений. Они возвращают (посредством выражения yield) значения по одному за запрос и возбуждают исклю чение StopIteration, когда (если это происходит) заканчиваются воз вращаемые значения. Генераторы могут использоваться везде, где мо гут использоваться итераторы; и для конечных генераторов все их зна чения могут быть извлечены в кортеж или в список, если передать ите ратор, возвращаемый генератором, функции tuple() или list(). Объектноориентированный подход практически всегда упрощает про граммный код в сравнении с исключительно процедурным подходом. Создавая свои классы, мы можем гарантировать доступность только допустимых операций (так как мы реализуем только соответствующие методы) и гарантировать, что ни одна из операций не переведет объект в недопустимое состояние (например, используя свойства для провер ки присваиваемых значений). Начав использовать объектноориенти рованный стиль, мы практически наверняка уйдем от использования глобальных структур данных и глобальных функций, оперирующих этими данными, создавая свои классы и реализуя методы, применяе мые к ним. Объектноориентированный подход позволяет упаковы вать в единую структуру данные и методы для работы с ними. Это по могает не запутаться во всех наших данных и функциях и упрощает создание программ, простых в сопровождении, так как функциональ ность хранится отдельно, в виде самостоятельных классов. |