Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
277 к ним осуществляется без квалификации имени. Доступ к перемен ным класса (иногда они называются статическими переменными) мо жет осуществляться посредством квалификации их имен именем класса. Доступ к глобальным переменным, то есть к переменным мо дуля, осуществляется без квалификации их имен. В некоторых книгах, посвященных языку Python, используется поня тие пространства имен – отображения имен на объекты. Модули – это пространства имен. Например, выполнив инструкцию import math, мы получаем возможность обращаться к объектам в модуле math, ква лифицируя их именем пространства имен (например, math.pi или math.sin() ). Точно так же классы и объекты являются пространствами имен. Например, если представить, что была выполнена инструкция z = complex(1, 2) , то пространство имен объекта z будет содержать два доступных нам атрибута (z.real и z.imag). Одно из преимуществ объектноориентированного подхода состоит в том, что если у нас имеется класс, мы можем специализировать его. Это означает, что можно создать новый класс, наследующий все атри буты (данные и методы) из оригинального класса, и добавить в него или заместить некоторые методы, или добавить дополнительные пере менные экземпляра. Мы можем создать подкласс (другое название специализации ) любого класса Python, будь то встроенный класс, класс из стандартной библиотеки 1 или один из наших собственных классов. Возможность специализации – одно из важнейших преиму ществ объектноориентированного программирования, поскольку она упрощает использование существующих классов, с опробованными и проверенными функциональными возможностями, в качестве осно вы для новых классов, расширяющих оригинал, добавляя новые атри буты данных или новые функциональные возможности простым и по нятным способом. Более того, имеется возможность передавать объек ты новых классов функциям и методам, которые были написаны для работы с оригинальным классом, и при этом они будут работать впол не корректно. Мы будем использовать термин базовый класс для обозначения насле дуемого класса. Базовым классом может быть как прямой предок, так и любой другой класс, расположенный выше в дереве наследования. Другой термин, обозначающий базовый класс, – суперкласс. Мы бу дем использовать термины подкласс, порожденный класс и дочерний класс для обозначения класса, наследующего (то есть специализирую щего) другой класс. В языке Python все встроенные и библиотечные классы, а также все созданные нами классы прямо или косвенно на 1 Некоторые библиотечные классы, реализованные на языке C, не могут быть специализированы. Такая особенность этих классов обязательно под черкивается в документации. 278 Глава 6. Объектно/ориентированное программирование следуют единый базовый класс object. Рис. 6.1 иллюстрирует некото рые термины, используемые при описании механизма наследования. Любой метод можно переопределить, то есть повторно реализовать в подклассе, как в языке Java (за исключением методов со специфика тором final). 1 Если предположить, что имеется объект класса MyDict (наследующего класс dict) и производится вызов метода, который оп ределяется обоими классами dict и MyDict, интерпретатор корректно вызовет версию метода для класса MyDict. Этот механизм называется динамическим связыванием методов или полиморфизмом. Если воз никнет необходимость вызвать версию метода базового класса внутри одноименного метода подкласса, сделать это можно с помощью встро енной функции super(). Кроме того, в языке Python используется механизм грубого определе ния типа (так называемая утиная типизация) – «если это ходит как утка и крякает как утка, значит, это утка». Говоря другими словами, если нам необходимо вызвать определенный метод объекта, то неваж но, к какому классу относится этот объект, главное, чтобы он имел ме тод, который предполагается вызвать. В предыдущей главе мы виде ли, что, когда возникала необходимость в объекте файла, мы могли получить его, вызвав функцию open() или создав объект io.StringIO, который имеет тот же самый API (Application Programming Interface – прикладной программный интерфейс), то есть обладает теми же самы ми методами, что и объект, возвращаемый функцией open(), откры вающей файл в текстовом режиме. Механизм наследования используется для моделирования отношений типа «является», то есть отношения, когда объекты одного класса по 1 В терминологии языка C++ все методы классов в языке Python являются виртуальными. object dict MyDict Подкласс класса object Специализация класса object Дочерний класс класса object Суперкласс для классов dict, MyDict,… Базовый класс для классов dict, MyDict,… Суперкласс для класса MyDict,… Базовый класс для класса MyDict,… Подкласс класса object Специализация класса object Дочерний класс класса object Подкласс класса dict Специализация класса dict Дочерний класс класса dict Рис. 6.1. Некоторые термины, используемые при описании механизма наследования Собственные классы 279 существу являются теми же самыми, что и объекты какогото другого класса, но с некоторыми отличиями, такими как дополнительные ат рибуты данных или дополнительные методы. Другой подход основан на использовании механизма агрегирования (или композиции) – когда класс включает одну или более переменных экземпляра, являющихся экземплярами других классов. Механизм агрегирования используется для моделирования отношений типа «имеет». В языке Python при соз дании любых классов используется механизм наследования, потому что все классы в конечном итоге имеют единый базовый класс object, и, кроме того, в большинстве классов используется механизм агреги рования, потому что в большинстве классов имеются переменные эк земпляров различных типов. Некоторые объектноориентированные языки программирования об ладают двумя особенностями, отсутствующими в языке Python. Пер вая особенность – это перегрузка, то есть возможность иметь в одном и том же классе несколько методов с одинаковыми именами, но с раз личными списками входных параметров. Благодаря наличию в языке Python очень гибкого механизма передачи аргументов отсутствие воз можности перегрузки практически не является ограничением. Вторая особенность – управление доступом; в языке Python не существует аб солютно надежных механизмов защиты частных данных. Однако если мы создаем атрибуты (переменные экземпляра или методы), имена ко торых начинаются двумя символами подчеркивания, интерпретатор будет предотвращать неумышленные попытки доступа к ним, так что эти атрибуты можно считать частными. (Делается это посредством подмены имен, как будет показано на примере в главе 8.) Аналогично тому, как мы первый символ имени наших собственных модулей писали в верхнем регистре, мы будем поступать и при имено вании наших собственных классов. Мы можем определить любое чис ло классов, как в самой программе, так и в модулях. Имена классов не обязательно должны соответствовать именам модулей, и модули могут содержать столько определений классов, сколько нам потребуется. Теперь, когда мы рассмотрели некоторые проблемы, которые могут быть решены с помощью классов, познакомились с необходимыми терминами и некоторыми основами, – можно приступать к созданию собственных классов. Собственные классы В предыдущих главах нам уже приходилось создавать собственные классы: наши собственные исключения. Ниже приводится синтаксис, используемый при создании собственных классов: class className: suite 280 Глава 6. Объектно/ориентированное программирование class className(base_classes): suite Поскольку при создании подклассов исключений мы не добавляли ни каких новых атрибутов (данных экземпляра или методов), в качестве блока кода (suite) мы использовали инструкцию pass (то есть ничего не добавляли), а так как блок кода состоял из единственной инструкции, мы помещали его в одной строке с инструкцией class. Обратите внима ние: как и инструкция def, инструкция class является самой обычной инструкцией, что дает возможность создавать классы динамически, когда в этом возникнет необходимость. Методы класса создаются с по мощью инструкций def внутри блока кода класса. Экземпляры класса создаются посредством обращения к имени класса, как к функции, ко торой передаются все необходимые аргументы. Например, инструк ция x = complex(4, 8) создаст комплексное число и запишет ссылку на него в переменную x. Атрибуты и методы Начнем с очень простого класса Point, который хранит координаты точки (x, y). Определение класса находится в файле Shape.py, а ниже приводится его полная реализация (за исключением строк документи рования): class Point: def __init__(self, x=0, y=0): self.x = x self.y = y def distance_from_origin(self): return math.hypot(self.x, self.y) def __eq__(self, other): return self.x == other.x and self.y == other.y def __repr__(self): return "Point({0.x!r}, {0.y!r})".format(self) def __str__(self): return "({0.x!r}, {0.y!r})".format(self) Поскольку базовый класс не был указан явно, класс Point является прямым наследником класса object, как если бы было записано опре деление class Point(object). Прежде чем приступать к обсуждению всех его методов, рассмотрим несколько примеров их использования: import Shape a = Shape.Point() repr(a) # вернет: 'Point(0, 0)' b = Shape.Point(3, 4) str(b) # вернет: '(3, 4)' b.distance_from_origin() # вернет: 5.0 b.x = 19 Собственные классы 281 str(b) # вернет: '(19, 4)' a == b, a != b # вернет: (False, True) Класс Point имеет два атрибута данных, self.x и self.y, и пять методов (не считая унаследованных методов), из которых четыре являются специальными методами – они показаны на рис. 6.2. После импорти рования модуля Shape появляется возможность использовать класс Point , как любой другой класс. Доступ к атрибутам можно осуществ лять непосредственно (например, y = a.y), а сам класс отлично интег рируется со всеми остальными классами языка Python, обеспечивая поддержку оператора равенства (==) и представления класса в репре зентативной и строковой формах. Интерпретатор Python достаточно умен, чтобы обеспечить поддержку оператора неравенства (!=) на осно ве имеющейся поддержки оператора равенства. (Однако имеется воз можность реализовать поддержку каждого оператора в отдельности, если потребуется обеспечить полный контроль, когда, к примеру, один оператор не является полной противоположностью другому.) При вызове метода интерпретатор автоматически передает ему первый аргумент – ссылку на сам объект (в языках C++ и Java она имеет имя this ). В соответствии с соглашениями мы обязаны включать этот пара метр в список под именем self. Все атрибуты объекта (данные и мето ды) должны квалифицироваться именем self. При этом потребуется вводить с клавиатуры чуть больше, чем в других языках программи рования, но в этом есть свое преимущество – полная ясность: мы все гда точно знаем, что обращаемся к атрибуту объекта, если квалифици руем его именем self. Чтобы создать объект, необходимо выполнить два действия. Сначала необходимо создать неинициализированную заготовку объекта, а затем необходимо подготовить объект к использованию, инициализировав object __new__() __init__() __eq__() __repr__() __str__() Point x y __new__() __init__() distance_from_origin() __eq__() __repr__() __str__() Обозначения унаследован реализован переопределен Рис. 6.2. Дерево наследования класса Point 282 Глава 6. Объектно/ориентированное программирование его. В некоторых языках программирования (таких как C++ и Java) эти два действия объединены в одно, но в языке Python они выполня ются отдельно друг от друга. Когда создается объект (например, p = Shape.Point() ), то сначала вызывается специальный метод __new__(), который создает объект, а затем выполняется инициализация объекта вызовом специального метода __init__(). В языке Python при создании практически любого клас са нам будет необходимо переопределять только метод __init__() , поскольку имеющейся реализации метода ob ject.__new__() почти всегда достаточно, и к тому же он вызывается автоматически, если мы не предусматрива ем собственную реализацию метода __new__(). (Ниже в этой главе мы увидим пример одного из редких случа ев, когда возникает необходимость переопределить ме тод __new__().) Отсутствие необходимости переопреде лять методы в подклассе – это еще одно преимущество объектноориентированного программирования. Если метод базового класса удовлетворяет нашим потребно стям, мы можем не переопределять его в своем классе. Если при обращении к методу объекта окажется, что класс объекта не реализует его, интерпретатор автома тически попытается отыскать его в базовых классах объ екта, а затем в их базовых классах, и так до тех пор, пока не найдет требуемый метод, а если метод не будет обна ружен, он возбудит исключение AttributeError. Например, если попробовать выполнить инструкцию p = Shape.Point(), интерпретатор начнет поиск метода Point.__new__(). Поскольку мы не переопределяли этот метод, интерпретатор попытается отыскать этот метод в базовом классе класса Point. В данном случае существует всего один базовый класс, object, который имеет требуемый метод, поэтому интерпретатор вызовет метод object.__new__() и создаст неинициали зированную заготовку объекта. Затем интерпретатор приступит к по иску метода инициализации, __init__(), и поскольку мы предусмотре ли его реализацию, интерпретатору не потребуется искать его в базо вых классах и он вызовет метод Point.__init__(). В заключение интер претатор запишет в переменную p ссылку на вновь созданный и инициализированный объект типа Point. Поскольку методы очень короткие и к тому же они приводились за не сколько страниц отсюда, для удобства мы приведем снова каждый ме тод перед обсуждением. def __init__(self, x=0, y=0): self.x = x self.y = y В методе инициализации создаются две переменные экземпляра, self.x и self.y, которым присваиваются значения параметров x и y. Посколь Альтерна тивный тип FuzzyBool , стр. 300 Собственные классы 283 ку при создании нового объекта класса Point интерпретатор сразу же обнаружит этот метод, он не будет автоматически вызывать метод object.__init__() . Как только интерпретатор обнаруживает необходи мый метод, он сразу же вызывает его, прекращая дальнейшие поиски. Пуристы объектноориентированного программирования могли бы на чать реализацию своего метода с вызова метода __init__() базового класса, обращением к super().__init__(). Действие вызова такой функ ции super() заключается в вызове метода __init__() базового класса. Для классов, порожденных непосредственно от класса object, в этом нет никакой необходимости, и в этой книге мы будем вызывать мето ды базовых классов, только когда это действительно нужно – напри мер, при создании классов, которые должны будут наследоваться, или при создании классов, не являющихся непосредственными наследни ками класса object. Отчасти это вопрос стиля, но тем не менее совер шенно разумно – всегда начинать метод __init__() своего класса с вы зова super().__init__(). def distance_from_origin(self): return math.hypot(self.x, self.y) Это обычный метод, выполняющий вычисления на основе переменных экземпляра объекта. Для методов весьма характерно иметь небольшой размер и получать в виде параметров только объект, в контексте кото рого они вызываются, поскольку нередко все данные, необходимые методу, доступны внутри объекта. def __eq__(self, other): return self.x == other.x and self.y == other.y Имена методов не должны начинаться и заканчиваться двумя симво лами подчеркивания, если они не являются предопределенными спе циальными методами. В языке Python каждому оператору сравнения соответствует свой специальный метод, как показано в табл. 6.1. Таблица 6.1. Специальные методы сравнивания Специальный метод Пример использования Описание __lt__(self, other) x < y Возвращает True, если x мень ше, чем y __le__(self, other) x <= y Возвращает True, если x мень ше или равно y __eq__(self, other) x == y Возвращает True, если x равно y __ne__(self, other) x != y Возвращает True, если x не рав но y __ge__(self, other) x >= y Возвращает True, если x больше или равно y __gt__(self, other) x > y Возвращает True, если x боль ше, чем y 284 Глава 6. Объектно/ориентированное программирование Все экземпляры классов по умолчанию поддерживают оператор == и операция сравнения всегда возвращает False. Мы можем переопреде лить это поведение, реализовав специальный метод __eq__(), как это было сделано в данном случае. Интерпретатор Python будет автомати чески подставлять метод __ne__() (not equal – не равно), реализующий действие оператора неравенства (!=), если в классе присутствует реа лизация метода __eq__(), но отсутствует реализация метода __ne__(). По умолчанию все экземпляры классов являются хеши руемыми, поэтому для них можно вызывать функцию hash() , использовать их в качестве ключей словаря и со хранять в множествах. Но если будет реализован метод __eq__() , экземпляры перестанут быть хешируемыми. Как исправить это положение, будет показано при обсу ждении класса FuzzyBool ниже. Реализовав этот специальный метод, мы получаем возможность срав нивать объекты Point, но при попытке сравнить объект Point с объек том другого типа, например, int, будет возбуждено исключение Attri buteError (поскольку объекты класса int не имеют атрибута x). С дру гой стороны, мы можем сравнивать объекты Point с другими объекта ми совместимых типов, у которых имеется атрибут x (благодаря грубому определению типов в языке Python), но это может приводить к неожиданным результатам. Если необходимо избежать сравнения в случаях, когда это не имеет смысла, можно использовать несколько подходов. Один из них состо ит в использовании инструкции assert, например, assert isinstan ce(other, Point) . Другой состоит в том, чтобы возбуждать исключение TypeError для обозначения попытки сравнения с неподдерживаемым типом, например, if not isinstance(other, Point): raise TypeError(). Третий способ (который, с точки зрения языка Python, является наи более правильным) заключается в следующем: if not isinstance(other, Point): return NotImplemented . В этом третьем случае, когда метод воз вращает NotImplemented, интерпретатор попытается вызвать метод other.__eq__(self) , чтобы определить, поддерживает ли тип other срав нение с типом Point, и если в этом типе не будет обнаружен такой ме тод или он также возвращает NotImplemented, интерпретатор возбудит исключение TypeError. (Обратите внимание, что значение NotImplement ed может вернуть только переопределенный специальный метод срав нения – из тех, что перечислены в табл. 6.1.) Встроенная функция isinstance() принимает объект и класс (или кор теж классов) и возвращает True, если объект принадлежит данному классу (или одному из классов, перечисленных в кортеже) или одному из базовых классов указанного класса (или одного из классов, пере численных в кортеже). def __repr__(self): return "Point({0.x!r}, {0.y!r})".format(self) Тип |