Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
Синтаксис Используется Описание __delattr__(self, name) del x.n Удаляет атрибут n из объекта x __dir__(self) dir(x) Возвращает список имен атри бутов объекта x __getattr__(self, name) v = x.n Возвращает значение атрибута n объекта x, если он существует __getattribute__(self, name) v = x.n Возвращает значение атрибута n объекта x; подробности в тексте __setattr__(self, name, value) x.n = v Присваивает значение v атрибу ту n объекта x Класс Image.ру , стр. 306 Улучшенные приемы объектно/ориентированного программирования 425 ключается в обслуживании всех свойств класса Image, доступных толь ко для чтения: def __getattr__(self, name): if name == "colors": return set(self.__colors) classname = self.__class__.__name__ if name in frozenset({"background", "width", "height"}): return self.__dict__["_{0}__{1}".format(classname, name)] raise AttributeError("'{0}' object has no attribute '{1}'" .format(classname, name)) Если попытаться обратиться к атрибуту объекта и атрибут не будет об наружен, интерпретатор вызовет метод __getattr__() (при условии, что класс предоставляет его реализацию и не был переопределен метод __getattribute__() ) с именем атрибута в качестве параметра. Реализа ция метода __getattr__() должна возбуждать исключение AttributeEr ror , если она не обслуживает указанный атрибут. Например, если в программе производится обращение к атрибуту ima ge.colors и интерпретатор не находит его, будет произведен вызов ме тода Image.__getattr__(image, "colors"). В данном случае метод __get attr__() обслужит атрибут с именем "colors" и вернет копию множест ва цветов, использующихся в изображении. Другие атрибуты являются неизменяемыми объектами, поэтому воз врат прямых ссылок на эти атрибуты не таит в себе никакой угрозы. Для каждого атрибута можно было бы предусмотреть отдельную инст рукцию elif, как показано ниже: elif name == "background": return self.__background Но вместо этого мы использовали более компактное решение. Зная, что все неспециальные атрибуты объекта хранятся в словаре self. __dict__ , мы предпочли обращаться к ним напрямую. Для частных ат рибутов (имена которых начинаются с двух символов подчеркивания) производится приведение их имен к форме _className__attributeName, и мы должны учитывать это обстоятельство при извлечении значений атрибутов из частного словаря объекта. Чтобы выполнить приведение имени при поиске частного атрибута и воссоздать корректный текст при возбуждении исключения Attribu teError , нам необходимо знать имя класса, которому принадлежит ме тод. (Это может быть не класс Image, потому что объект может оказать ся экземпляром подкласса, наследующего класс Image.) Каждый объ ект имеет специальный атрибут __class__, поэтому внутри методов все гда можно обратиться к атрибуту self.__class__, не рискуя попасть в бесконечную рекурсию. Обратите внимание на тонкое различие: при использовании метода __getattr__() и выражения self.__class__ обеспечивается доступ к ат 426 Глава 8. Усовершенствованные приемы программирования рибуту экземпляра класса (который может быть подклассом), а при прямом обращении к атрибуту используется класс, в котором этот ат рибут был определен. Нам осталось рассмотреть еще один специальный метод – метод __getattribute__() . Если метод __getattr__() вызывается в по следнюю очередь, когда выполняется поиск (неспециальных) атрибутов, то метод __getattribute__() вызывается в первую очередь, при каждом обращении к любому атрибуту. Хотя в оп ределенных случаях метод __getattribute__() может оказаться не только полезным, но и необходимым, тем не менее переопре деление этого метода может оказаться непростым делом. При переопределении особое внимание следует уделять тому, чтобы исключить возможность рекурсивного вызова – избежать ре курсии в таких случаях часто удается с помощью вызовов su per().__getattribute__() или object.__getattribute__(). Кроме того, поскольку метод __getattribute__() вызывается при обра щении к любому атрибуту, его переопределение легко может привести к потере производительности по сравнению с прямым доступом к атрибутам или свойствам. Ни один из классов, ко торые приводятся в этой книге, не переопределяет этот метод. Функторы В языке Python объектами функций являются ссылки на любые вы зываемые объекты, такие как функции, лямбдафункции или методы. Под это определение также подпадают и классы, поскольку ссылки на классы можно вызывать как функции, которые при вызове возвраща ют объект данного класса, например x = int(5). В информатике функ тором называется объект, который может вызываться, как если бы он был функцией, поэтому в терминах языка Python функтор является разновидностью объекта функции. Любой класс, имеющий специаль ный метод __call__(), является функтором. Главное достоинство функ торов заключается в том, что они могут поддерживать некоторую ин формацию о состоянии. Например, можно создать функтор, который всегда удаляет основные знаки пунктуации с обоих концов строки, как показано ниже: strip_punctuation = Strip(",;:.!?") strip_punctuation("Land ahoy!") # вернет: 'Land ahoy' Здесь создается экземпляр функтора Strip, инициализированный зна чением ",;:.!?". Всякий раз, когда будет вызываться экземпляр этого функтора, он будет возвращать полученную строку с отброшенными знаками пунктуации. Ниже приводится полная реализация класса Strip : Улучшенные приемы объектно/ориентированного программирования 427 class Strip: def __init__(self, characters): self.characters = characters def __call__(self, string): return string.strip(self.characters) Того же эффекта можно было бы добиться с помощью простой функ ции или лямбдафункции, но когда необходимо хранить чуть больше информации о состоянии или выполнять более сложную обработку, часто правильным решением будет использование функтора. Способность функтора сохранять информацию о состоянии с помощью класса обеспечивает ему высокую гибкость и чрезвычайно широкие возможности, но иногда такая гибкость и широта оказываются из лишними. Существует еще один способ сохранять информацию о со стоянии, который заключается в использовании замыканий. Замыка ние – это функция или метод, которые запоминают некоторое состоя ние. Например: def make_strip_function(characters): def strip_function(string): return string.strip(characters) return strip_function strip_punctuation = make_strip_function(",;:.!?") strip_punctuation("Land ahoy!") # вернет: 'Land ahoy' Функция make_strip_function() принимает в качестве единственного аргумента строку символов, которые следует удалять, и возвращает функцию strip_function(), принимающую строковый аргумент и уда ляющую из него символы, полученные в момент создания замыкания. Мы можем создать произвольное число экземпляров класса Strip, ка ждый со своим набором удаляемых символов, и точно так же мы мо жем создать произвольное число замыканий, каждое со своим набором символов. Классическим примером использования функторов может служить ключевая функция, применяемая при сортировке. Ниже приводится универсальный класс функтора SortKey (из файла SortKey.py): class SortKey: def __init__(self, *attribute_names): self.attribute_names = attribute_names def __call__(self, instance): values = [] for attribute_name in self.attribute_names: values.append(getattr(instance, attribute_name)) return values 428 Глава 8. Усовершенствованные приемы программирования Когда создается объект SortKey, он сохраняет кортеж с именами атри бутов, с которыми он был инициализирован. Когда производится вы зов объекта, создается список значений атрибутов для заданного эк земпляра, следующих в том же порядке, в каком они были указаны при инициализации объекта SortKey. Например, представим, что у нас имеется класс Person: class Person: def __init__(self, forename, surname, email): self.forename = forename self.surname = surname self.email = email Допустим, что у нас имеется список people объектов Person. Тогда этот список можно отсортировать по фамилиям людей следующим спосо бом: people.sort(key=SortKey("surname")). Если в списке присутствуют одинаковые фамилии, то можно отсортировать список сначала по фа милиям, а потом по именам: people.sort(key=SortKey("surname", "fore name")) . А если в списке присутствует набор одинаковых фамилий и имен, можно включить в сортировку еще и адреса электронной поч ты. Безусловно, точно так же можно было бы отсортировать список сначала по именам, а потом по фамилиям, достаточно лишь изменить порядок следования атрибутов, передаваемых функтору SortKey. Другой способ добиться того же эффекта, но вообще без создания функ тора, заключается в использовании функции operator.attrgetter() из модуля operator. Например, сортировку списка по фамилиям можно было бы выполнить так: people.sort(key=operator.attrgetter("surname")). А сортировку по фамилиям и именам так: people.sort(key=opera tor.attrgetter("surname", "forename")) . Функция operator.attrgetter() возвращает функцию (замыкание), при обращении в контексте объек та возвращающую атрибуты объекта, имена которых были указаны при создании замыкания. В языке Python функторы используются реже, чем в других языках программирования, поддерживающих такую возможность, потому что в языке Python имеются другие средства достижения того же эф фекта, например, замыкания и функции доступа к атрибутам. Менеджеры контекста Менеджеры контекста позволяют упростить программный код, гаран тируя выполнение определенных операций до и после выполнения не которого блока программного кода. Такое поведение обусловлено тем, что менеджеры контекста определяют два специальных метода – __enter__() и __exit__(), которые интерпретируются особым образом в области видимости инструкции with. Когда с помощью инструкции with создается менеджер контекста, автоматически вызывается его ме Улучшенные приемы объектно/ориентированного программирования 429 тод __enter__(), а когда поток выполнения покидает область видимости менеджера контекста, автоматически вызывается его метод __exit__(). У нас имеется возможность создавать свои собственные менеджеры контекста или использовать предопределенные – как будет показано ниже в этом подразделе объекты файлов, возвращаемые встроенной функцией open(), являются менеджерами контекста. Ниже приводит ся синтаксис использования менеджера контекста: with expression as variable: suite Выражение expression должно быть менеджером контекста или вос производить его. Если в инструкции указана необязательная часть as variable , в переменную variable записывается ссылка на объект, воз вращаемый методом __enter__() менеджера контекста (зачастую это сам менеджер контекста). Поскольку менеджеры контекста гаранти руют вызов метода __exit__() (даже в случае исключений), во многих ситуациях они могут использоваться для устранения блоков finally. Некоторые типы данных в языке Python являются менеджерами кон текста, например, все объекты файлов, создаваемых функцией open(); поэтому у нас имеется возможность отказаться от использования бло ков finally при работе с файлами, как это показано в следующих двух эквивалентных фрагментах (если исходить из предположения, что гдето в другом месте присутствует определение функции process()): fh = None try: fh = open(filename) for line in fh: process(line) try: except EnvironmentError as err: with open(filename) as fh: print(err) for line in fh: finally: process(line) if fh is not None: except EnvironmentError as err: fh.close() print(err) Объект файла является менеджером контекста, реализация метода __exit__() которого всегда закрывает файл, если он был открыт. Метод __exit__() будет выполняться независимо от того, возникло исключе ние или нет, но во втором случае исключение продолжит свое распро странение вверх по стеку возвратов. Эта особенность гарантирует, что файл будет закрыт и у нас останется возможность перехватить и обра ботать любую ошибку, в данном случае – вывести сообщение. В действительности менеджеры контекстов не обязаны обеспечивать дальнейшего распространения исключений, но это привело бы к со крытию любых исключений, что почти всегда оказалось бы программ ной ошибкой. Все встроенные менеджеры контекста и менеджеры 430 Глава 8. Усовершенствованные приемы программирования контекста из стандартной библиотеки обеспечивают дальнейшее рас пространение исключений. Иногда бывает необходимо одновременно использовать два или более менеджеров контекста. Например: try: with open(source) as fin: with open(target, "w") as fout: for line in fin: fout.write(process(line)) except EnvironmentError as err: print(err) Здесь выполняется чтение строк из исходного файла и запись обрабо танных строк в выходной файл. Использование вложенных инструкций with может быстро привести к непомерному увеличению отступов. К счастью, модуль contextlib из стандартной библиотеки предоставляет дополнительную поддержку менеджеров контекста, включая функцию context.nest(), которая по зволяет обрабатывать два или более менеджеров контекста одной ин струкцией with. Ниже приводится видоизмененная версия программ ного кода, который только что был продемонстрирован, в которой мы опустили строки, оставшиеся без изменений: try: with contextlib.nested(open(source), open(target, "w")) as ( fin, fout): for line in fin: Менеджерами контекста являются не только объекты файлов. Например, некоторые классы, связанные с реа лизацией многопоточной модели выполнения, использу ют менеджеры контекста для установки блокировок. Ме неджеры контекста могут использоваться также с чис лами decimal.Decimal, что очень удобно при реализации вычислений с определенными параметрами (например, с различной точностью). Если возникает необходимость реализовать собственный менеджер контекста, следует создать класс, предоставляющий два метода: __enter__() и __exit__(). Всякий раз, когда инструкция with будет при меняться к экземпляру такого класса, интерпретатор автоматически будет вызывать его метод __enter__(), а возвращаемое им значение бу дет присваиваться переменной в части as variable (или просто отбра сываться, если переменная не указана). Когда поток выполнения бу дет покидать область видимости инструкции with, интерпретатор бу дет вызывать метод __exit__() (с информацией об исключении, если оно возникло, в виде аргумента). Многопоточ ная модель выполнения, стр. 467 Улучшенные приемы объектно/ориентированного программирования 431 Предположим, что нам требуется выполнить некоторые операции над списком в атомарном режиме, то есть либо все операции должны быть выполнены, либо ни одна из них, чтобы получившийся список всегда находился в предсказуемом состоянии. Например, допустим, что у нас имеется список целых чисел и нам требуется добавить одно число, уда лить одно число и изменить пару чисел, причем все это должно быть выполнено как единая операция. Реализовать это можно следующим способом: try: with AtomicList(items) as atomic: atomic.append(58289) del atomic[3] atomic[8] = 81738 atomic[index] = 38172 except (AttributeError, IndexError, ValueError) as err: print("no changes applied:", err) Если в ходе выполнения операций никаких исключений не возникло, все операции будут применены к оригинальному списку (items), но ес ли возникло исключение, список останется без изменений. Ниже при водится реализация менеджера контекста AtomicList: class AtomicList: def __init__(self, alist, shallow_copy=True): self.original = alist self.shallow_copy = shallow_copy def __enter__(self): self.modified = (self.original[:] if self.shallow_copy else copy.deepcopy(self.original)) return self.modified def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.original[:] = self.modified При создании объекта AtomicList мы сохраняем ссылку на оригинальный список. Обратите внимание на флаг, который определяет, какое копирование будет приме няться к списку – поверхностное или глубокое. (Поверх ностное копирование прекрасно подходит для списков чисел или строк, но если список содержит другие списки или другие коллекции, поверхностного копирования бу дет недостаточно.) Затем, когда менеджер контекста AtomicList используется в инструк ции with, вызывается его метод __enter__(). В этот момент создается и возвращается копия списка, чтобы все изменения выполнялись в копии. Поверхно стное и глубокое копирование, стр. 173 432 Глава 8. Усовершенствованные приемы программирования По достижении конца области видимости инструкции with вызывается метод __exit__(). Если в процессе работы исключений не возникло, ар гумент exc_type («exception type» – тип исключения) будет содержать значение None, откуда следует, что можно безопасно заместить элемен ты оригинального списка элементами модифицированного списка. (Здесь нельзя просто использовать инструкцию self.original = self.mo dified , потому что она просто заменит одну ссылку на объект другой ссылкой на объект и не окажет никакого воздействия на оригиналь ный список.) Но если было возбуждено исключение, метод ничего не делает с оригинальным списком, а модифицированный список просто уничтожается. Возвращаемое значение метода __exit__() используется интерпретато ром, чтобы определить, следует ли продолжить распространение ис ключения, если оно возникло. Значение True свидетельствует о том, что метод выполнил обработку исключения и дальнейшее распростра нение исключения не требуется. В большинстве случаев мы будем воз вращать значение False или некоторое выражение, которое в логиче ском контексте дает значение False, чтобы обеспечить возможность распространения исключений. В отсутствие явной инструкции return наш метод __exit__() будет возвращать значение None, которое в логи ческом контексте дает значение False, в результате чего любые исклю чения будут продолжать свое распространение. В главе 10 мы будем использовать собственный менеджер контекста, чтобы обеспечить закрытие сетевых подключений и сжатых файлов, а в главе 9 – некоторые менеджеры контекста из модуля threading – для проверки отсутствия взаимоисключающих блокировок. Кроме то го, при работе над упражнениями к этой главе у нас появится шанс создать более универсальный менеджер контекста для выполнения атомарных операций. |