Программирование на Python 3. Руководство издательство СимволПлюс
Скачать 3.74 Mb.
|
441 поддержка этого оператора является минимально необходимой. Если в классе присутствует поддержка оператора ==, мы будем использовать существующую реализацию; в противном случае создадим ее. После этого создаются остальные методы сравнивания, и возвращается класс, содержащий реализацию всех шести методов. Использование декораторов классов является, пожалуй, самым простым и самым легким способом изменения классов. Другой способ основан на использовании мета классов – эта тема будет рассматриваться ниже в этой главе. Абстрактные базовые классы Абстрактным базовым классом (Abstract Base Class, ABC) называется класс, который не может использоваться для создания объектов. На значение таких классов состоит в определении интерфейсов, то есть в том, чтобы перечислить методы и свойства, которые должны быть реализованы в классах, наследующих абстрактный базовый класс. Это удобно, так как можно использовать абстрактный базовый класс как своего рода договоренность – договоренность о том, что любые порож денные классы обеспечат реализацию методов и свойств, объявленных в абстрактном базовом классе. 1 Абстрактные классы – это классы, имеющие как минимум один абст рактный метод или свойство. Объявления абстрактных методов могут не содержать их реализацию (то есть блок кода метода состоит из един ственной инструкции pass, или, если необходимо предусмотреть обяза тельное переопределение метода в подклассах, из инструкции raize NotImplementedError() ) или могут иметь действующую реализацию, ко торая может вызываться подклассами, например, предусматриваю щую обработку общих случаев. Кроме того, абстрактные классы могут содержать обычные (то есть неабстрактные) методы и свойства. Классы, наследующие ABC, могут использоваться для создания экзем пляров, только если они переопределяют все унаследованные абст рактные методы и абстрактные свойства. Дочерние классы с помощью функции super() могут использовать версии абстрактных методов, имеющих действующую реализацию (даже если она состоит из единст венной инструкции pass). Все неабстрактные методы или свойства на следуются дочерними классами обычным образом. Любые абстракт ные базовые классы должны использовать метакласс abc.ABCMeta (из модуля abc) или метакласс от одного из его подклассов. Метаклассы мы будем рассматривать немного ниже. 1 Абстрактные базовые классы языка Python описываются в PEP 3119 (www.python.org/dev/peps/pep3119), где вы также найдете очень полезные объяснения, которые стоит прочитать. Метаклассы, стр. 452 442 Глава 8. Усовершенствованные приемы программирования В языке Python имеется две группы абстрактных базовых классов. Од на находится в модуле collections, а другая – в модуле numbers. Они по зволяют получать информацию об объекте; например, если у нас име ется переменная x, то мы можем определить, является она последова тельностью – с помощью функции isinstance(x,collections.MutableSe quence) или целым числом – с помощью инструкции isinstance(x, numbers.Integral) . Это особенно удобно, учитывая динамическую типи зацию в языке Python, когда нам не требуется знать точный тип объек та, а достаточно лишь убедиться, что он поддерживает операции, кото рые предполагается к нему применить. Числовые абстрактные классы и абстрактные классы коллекций перечислены в табл. 8.3 и 8.4. Еще одним основным абстрактным базовым классом является класс io.IO Base , который наследуется всеми классами, выполняющими работу с файлами и потоками. Таблица 8.3. Абстрактные базовые классы в модуле numbers ABC Наследует API Примеры Number object complex, decimal.Decimal, float, fractions.Fraction, int Complex Number == , !=, +, , *, /, abs(), bool(), complex() , conjugate(); а так же свойства real и imag complex , decimal.Decimal, float, fractions.Fraction, int Real Complex < , <=, ==, !=, >=, >, +, , *, /, //, %, abs(), bool() , complex(), conjugate(), divmod() , float(), math.ceil(), math.floor() , round(), trunc(); а также свойства real и imag decimal.Decimal , float, fractions.Fraction, int Rational Real < , <=, ==, !=, >=, >, +, , *, /, //, %, abs(), bool() , complex(), conjugate(), divmod() , float(), math.ceil(), math.floor() , round(), trunc(); а также свойства real, imag, numerator и denominator fractions.Fraction, int Integral Rational < , <=, ==, !=, >=, >, +, , *, /, //, %, <<, >> , , &, ^, |, abs(), bool(), complex(), conjugate() , divmod(), float(), math.ceil() , math.floor(), pow(), ro und() , trunc(); а также свойства real, imag , numerator и denominator int Улучшенные приемы объектно/ориентированного программирования 443 Таблица 8.4. Основные абстрактные базовые классы в модуле collections ABC Наследует API Примеры Callable object () Все функции, методы и лямбдафункции Container object in bytearray , bytes, dict , frozenset, list , set, str, tuple Hashable object hash() bytes , frozenset, str , tuple Iterable object iter() bytearray , bytes, collections.deque, dict , frozenset, list , set, str, tuple Iterator Iterable iter() , next() Sized object len() bytearray , bytes, collections.deque, dict , frozenset, list , set, str, tuple Mapping Container, Iterable, Sized == , !=, [], len(), iter(), in, get() , items(), keys(), val ues() dict MutableMapping Mapping == , !=, [], del, len(), iter() , in, clear(), get(), items() , keys(), pop(), pop item() , setdefault(), up date() , values() dict Sequence Container, Iterable, Sized [] , len(), iter(), rever sed() , in, count(), index() bytearray , bytes, list, str , tuple Mutable Sequence Container, Iterable, Sized [] , +=, del, len(), iter(), reversed() , in, append(), count() , extend(), index(), insert() , pop(), remove(), reverse() bytearray , list Set Container, Iterable, Sized < , <=, ==, !=, =>, >, &, |, ^, len() , iter(), in, isdis joint() frozenset , set MutableSet Set < , <=, ==, !=, =>, >, &, |, ^, &= , |=, ^=, =, len(), iter(), in , add(), clear(), dis card() , isdisjoint(), pop(), remove() set 444 Глава 8. Усовершенствованные приемы программирования Чтобы полностью интегрировать наши собственные числовые классы или классы коллекций, мы должны наследовать их от стандартных аб страктных базовых классов. Например, класс SortedList является по следовательностью, но если ничего не предпринять, то инструкция isinstance(L, collections.Sequence) вернет значение False. Чтобы испра вить этот недостаток, можно просто унаследовать соответствующий абстрактный базовый класс: class SortedList(collections.Sequence): После того как collections.Sequence будет использован в качестве базового класса, инструкция isinstance() бу дет возвращать True. Кроме того, в этом случае нам при дется реализовать методы __init__() (или __new__()), __getitem__() и __len__() (что мы уже сделали). Абстракт ный базовый класс collections.Sequence также предостав ляет неабстрактные реализации методов __contains__(), __iter__() , __reversed__(), count() и index(). Мы переоп ределили в классе SortedList все эти методы, но мы мог ли бы использовать версии этих методов из абстрактного базового класса, не создавая повторные реализации. Мы не можем объявить класс SortedList подклассом класса collections.MutableSequence , хотя список относится к ка тегории изменяемых объектов, потому что класс Sorted List не имеет всех методов, которые должны реализовать наследники класса collections.MutableSequence, – таких как __setitem__() и append(). (Реализация такой версии класса SortedList приводится в файле SortedListAbc.py. Альтернативный способ превращения класса SortedList в подкласс класса collections.Sequence будет описан в раз деле «Метаклассы».) Теперь, когда мы знаем, как создавать собственные классы с использо ванием стандартных абстрактных классов, перейдем к другому приме нению абстрактных базовых классов: для обеспечения соглашений об интерфейсе в наших собственных классах. Мы рассмотрим три раз личных примера, чтобы представить различные аспекты создания и использования абстрактных базовых классов. Начнем с очень простого примера, демонстрирующего, как организо вать работу со свойствами, доступными для чтения и для записи. Класс используется для представления бытовых приборов. Каждый объект класса, представляющий прибор, должен содержать строку с названием модели, доступную только для чтения, и цену, доступную для чтения и для записи. Нам также необходимо гарантировать пере определение метода __init__() базового абстрактного класса в классах наследниках. Ниже приводится определение абстрактного базового класса (из файла Appliance.py); мы опустили строку с инструкцией Метаклассы, стр. 452 Улучшенные приемы объектно/ориентированного программирования 445 import abc , которая необходима, чтобы получить доступ к функциям abstractmethod() и abstractproperty(), каждая из которых может исполь зоваться как декоратор: class Appliance(metaclass=abc.ABCMeta): @abc.abstractmethod def __init__(self, model, price): self.__model = model self.price = price def get_price(self): return self.__price def set_price(self, price): self.__price = price price = abc.abstractproperty(get_price, set_price) @property def model(self): return self.__model В качестве метакласса мы указали abc.ABCMeta, так как это является обязательным требованием при создании абстрактных классов. Безус ловно, точно так же можно было бы использовать любой из подклассов класса abc.ABCMeta. Метод __init__() объявлен абстрактным, чтобы га рантировать его переопределение в дочерних классах, и предусмотре на его реализация, которая, как ожидается (но не обязательно), будет использоваться классаминаследниками. Мы не можем использовать декоратор для создания абстрактного свойства, доступного для чтения и для записи; кроме того, мы не использовали частные имена для ме тодов чтения и записи, так как это привело бы к неудобствам при пере определении в подклассах. Свойство model не является абстрактным, поэтому его не обязательно переопределять в подклассах. Класс Appli ance не может использоваться для создания объектов, так как он содер жит абстрактные атрибуты. Ниже приводится пример его подкласса: class Cooker(Appliance): def __init__(self, model, price, fuel): super().__init__(model, price) self.fuel = fuel price = property(lambda self: super().price, lambda self, price: super().set_price(price)) Класс Cooker должен переопределить метод __init__() и свойство price. Переопределив свойство, мы просто переложили всю работу на базо вый класс. Свойство model, доступное только для чтения, наследуется в обычном порядке. Мы могли бы на основе класса Appliance создать намного больше классов, таких как Fridge, Toaster и т. д. 446 Глава 8. Усовершенствованные приемы программирования Следующий абстрактный базовый класс, который мы рассмотрим, еще короче. Это абстрактный класс функтора (в файле TextFilter.py), выполняющего фильтрацию текста: class TextFilter(metaclass=abc.ABCMeta): @abc.abstractproperty def is_transformer(self): raise NotImplementedError() @abc.abstractmethod def __call__(self): raise NotImplementedError() Абстрактный класс TextFilter вообще не содержит никакой функцио нальности – он существует исключительно ради того, чтобы опреде лить интерфейс, – в данном случае свойство is_transformer и метод __call__() , которые должны быть переопределены во всех его подклас сах. Поскольку абстрактные свойство и метод не имеют реализации, отсутствует возможность обращаться к ним из подклассов, поэтому при попытке задействовать их (например, с помощью функции su per() ) вместо выполнения безвредной инструкции pass возбуждается исключение. Ниже приводится пример простого подкласса: class CharCounter(TextFilter): @property def is_transformer(self): return False def __call__(self, text, chars): count = 0 for c in text: if c in chars: count += 1 return count Данный фильтр текста не является преобразователем, потому что он не изменяет заданный текст, а просто возвращает количество указан ных символов в тексте. Ниже приводится пример использования этого класса: vowel_counter = CharCounter() vowel_counter("dog fish and cat fish", "aeiou") # вернет: 5 Два других класса текстовых фильтров, RunLengthEncode и RunLengthDe code , являются преобразователями. Ниже приводятся примеры их ис пользования: rle_encoder = RunLengthEncode() rle_text = rle_encoder(text) Улучшенные приемы объектно/ориентированного программирования 447 rle_decoder = RunLengthDecode() original_text = rle_decoder(rle_text) Класс RunLengthEncode преобразует строку байтов в кодировке UTF8, замещая байты 0x00 последовательностью 0x00, 0x01, 0x00, и любые по следовательности, содержащие от трех до 255 одинаковых байтов, – последовательностями 0x00, количество, байт. Если в строке имеется много фрагментов, состоящих из четырех идущих подряд одинаковых символов, этот класс будет способен воспроизвести более короткую строку байтов, чем простая последовательность байтов в кодировке UTF8. Класс RunLengthDecode принимает строку байтов, созданную классом RunLengthEncode, и возвращает оригинальную строку. Ниже приводится начало определения класса RunLengthDecode: class RunLengthDecode(TextFilter): @property def is_transformer(self): return True def __call__(self, rle_bytes): Мы опустили тело метода __call__(), но вы можете увидеть его в файле с исходными текстами, среди примеров к этой книге. 1 Класс RunLength Encode имеет ту же структуру. Последний абстрактный базовый класс, который мы рассмотрим, опи сывает прикладной программный интерфейс (Application Program ming Interface, API) и предоставляет реализацию по умолчанию меха низма отмены изменений. Ниже приводится полное определение абст рактного класса (из файла Abstract.py): class Undo(metaclass=abc.ABCMeta): @abc.abstractmethod def __init__(self): self.__undos = [] @abc.abstractproperty def can_undo(self): return bool(self.__undos) @abc.abstractmethod def undo(self): assert self.__undos, "nothing left to undo" self.__undos.pop()(self) def add_undo(self, undo): self.__undos.append(undo) 1 TextFilter.py . – Прим. перев. 448 Глава 8. Усовершенствованные приемы программирования Методы __init__() и undo() должны переопределяться в дочерних классах, потому что оба они объявлены абстрактными. Точно так же должно переопределяться свойство can_undo, доступное только для чтения. Подклассы могут не переопределять метод add_undo(), хотя это и не возбраняется. Метод undo() таит в себе одну хитрость. Список self.__undos , как ожидается, должен хранить ссылки на методы. Каж дый метод в списке должен выполнять действия по отмене соответст вующих изменений – все станет намного понятнее, когда мы рассмот рим подкласс класса Undo, который приводится чуть ниже. То есть, чтобы выполнить отмену, из списка self.__undos извлекается послед ний метод отмены и затем вызывается как функция, которой в виде аргумента передается ссылка self. (Мы вынуждены передавать ссыл ку self, потому что в данном случае метод вызывается как функция, а не как метод.) Ниже приводится начало определения класса Stack. Он наследует класс Undo, поэтому любые действия, выполняемые над ним, можно от менить, вызвав метод Stack.undo() без аргументов: class Stack(Undo): def __init__(self): super().__init__() self.__stack = [] @property def can_undo(self): return super().can_undo def undo(self): super().undo() def push(self, item): self.__stack.append(item) self.add_undo(lambda self: self.__stack.pop()) def pop(self): item = self.__stack.pop() self.add_undo(lambda self: self.__stack.append(item)) return item Мы опустили методы Stack.top() и Stack.__str__(), поскольку ни в од ном из них не содержится ничего нового для нас, и ни один из них ни как не взаимодействует с базовым классом Undo. В случае со свойством can_undo и методом undo() мы просто перекладываем работу на базовый класс. Если бы они не были объявлены как абстрактные, нам вообще не пришлось бы переопределять их, чтобы добиться того же эффекта. Но в данном случае мы специально предусмотрели обязательное их пе реопределение в подклассах, чтобы реализация отмены выполнялась с учетом особенностей подкласса. Методы push() и pop() выполняют ос новную операцию и добавляют в список методов отмены функцию, Улучшенные приемы объектно/ориентированного программирования 449 с помощью которой можно будет выполнить отмену только что выпол ненной операции. Наибольшую пользу абстрактные классы приносят в крупных про граммах, в библиотеках и в прикладных платформах, где они помога ют обеспечить взаимодействие между классами независимо от того, кем они написаны, и от особенностей их реализации, потому что они будут обеспечивать прикладные интерфейсы, объявляемые абстракт ными базовыми классами. Множественное наследование Множественное наследование возникает там, где один класс наследует два или более других классов. Хотя язык Python (и, например, C++) полностью поддерживает множественное наследование, некоторые языки программирования, наиболее заметным из которых является язык Java, такой возможностью не обладают. Одна из проблем состоит в том, что множественное наследование может привести к тому, что один и тот же класс будет унаследован несколько раз (например, когда два базовых класса наследуют один общий класс), а это означает, что вызываемая версия метода, не реализованного в подклассе, но реали зованного в двух или более базовых классах (или в их базовых классах и т. д.), зависит от того, в каком порядке выполняется поиск в базовых классах, что может сделать классы, наследующие несколько классов, довольно неустойчивыми. Вообще говоря, множественного наследования можно избежать, при меняя простое наследование (один базовый класс) и добавляя мета классы, когда возникает необходимость в поддержке дополнительных API, поскольку, как будет показано в следующем подразделе, мета класс может использоваться, чтобы взять обязательство о поддержке определенного API, без фактического наследования какихлибо мето дов или атрибутов данных. Как вариант, при множественном наследо вании можно использовать один конкретный класс и один или более абстрактных классов – для обеспечения поддержки дополнительных API. Еще одно решение состоит в том, чтобы использовать простое на следование и агрегировать экземпляры других классов. Тем не менее в некоторых случаях множественное наследование пре доставляет очень удобное решение. Например, предположим, что нам необходимо создать новую версию класса Stack, рассматривавшегося в предыдущем подразделе, которая обеспечивала бы возможность за гружать и сохранять данные с помощью модуля pickle. Нам необходи мо добавить возможность загрузки и сохранения в нескольких клас сах, поэтому мы реализуем эту функциональность в виде отдельного класса: class LoadSave: def __init__(self, filename, *attribute_names): |