Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
Дескрипторы Дескрипторы – это классы, которые обеспечивают доступ к атрибутам других классов. Любой класс, реализующий один или более специаль ных методов дескрипторов – __get__(), __set__() и __delete__(), назы вается дескриптором (и может использоваться как дескриптор). Реализации встроенных функций property() и classmethod() использу ют в своей работе дескрипторы. Чтобы разобраться в дескрипторах, важно понять, что хотя они и создаются в классах как атрибуты клас сов, тем не менее интерпретатор обращается к ним как к экземплярам классов. Чтобы пояснить вышесказанное, представим, что у нас имеется класс, экземпляры которого хранят некоторые строки. Нам необходимо обес печить доступ к строкам обычным способом, например, как к свойст ву, а также необходимо обеспечить возможность получения версий строк, в которых были бы экранированы служебные символы XML. Улучшенные приемы объектно/ориентированного программирования 433 Одним из простых решений было бы сразу же создавать копии экрани рованных строк. Но если у нас имеются тысячи строк и нам необходи мо прочитать лишь несколько экранированных версий строк, такое решение привело бы к неоправданному перерасходу памяти и вычис лительных мощностей. Поэтому мы создадим дескриптор, который бу дет возвращать экранированные строки по требованию, не сохраняя их в памяти. Сначала рассмотрим клиентский класс (классвладелец), то есть класс, который использует дескриптор: class Product: __slots__ = ("__name", "__description", "__price") name_as_xml = XmlShadow("name") description_as_xml = XmlShadow("description") def __init__(self, name, description, price): self.__name = name self.description = description self.price = price Единственное, что мы опустили, – это определение свойств. Свойство name доступно только для чтения, а свойства description и price доступ ны для чтения и для записи. Все эти свойства определяются обычным способом. (Полный программный код вы найдете в файле XmlSha dow.py .) Мы использовали переменную __slots__, чтобы у класса не бы ло атрибута __dict__ и его экземпляры могли иметь только эти три ат рибута. Такое решение никак связано с использованием дескриптора и не является обязательным. Атрибуты класса name_as_xml и descripti on_as_xml определяются как экземпляры дескриптора XmlShadow. И хотя объекты класса Product не имеют атрибутов name_as_xml и descripti on_as_xml , тем не менее благодаря дескриптору мы имеем возможность написать следующий программный код (фрагмент взят из доктестов модуля): >>> product = Product("Chisel <3cm>", "Chisel & cap", 45.25) >>> product.name, product.name_as_xml, product.description_as_xml ('Chisel <3cm>', 'Chisel <3cm>', 'Chisel & cap') Такое возможно благодаря тому, что при попытке обратиться к атри буту, например, name_as_xml, интерпретатор обнаруживает, что класс Product имеет дескриптор с таким именем и использует этот дескрип тор для получения значения атрибута. Ниже приводится полное опре деление класса XmlShadow: class XmlShadow: def __init__(self, attribute_name): self.attribute_name = attribute_name def __get__(self, instance, owner=None): return xml.sax.saxutils.escape( getattr(instance, self.attribute_name)) 434 Глава 8. Усовершенствованные приемы программирования В момент создания объектов name_as_xml и description_as_xml им посред ством вызова метода инициализации класса XmlShadow передаются име на соответствующих атрибутов класса Product, чтобы дескриптор знал, с каким атрибутом ему предстоит работать. Затем, когда интерпрета тор выполняет поиск атрибута name_as_xml или description_as_xml, он вызывает метод __get__() дескриптора. Аргумент self – это экземпляр дескриптора, аргумент instance – это экземпляр класса Product (то есть значение ссылки self экземпляра класса Product), а аргумент owner – это класс владельца (в данном случае – класс Product). Для по лучения значения соответствующего атрибута экземпляра класса Product используется функция getattr() (в данном случае – значение соответствующего свойства), которая возвращает его экранированную версию. В случае, когда в программе только для малой части всех строк необ ходимо предоставить экранированные версии строк, но эти строки очень длинные, а обращения к ним следуют достаточно часто, можно было бы предусмотреть использование кэша. Например: class CachedXmlShadow: def __init__(self, attribute_name): self.attribute_name = attribute_name self.cache = {} def __get__(self, instance, owner=None): xml_text = self.cache.get(id(instance)) if xml_text is not None: return xml_text return self.cache.setdefault(id(instance), xml.sax.saxutils.escape( getattr(instance, self.attribute_name))) Здесь в качестве ключа используется уникальный числовой идентифи катор, а не сам экземпляр, потому что ключи словаря должны быть хе шируемыми (каковыми и являются числовые идентификаторы), но нам не хотелось бы накладывать такое ограничение на классы, ис пользующие дескриптор CachedXmlShadow. Ключи необходимы, потому что дескрипторы создаются для всего класса, а не для его экземпля ров. (Метод dict.setdefault() возвращает значение для заданного клю ча или, если элемента с таким ключом нет, создает новый элемент с за данным ключом и значением и возвращает значение, что весьма удоб но для нас.) Получив представление о том, как могут использоваться дескрипторы для генерирования данных без необходимости сохранять их, перейдем теперь к рассмотрению дескриптора, который может использоваться для сохранения всех атрибутов данных объекта, сняв с объекта необ ходимость чтолибо сохранять. В следующем примере мы будем ис пользовать словарь, но в более жизненной ситуации данные можно бы ло бы сохранять в файле или в базе данных. Ниже приводится начало Улучшенные приемы объектно/ориентированного программирования 435 определения класса Point, использующего дескриптор (из файла Exter nalStorage.py ). class Point: __slots__ = () x = ExternalStorage("x") y = ExternalStorage("y") def __init__(self, x=0, y=0): self.x = x self.y = y Определив пустой кортеж в качестве значения атрибута __slots__, мы тем самым гарантируем, что класс вообще не будет иметь никаких ат рибутов данных. При попытке выполнить присваивание атрибуту self.x интерпретатор обнаружит наличие дескриптора с именем «x» и вызовет его метод __set__(). Остальная часть определения класса здесь не показана, но она полностью повторяет определение класса Point из главы 6. Ниже приводится полное определение класса деск риптора ExternalStorage: class ExternalStorage: __slots__ = ("attribute_name",) __storage = {} def __init__(self, attribute_name): self.attribute_name = attribute_name def __set__(self, instance, value): self.__storage[id(instance), self.attribute_name] = value def __get__(self, instance, owner=None): if instance is None: return self return self.__storage[id(instance), self.attribute_name] Каждый объект класса ExternalStorage имеет единственный атрибут данных, attribute_name, который хранит имя атрибута данных класса владельца. Всякий раз, когда выполняется присваивание значения ат рибуту, оно сохраняется в частном словаре класса __storage. Точно так же, когда производится попытка прочитать значение атрибута, оно из влекается из словаря __storage. Как и в любых других методах дескриптора, аргумент self ссылается на экземпляр дескриптора, а instance – это ссылка self для объекта, содержащего дескриптор, то есть здесь self ссылается на объект клас са ExternalStorage, а instance – на объект класса Point. Несмотря на то, что атрибут __storage является атрибутом класса, тем не менее к нему можно обращаться следующим образом: self.__storage (точно так же, как можно обращаться к некоторому методу класса self.method() ), потому что интерпретатор, не обнаружив его среди 436 Глава 8. Усовершенствованные приемы программирования атрибутов экземпляра, найдет его среди атрибутов класса. Единствен ный недостаток такого подхода состоит в том, что если экземпляр бу дет иметь атрибут с именем, совпадающим с именем атрибута класса, при попытке обратиться к этому имени всегда будет использоваться атрибут экземпляра. (Если это действительно необходимо, к атрибуту класса всегда можно обратиться, квалифицировав его именем класса, то есть ExternalStorage.__storage. Хотя такое жесткое определение в общем случае может отрицательно сказаться при создании подклас сов, к частным атрибутам это не относится, так как механизм интер претатора приведения имен все равно включает имя класса в имена та ких атрибутов.) Здесь используется немного более сложная, чем прежде, реализация специального метода __get__(), потому что мы предусмотрели возмож ность обращения объекта ExternalStorage к самому себе. Например, представим, что у нас имеется экземпляр p = Point(3, 4), в этом случае доступ к координате x можно получить, обратившись к атрибуту p.x, а доступ к объекту ExternalStorage, хранящему все координаты x, обра тившись к Point.x. В завершение обсуждения дескрипторов создадим дескриптор Property, имитирующий поведение встроенной функции property() по крайней мере в отношении реализации методов доступа. Полный программный код находится в файле Property.py. Ниже приводится полное определе ние класса NameAndExtension, использующего этот дескриптор: class NameAndExtension: def __init__(self, name, extension): self.__name = name self.extension = extension @Property # Задействуется нестандартный дескриптор Property def name(self): return self.__name @Property # Задействуется нестандартный дескриптор Property def extension(self): return self.__extension @extension.setter # Задействуется нестандартный дескриптор Property def extension(self, extension): self.__extension = extension Порядок использования дескриптора точно такой же, как и в случае использования встроенных декораторов @property и @propertyName.set ter . Ниже приводится начало определения дескриптора Property: class Property: def __init__(self, getter, setter=None): self.__getter = getter Улучшенные приемы объектно/ориентированного программирования 437 self.__setter = setter self.__name__ = getter.__name__ Метод инициализации класса принимает одну или две функции в каче стве аргументов. Если он используется как декоратор, он просто полу чит декорируемую функцию, которая станет функцией чтения, а в ка честве функции записи будет установлено значение None. В качестве имени свойства здесь используется имя метода чтения. Для каждого свойства, для которого уже определена функция чтения, имеется воз можность определить функцию записи, используя имя свойства. def __get__(self, instance, owner=None): if instance is None: return self return self.__getter(instance) Когда выполняется обращение к свойству, возвращается результат вызова функции чтения, которой в первом аргументе передается эк земпляр класса. На первый взгляд запись self.__getter() напоминает вызов метода, но в действительности это не так. На самом деле self.__getter – это атрибут, который содержит ссылку на заданный ме тод. Поэтому фактически сначала происходит извлечение значения атрибута (self.__getter), а затем это значение вызывается как функ ция (). А так как атрибут вызывается как функция, а не как метод, мы должны явно передать ей соответствующий объект self. Внутри мето дов дескриптора ссылка на сам объект (экземпляр класса, использую щего дескриптор) называется instance (так как self – это объект деск риптора). То же относится и к методу __set__(). def __set__(self, instance, value): if self.__setter is None: raise AttributeError("'{0}' is readonly".format( self.__name__)) return self.__setter(instance, value) В случае отсутствия функции записи возбуждается исключение Attri buteError ; в противном случае функция вызывается и ей передаются ссылка на экземпляр класса и новое значение атрибута. def setter(self, setter): self.__setter = setter return self.__setter Этот метод вызывается, когда интерпретатор достигает, например, вы зова @extesion.setter, с декорируемой функцией в качестве аргумента. Он сохраняет указанный метод записи (который теперь может вызы ваться методом __set__()) и возвращает функцию записи, потому что любой декоратор должен возвращать декорированную им версию функции или метода. Мы рассмотрели три совершенно разные области использования деск рипторов. Дескрипторы представляют собой очень гибкое и мощное 438 Глава 8. Усовершенствованные приемы программирования средство, позволяющее выполнять за кулисами самые разные дейст вия и выглядеть при этом простыми атрибутами клиентского класса (класса владельца). Декораторы классов Точно так же, как имеется возможность создавать декораторы для функций и методов, можно создавать декораторы для целых классов. Декораторы классов принимают класс (результат действия инструк ции class) и должны возвращать класс – обычно модифицированную версию декорируемого класса. В этом подразделе мы познакомимся с двумя декораторами классов и рассмотрим их реализацию. В главе 6 мы создали собственный тип коллекции Sort edList , который содержит обычный список в виде част ного атрибута self.__list. Восемь методов этого класса просто перекладывают свою работу на методы частного атрибута. В качестве примера ниже приводится реализа ция методов SortedList.clear() и SortedList.pop(): def clear(self): self.__list = [] def pop(self, index=1): return self.__list.pop(index) В методе clear() не делается ничего особенного, так как тип list не имеет соответствующего метода, но в методе pop() и еще в шести дру гих методах, которые делегируются классом SortedList, можно просто вызывать соответствующие методы класса list. Реализовать это мож но с помощью декоратора классов @delegate, реализацию которого можно найти в модуле Util, поставляемом с примерами к книге. Ниже приводится начало определения новой версии класса SortedList: @Util.delegate("__list", ("pop", "__delitem__", "__getitem__", "__iter__", "__reversed__", "__len__", "__str__")) class SortedList: Первый аргумент – это имя атрибута, которому будут делегированы операции, а второй аргумент – это последовательность из одного или более методов, которые должен будет реализовать декоратор dele gate() , чтобы избавить нас от этой рутины. Этот подход используется при определении класса SortedList, в файле SortedListDelegate.py, по этому в нем отсутствует явная реализация перечисленных методов, но несмотря на это он полностью их поддерживает. Ниже приводится оп ределение декоратора классов, который создает реализацию методов: def delegate(attribute_name, method_names): def decorator(cls): nonlocal attribute_name if attribute_name.startswith("__"): Класс SortedList , стр. 314 Улучшенные приемы объектно/ориентированного программирования 439 attribute_name = "_" + cls.__name__ + attribute_name for name in method_names: setattr(cls, name, eval("lambda self, *a, **kw: " "self.{0}.{1}(*a, **kw)".format( attribute_name, name))) return cls return decorator Мы не можем использовать простой декоратор, т. к. декоратору требу ется передавать аргументы, поэтому мы создали функцию, которая принимает наши аргументы и возвращает декоратор класса. Сам деко ратор принимает единственный аргумент – класс (так же как декора тор функций принимает функцию или метод в виде единственного ар гумента). Мы вынуждены использовать инструкцию nonlocal, потому что вло женная функция обращается к аргументу attribute_name, находящему ся в области видимости внешней функции. А нам, в случае необходи мости, нужна возможность корректировать имя атрибута, чтобы учесть приведение имен частных атрибутов. Декоратор обладает весь ма простым поведением: он выполняет итерации по всем именам мето дов, которые были переданы функции delegate(), и для каждого из них создает новый метод, который устанавливается в качестве атрибу та класса с заданным именем метода. Для создания каждого из делегируемых методов используется функ ция eval(), которая может использоваться для выполнения единствен ной инструкции lambda, воспроизводящей метод или функцию. Напри мер, данный программный код воспроизводит метод pop(), как показа но ниже: lambda self, *a, **kw: self._SortedList__list.pop(*a, **kw) Использование формы представления аргументов * и ** позволяет лю бым аргументам, даже относящимся к делегируемым методам, прини мать требуемую форму списка аргументов. Например, метод list.pop() принимает единственный аргумент – номер позиции в списке (или ни одного аргумента, в этом случае используется значение по умолчанию – номер позиции последнего элемента). Этот прием пригоден, даже ко гда методу передается неверное число аргументов или недопустимые значения, потому что в этом случае вызываемый метод класса list воз будит соответствующее исключение. Второй декоратор классов, который мы рассмотрим, так же будет применяться к классу, созданному в главе 6. Когда мы создавали класс FuzzyBool, мы упоминали, что при наличии реализации всего двух специальных мето дов, __lt__() и __eq__() (выполняющих операции < и ==), остальные методы сравнения будут генерироваться авто матически. Но тогда мы не показали полное начало оп ределения класса: Класс FuzzyBool , стр. 291 440 Глава 8. Усовершенствованные приемы программирования @Util.complete_comparisons class FuzzyBool: Остальные четыре метода сравнивания определяются декоратором complete_comparsions() класса. Используя только реализацию поддерж ки оператора < (или < и ==), декоратор воспроизводит реализацию не достающих методов, используя следующие логические соотношения: x = y ⇔ ¬ (x < y ∨ y < x) x ≠ y ⇔ ¬ (x = y) x > y ⇔ y < x x ≤ y ⇔ ¬ (y < x) x ≥ y ⇔ ¬ (x < y) Если декорируемый класс содержит реализацию операторов < и ==, де коратор будет использовать обе реализации; при этом декоратор пе рейдет к воспроизводению всех методов сравнивания через оператор <, если в классе присутствует реализация только этого оператора. (В дей ствительности интерпретатор автоматически воспроизводит поддерж ку оператора >, если поддерживается оператор <; !=, если поддержива ется оператор == и >=, если поддерживается <=. Поэтому будет вполне достаточно реализовать всего три оператора: <, <= и ==, а реализацию поддержки остальных операторов оставить за интерпретатором. Одна ко при использовании декоратора класса этот минимум снижается до реализации единственного оператора <. Это, вопервых, очень удобно, а вовторых, гарантирует, что все операторы сравнивания будут ис пользовать одну и ту же непротиворечивую логику.) def complete_comparisons(cls): assert cls.__lt__ is not object.__lt__, ( "{0} must define < and ideally ==".format(cls.__name__)) if cls.__eq__ is object.__eq__: cls.__eq__ = lambda self, other: (not (cls.__lt__(self, other) or cls.__lt__(other, self))) cls.__ne__ = lambda self, other: not cls.__eq__(self, other) cls.__gt__ = lambda self, other: cls.__lt__(other, self) cls.__le__ = lambda self, other: not cls.__lt__(other, self) cls.__ge__ = lambda self, other: not cls.__lt__(self, other) return cls Одна из проблем, которую необходимо решить декоратору, состоит в том, что класс object, от которого в конечном счете происходят все остальные классы, определяет реализацию всех шести операторов сравнивания, возбуждающих исключение TypeError. Поэтому необхо димо узнать, были ли переопределены методы поддержки операторов < и == (и, следовательно, узнать, можно ли их использовать). Это легко можно сделать, сравнив соответствующие специальные методы деко рируемого класса с методами класса object. Если декорируемый класс не имеет собственной реализации поддерж ки оператора <, инструкция assert возбудит исключение, потому что |