Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
Создание типа данных FuzzyBool с нуля означает, что мы должны соз дать атрибут для хранения значения типа FuzzyBool и все необходимые методы. Ниже приводится инструкция class и метод инициализации, взятые из файла FuzzyBool.py: class FuzzyBool: def __init__(self, value=0.0): self.__value = value if 0.0 <= value <= 1.0 else 0.0 Мы сделали атрибут частным, потому что нам необходи мо, чтобы тип FuzzyBool вел себя как неизменяемый объ ект, для которого было бы неправильно разрешать пря мой доступ к атрибуту. Кроме того, если в аргументе value получено число, находящееся вне диапазона до пустимых значений, мы принудительно замещаем его значением по умолчанию 0.0 (ложь). В предыдущем под разделе, в классе ShapeAlt.Circle, мы использовали поли тику строгого ограничения, возбуждая исключение при получении недопустимых значений радиуса во время создания нового объекта Circle. Дерево наследования класса FuzzyBool приводится на рис. 6.4. Свойство radius клас са Shape Alt.Circle , стр. 289 Собственные классы 293 Простейшим логическим оператором является логическое НЕ, в каче стве которого мы будем использовать битовый оператор инверсии (): def __invert__(self): return FuzzyBool(1.0 self.__value) Битовый и логический оператор И (&) реализуется специальным мето дом __and__(), а соответствующий ему комбинированный оператор присваивания (&=) – методом __iand__(): def __and__(self, other): return FuzzyBool(min(self.__value, other.__value)) def __iand__(self, other): self.__value = min(self.__value, other.__value) return self Логический оператор И возвращает новый объект FuzzyBool, основыва ясь на значениях объектов self и other, тогда как комбинированный оператор присваивания изменяет значение частного атрибута. Строго говоря, такое поведение не совсем свойственно неизменяемым объек там, но оно совпадает с поведением некоторых других неизменяемых типов языка Python, таких как int. Например, при использовании оператора += создается впечатление, что изменяется операнд слева, но в действительности выполняется перепривязка ссылки на новый объ ект int, хранящий результат операции сложения; хотя в случае Fuzzy Bool перепривязку выполнять не требуется, так как мы действительно изменяем сам объект. Причина, по которой метод возвращает значе object __new__() __init__() __eq__() __repr__() __str__() __hash__() __format__() FuzzyBool __value __new__() __init__() __eq__() __repr__() __str__() __hash__() __format__() __bool__() __float__() __invert__() __and__() __iand__() conjunction() # статический Обозначения унаследован реализован переопределен Рис. 6.4. Дерево наследования класса FuzzyBool 294 Глава 6. Объектно/ориентированное программирование ние self, заключается в необходимости обеспечения возможности объ единять операции в цепочку. Таблица 6.2. Фундаментальные специальные методы Мы могли бы также реализовать метод __rand__(). Этот метод вызыва ется в случае, когда объекты self и other принадлежат разным типам, а метод __and__() для данной пары типов не реализован. 1 В классе Fuzzy Bool в этом нет никакой необходимости. Для большинства двухмест ных операторов имеются специальные методы двух версий: «i» (inpla ce – изменяется сам объект) и «r» (reflect, то есть производится обмен операндов местами). Мы не показываем реализации методов __or__(), соответствующий ло гическому оператору |, и __ior__(), соответствующий комбинирован ному оператору присваивания |=, потому что они полностью эквива лентны методам реализации операции И, за исключением того, что в результате возвращается не минимальное, а максимальное значение пары self и other. Специальный метод Пример использования Описание __bool__(self) bool(x) Если реализован, возвращает значение истинности для x. Удобно, если исполь зуются конструкции вида if x: ... __format__(self, format_spec) "{0}".format(x) Обеспечивает поддержку метода str.for mat() для классов __hash__(self) hash(x) Если реализован, x сможет использо ваться как ключ словаря или храниться в множестве __init__(self, args) x = X(args) Вызывается при инициализации объекта __new__(cls, args) x = X(args) Вызывается при соз дании объекта __repr__(self) repr(x) Возвращает строку с репрезентативной формой представления x, которая обес печивает равенство eval(repr(x)) == x __repr__(self) ascii(x) Возвращает строку с ре презентативной формой представления x с ис пользованием только символов набора ASCII __str__(self) str(x) Возвращает строковое представление x, пригодное для восприятия человеком 1 То есть возвращает значение NotImplemented. – Прим. перев. Переопределение метода __new__() , стр. 300 Метод str. format() , стр. 103 Собственные классы 295 def __repr__(self): return ("{0}({1})".format(self.__class__.__name__, Мы предусмотрели реализацию метода __repr__(), воспроизводящего репрезентативную форму представления. Например, последователь ность инструкций f = FuzzyBool.FuzzyBool(.75); repr(f) будет воспро изводить строку 'FuzzyBool(0.75)'. Все объекты имеют ряд специальных атрибутов, автоматически созда ваемых интерпретатором, один из которых называется __class__ и со держит ссылку на класс объекта. Все классы обладают частным атри бутом __name__, который также создается автоматически. Мы исполь зуем эти атрибуты для получения имени класса в репрезентативной форме представления. Это означает, что если от класса FuzzyBool будет порожден дочерний класс, добавляющий дополнительные методы, унаследованный метод __repr__() будет продолжать корректно рабо тать и в контексте подкласса, потому что он будет получать имя класса для этого подкласса. def __str__(self): return str(self.__value) Специальный метод __del__() Специальный метод __del__(self) вызывается при унич тожении объекта – по крайней мере в теории. На практи ке метод __del__() может не вызываться никогда, даже при завершении программы. Более того, когда выполня ется инструкция del x, все, что происходит при этом, – удаляется ссылка на объект x и уменьшается счетчик ссылок, указывающих на объект x. Только когда этот счетчик достигает значения 0, есть вероятность, что ме тод __del__() будет вызван, но интерпретатор Python не дает никаких гарантий, что этот метод будет когдани будь вызван. По этой причине метод __del__() очень ред ко переопределяется – он не переопределяется ни в одном из примеров в этой книге, и он не должен использоваться для освобождения ресурсов, для закрытия файлов, сете вых соединений или подключений к базам данных. Язык Python предоставляет два отдельных механизма, при использовании которых можно реализовать коррект ное освобождение ресурсов. Один из них заключается в использовании блоков try ... finally, как это было по казано ранее и как это будет еще показано в главе 7. Дру гой механизм основан на использовании контекста объ екта в соединении с инструкцией with, о которой будет рассказываться в главе 8. 296 Глава 6. Объектно/ориентированное программирование Таблица 6.3. Арифметические и битовые специальные методы В качестве строковой формы представления мы просто возвращаем значение с плавающей точкой, преобразованное в строку. Мы не ис пользовали функцию super(), чтобы избежать попадания в бесконеч ную рекурсию, и вызываем функцию str(), передавая ей атрибут self.__value , а не сам экземпляр объекта. Специальный метод Пример ис пользования Специальный метод Пример ис пользования __abs__(self) abs(x) __complex__(self) complex(x) __float__(self) float(x) __int__(self) int(x) __index__(self) bin(x) oct(x) hex(x) __round__(self, digits) round(x, digits) __pos__(self) +x __neg__(self) x __add__(self, other) x + y __sub__(self, other) x y __iadd__(self, other) x += y __isub__(self, other) x = y __radd__(self, other) y + x __rsub__(self, other) y x __mul__(self, other) x * y __mod__(self, other) x % y __imul__(self, other) x *= y __imod__(self, other) x %= y __rmul__(self, other) y * x __rmod__(self, other) y % x __floordiv__(self, other) x // y __truediv__(self, other) x / y __ifloordiv__(self, other) x //= y __itruediv__(self, other) x /= y __rfloordiv__(self, other) y // x __rtruediv__(self, other) y / x __divmod__(self, other) divmod(x, y) __rdivmod__(self, other) divmod(y, x) __pow__(self, other) x ** y __and__(self, other) x & y __ipow__(self, other) x **= y __iand__(self, other) x &= y __rpow__(self, other) y ** x __rand__(self, other) y & x __xor__(self, other) x ^ y __or__(self, other) x | y __ixor__(self, other) x ^= y __ior__(self, other) x |= y __rxor__(self, other) y ^ x __ror__(self, other) y | x __lshift__(self, other) x << y __rshift__(self, other) x >> y __ilshift__(self, other) x <<= y __irshift__(self, other) x >>= y __rlshift__(self, other) y << x __rrshift__(self, other) y >> x __invert__(self) x Собственные классы 297 def __bool__(self): return self.__value > 0.5 def __int__(self): return round(self.__value) def __float__(self): return self.__value Специальный метод __bool__() преобразует экземпляр в тип bool, то есть он всегда должен возвращать либо True, либо False. Специальный метод __int__() реализует преобразование в целое число. Мы использо вали здесь встроенную функцию round(), потому что функция int() просто усекает дробную часть (поэтому для любого значения FuzzyBool, кроме 1.0, метод всегда возвращал бы значение 0). Преобразование в число с плавающей точкой выполняется очень просто, потому что са мо значение уже является числом с плавающей точкой. def __lt__(self, other): return self.__value < other.__value def __eq__(self, other): return self.__value == other.__value Чтобы обеспечить полную поддержку всех операторов сравнения (<, <=, ==, !=, >=, >), необходимо реализовать хотя бы три из них: <, <= и ==, потому что интерпретатор сможет вывести действие оператора > из оператора <, != – из == и >= – из <=. Мы привели реализацию лишь двух ме тодов, потому что все они очень похожи между собой. 1 def __hash__(self): return hash(id(self)) По умолчанию экземпляры наших собственных классов поддержива ют оператор == (который всегда возвращает False) и являются хеши руемыми (поэтому они могут использоваться в качестве ключей слова ря или добавляться в множества). Но если реализовать специальный метод __eq__(), выполняющий корректную проверку на равенство, эк земпляры перестанут быть хешируемыми. Это можно исправить, реа лизовав специальный метод __hash__(), что мы и сделали. Язык Python предоставляет функцию хеширования строк, чисел, фиксированных множеств и других классов. Здесь мы просто восполь зовались встроенной функцией hash() (которая может работать с лю бым типами данных, имеющими специальный метод __hash__()) и пе редаем ей уникальный идентификатор объекта, на основании которо го вычисляется хешзначение. (Мы не можем использовать частный 1 В действительности мы реализовали лишь два метода, __lt__() и __eq__(), приведенные здесь, – остальные методы сравнения генерируются автома тически, как будет показано в главе 8. Полная под держка всего набора опера торов сравне ния, стр. 439 298 Глава 6. Объектно/ориентированное программирование атрибут self.__value, потому что он может изменяться комбинирован ными операторами присваивания, а хешзначение никогда не должно изменяться.) Встроенная функция id() возвращает уникальное целое число для объекта, который передается в виде аргумента. Обычно этим целым числом является адрес объекта в памяти, однако мы можем только предполагать, что в программе не может существовать двух объектов с одинаковыми числовыми идентификаторами. Функция id() исполь зуется внутри реализации оператора is, который определяет, указыва ют ли две ссылки на один и тот же объект. def __format__(self, format_spec): return format(self.__value, format_spec) Встроенная функция format() – единственная действительно необхо димая функция в объявлениях классов. Она принимает единственный объект и необязательную спецификацию формата и возвращает стро ку с объектом, отформатированным соответствующим образом. Когда объект используется в строке формата, вызывает ся метод __format__() объекта с самим объектом и специ фикацией формата в виде аргументов. Метод возвращает строку с экземпляром, отформатированным соответст вующим образом, как было показано ранее. Все встроенные классы имеют соответствующие методы __format__(). В данном случае мы использовали метод float.__format__(), передавая значение с плавающей точкой и полученную строку формата. Того же эффекта можно было бы добиться другим способом: def __format__(self, format_spec): return self.__value.__format__(format_spec) При использовании встроенной функции format() немного уменьшает ся объем ввода с клавиатуры, и программный код выглядит более оче видно. Никто не заставляет нас использовать функцию format(), поэто му мы могли бы изобрести свой собственный язык форматирования и интерпретировать его внутри метода __format__(); главное – чтобы он возвращал строку. @staticmethod def conjunction(*fuzzies): return FuzzyBool(min([float(x) for x in fuzzies])) Встроенная функция staticmethod() предназначена для использования в качестве декоратора, как видно из этого объявления. Статические методы – это обычные методы, которые не получают аргумент self или любой другой первый аргумент, который автоматически передавался бы интерпретатором Python. Оператор & допускает объединение в цепочки так, чтобы, например, значения f, g и h типа FuzzyBool могли быть объединены в одном выра Примеры ис пользования объектов типа FuzzyBool , стр. 291 Собственные классы 299 жении f & g & h. Такой способ удобно использовать при небольшом числе объектов FuzzyBool, но когда в выражении участвует десяток опе рандов или более, такой порядок вычислений становится слишком не эффективным, поскольку вызов функции производится для каждого оператора &. Благодаря методу, который определен здесь, мы можем получить тот же результат, используя единственный вызов функции FuzzyBool.FuzzyBool.conjunction(f, g, h) . Этот вызов можно переписать более кратко, использовав экземпляр FuzzyBool, но поскольку статиче ские методы не получают аргумент self, то при вызове метода относи тельно экземпляра и в выражении участвует сам экземпляр, мы долж ны передавать его явно, например, f.conjunction(f, g, h). Мы не показали соответствующую реализацию метода disjunction(), так как он отличается только именем и тем, что вместо функции min() использует функцию max(). Некоторые программисты считают использование статических мето дов несвойственным языку Python и используют их только при пере носе программ с других языков программирования (таких как C++ или Java) или если метод не использует аргумент self. В языке Python вместо статических методов лучше создавать функции модуля, как бу дет показано в следующем подразделе, или методы класса, как будет показано в последнем разделе. Точно так же, когда переменная создается в пределах класса, но за пределами какоголибо метода, она становится статической перемен ной (переменной класса). В качестве констант обычно более удобно ис пользовать частные глобальные переменные модуля, а переменные класса часто бывают полезны, когда необходимо хранить информа цию, общую для всех экземпляров класса. Мы завершили реализацию класса FuzzyBool «с нуля». Нам пришлось переопределить 15 методов (17, если бы мы выполнили минимум по всем четырем операторам сравнения) и реализовать два статических метода. В следующем подподразделе мы покажем альтернативную реализацию, на этот раз унаследовав класс float. В этом случае нам придется переопределить всего восемь методов и реализовать две функции модуля, а также «исключить реализацию» 32 методов. В большинстве объектноориентированных языков программирова ния наследование используется, чтобы создать новый класс, обладаю щий всеми методами и атрибутами родительского класса, а также до полнительными методами и атрибутами. Язык Python целиком и пол ностью поддерживает эту парадигму, позволяя добавлять новые мето ды или переопределять унаследованные методы, чтобы изменить их поведение. Но, помимо этого, язык Python позволяет исключать реа лизации методов, то есть определять новый класс так, как если бы он вообще не имел некоторых унаследованных методов. Такой прием мо жет вызвать протесты со стороны пуристов объектноориентирован ного программирования, так как он искажает идею полиморфизма, но 300 Глава 6. Объектно/ориентированное программирование в языке Python, по крайней мере иногда, этот прием может быть по лезен. Создание типов данных из других типов данных Реализация класса FuzzyBool, которая обсуждается в этом подподраз деле, находится в файле FuzzyBoolAlt.py. Одно из основных отличий от предыдущей версии состоит в том, что статические методы conjunc tion() и disjunction() в этой версии реализованы как функции модуля. Например: def conjunction(*fuzzies): return FuzzyBool(min(fuzzies)) На этот раз программный код получился намного проще, чем прежде, потому что класс FuzzyBoolAlt.FuzzyBool наследует класс float, и пото му объекты класса FuzzyBool могут использоваться непосредственно, без необходимости выполнять какиелибо преобразования. (Дерево на следования приводится на рис. 6.5.) Порядок обращения к функции теперь также выглядит более понятным, чем прежде. Вместо того что бы указывать имя модуля и имя класса (или использовать экземпляр класса), после выполнения инструкции import FuzzyBoolAlt мы можем производить вызовы как FuzzyBoolAlt.conjunction(). Ниже приводится инструкция class, объявляющая класс FuzzyBool, и реализация метода __new__(): class FuzzyBool(float): def __new__(cls, value=0.0): return super().__new__(cls, value if 0.0 <= value <= 1.0 else 0.0) object __new__() __init__() __eq__() __repr__() __str__() float |