Математический анализ. 3е издание
Скачать 4.86 Mb.
|
Вызовы методов классов В предыдущем разделе мы видели, как атрибут I2.w в нашем примере дерева классов транслируется в C2.w при выполнении поиска в дереве на следования. Не менее важно понять, что точно так же наследуются и ме тоды (то есть функции, присоединенные к классам в виде атрибутов). Если ссылка I2.w – это вызов функции, тогда в действительности это выражение означает: «вызвать функцию С3.w для обработки I2». То есть интерпретатор Python автоматически отобразит вызов I2.w() на вызов C3.w(), передав унаследованной функции экземпляр в виде пер вого аргумента. Фактически всякий раз, когда вызывается функция, присоединенная к классу, подразумевается не класс целиком, а экземпляр класса. Этот подразумеваемый экземпляр, или контекст, является одной из причин, 568 Глава 22. ООП: общая картина почему данная модель называется объектноориентированной – всегда существует объект, над которым выполняются действия. В более реа листичном примере мы могли бы вызывать метод с именем giveRaise, присоединенный как атрибут к классу Employee, – вызов этого метода был бы бессмысленным без указания служащего, которому дается надбавка к зарплате. Как мы увидим позднее, Python передает методам подразумеваемый экземпляр в виде специального первого аргумента, в соответствии с со глашением именуемого self. Мы также узнаем, что методы могут вы зываться как через экземпляры (например, bob.giveRaise()), так и че рез классы (например, Employee.giveRaise(bob)), причем обе формы иг рают одну и ту же роль в наших сценариях. Чтобы увидеть, как мето ды принимают свои подразумеваемые экземпляры, нам необходимо рассмотреть примеры программного кода. Создание деревьев классов Несмотря на всю отвлеченность наших разговоров, тем не менее за ку лисами всех этих идей стоит реальный программный код. Мы создаем деревья и объекты в них с помощью инструкций class и вызовов клас сов, которые позднее мы рассмотрим более подробно. В двух словах: • Каждая инструкция class создает новый объект класса. • Каждый раз, когда вызывается класс, он создает новый объект эк земпляра. • Экземпляры автоматически связываются с классами, из которых они были созданы. • Классы связаны со своими суперклассами, перечисленными в круг лых скобках в заголовке инструкции class, – порядок следования в списке определяет порядок расположения в дереве. Чтобы создать дерево, изображенное на рис. 22.1, например, мы могли бы использовать следующий программный код (здесь я опустил реали зацию классов): class C2: ... # Создать объекты классов (овалы) class C3: ... class C1(C2, C3): ... # Связанные с суперклассами I1 = C1() # Создать объекты экземпляров (прямоугольники), I2 = C1() # связанные со своими классами Здесь мы построили дерево объектов классов, выполнив три инструк ции class, и два объекта экземпляров, вызвав класс C1 дважды, как ес ли бы это была обычная функция. Экземпляры помнят класс, из кото рого они были созданы, а класс C1 помнит о своих суперклассах. С технической точки зрения в этом примере используется то, что на зывается множественным наследованием, которое означает, что не кий класс имеет более одного суперкласса над собой в дереве классов. ООП с высоты 30 000 футов 569 В языке Python, если в инструкции class в круглых скобках перечис лено более одного суперкласса (как в случае с классом C1 в данном при мере), их порядок следования слева направо определяет порядок поис ка атрибутов в суперклассах. Изза особенностей поиска в дереве наследования имеет большое значе ние, к какому из объектов присоединяется тот или иной атрибут, – тем самым определяется его область видимости. Атрибуты, присоединяе мые к экземплярам, принадлежат только этим конкретным экземпля рам, но атрибуты, присоединяемые к классам, совместно используют ся всеми подклассами и экземплярами. Позднее мы подробно изучим программный код, выполняющий присоединение атрибутов к этим объектам. Мы увидим, что: • атрибуты обычно присоединяются к классам с помощью инструк ций присваивания внутри инструкции class, а не во вложенных ин струкциях def, определяющих функции. • атрибуты обычно присоединяются к экземплярам с помощью при сваивания значений специальному аргументу с именем self, пере даваемому функциям внутри классов. Например, классы определяют поведение своих экземпляров с помо щью функций, создаваемых инструкциями def внутри инструкции class . Поскольку такие вложенные инструкции def выполняют при сваивание именам внутри класса, они присоединяются к объектам классов в виде атрибутов и будут унаследованы всеми экземплярами и подклассами: class C1(C2, C3): # Создать и связать класс C1 def setname(self, who): # Присвоить: C1.setname self.name = who # Self – либо I1, либо I2 I1 = C1() # Создать два экземпляра I2 = C1() I1.setname('bob') # Записать 'bob' в I1.name I2.setname('mel') # Записать 'mel' в I2.name print I1.name # Выведет 'bob' Синтаксис инструкции def в этом контексте – совершенно обычный. С функциональной точки зрения, когда инструкция def появляется внутри инструкции class, как в этом примере, она обычно называется ме+ тодом и автоматически принимает специальный первый аргумент с име нем self, который содержит ссылку на обрабатываемый экземпляр. 1 Так как классы являются фабриками, способными производить мно жество экземпляров, их методы обычно используют этот, получаемый 1 Если когданибудь вам приходилось использовать язык C++ или Java, вы без труда поймете, что в языке Python имя self – это то же, что указатель this , но в языке Python аргумент self всегда используется явно, чтобы сде лать обращения к атрибутам более очевидными. 570 Глава 22. ООП: общая картина автоматически, аргумент self для получения или изменения значений атрибутов конкретного экземпляра, который обрабатывается мето дом. В предыдущем фрагменте программного кода имя self использу ется для сохранения имени служащего в конкретном экземпляре. Подобно простым переменным, атрибуты классов и экземпляров не объявляются заранее, а появляются, когда им впервые выполняется присваивание значений. Когда метод присваивает значение атрибуту с помощью имени self, он тем самым создает атрибут экземпляра, на ходящегося в нижнем уровне дерева классов (то есть в одном из прямо угольников), потому что имя self автоматически ссылается на обраба тываемый экземпляр. Фактически благодаря тому, что все объекты в дереве классов – это всего лишь объекты пространств имен, мы можем получать или уста навливать любой из их атрибутов, используя соответствующие имена. Например, выражение C1.setname является таким же допустимым, как и I1.setname, при условии, что имена C1 и I1 находятся в области види мости программного кода. В настоящий момент класс C1 не присоединяет атрибут name к экземп лярам, пока не будет вызван метод setname. Фактически попытка обра титься к имени I1.name до вызова I1.setname приведет к появлению ошибки, сообщающей о неопределенном имени. Если в классе потре буется гарантировать, что атрибут, такой как name, всегда будет при сутствовать в экземплярах, то такой атрибут должен создаваться на этапе создания класса, как показано ниже: class C1(C2, C3): def __init__(self, who): # Создать имя при создании класса self.name = who # Self – либо I1, либо I2 I1 = C1('bob') # Записать 'bob' в I1.name I2 = C1('mel') # Записать 'mel' в I2.name print I1.name # Выведет 'bob' В этом случае интерпретатор Python автоматически будет вызывать метод с именем __init__ каждый раз при создании экземпляра класса. Новый экземпляр будет передаваться методу __init__ в виде первого аргумента self, а любые значения, перечисленные в круглых скобках при вызове класса, будут передаваться во втором и последующих за ним аргументах. В результате инициализация экземпляров будет вы полняться в момент их создания, без необходимости вызывать допол нительные методы. Метод __init__ известен как конструктор, так как он запускается на этапе конструирования экземпляра. Этот метод является типичным представителем большого класса методов, которые называются метода+ ми перегрузки операторов . Более подробно эти методы будут рассмат риваться в последующих главах. Такие методы наследуются в дереве классов как обычно, а их имена начинаются и заканчиваются двумя ООП с высоты 30 000 футов 571 символами подчеркивания, чтобы подчеркнуть их особенное назначе ние. Интерпретатор Python вызывает их автоматически, когда экземп ляры, поддерживающие их, участвуют в соответствующих операциях, и они главным образом являются альтернативой вызовам простых ме тодов. Кроме того, они являются необязательными: при их отсутствии соответствующие операции экземплярами не поддерживаются. Например, чтобы реализовать пересечение множеств, класс может пре дусмотреть реализацию метода intersect или перегрузить оператор &, описав логику его работы в методе с именем __and__. Поскольку исполь зование операторов делает экземпляры более похожими на встроенные типы, это позволяет определенным классам обеспечивать непротиворе чивый и естественный интерфейс и быть совместимыми с программ ным кодом, который предполагает выполнение операций над объекта ми встроенных типов. ООП – это многократное использование программного кода Вот, в основном, и все описание ООП в языке Python (за исключением некоторых синтаксических особенностей). Конечно, в ООП присутст вует не только наследование. Например, перегрузка операторов может применяться гораздо шире, чем описывалось до сих пор, – классы мо гут предоставлять собственные реализации таких операций, как дос туп к элементам по их индексам, получение значений атрибутов, вы вод и многие другие. Но вообще говоря, ООП реализует поиск атрибу тов в деревьях. Тогда зачем нам погружаться в тонкости создания деревьев объектов и выполнения поиска в них? Нужно накопить некоторый опыт, чтобы увидеть, как при грамотном использовании классы поддерживают возможность многократного использования программного кода спосо бами, которые недоступны в других программных компонентах. Ис пользуя классы, мы программируем, настраивая написанное про граммное обеспечение, вместо того чтобы изменять существующий программный код или писать новый код в каждом новом проекте. С фундаментальной точки зрения, классы – это действительно всего лишь пакеты функций и других имен, которые во многом напоминают модули. Однако автоматический поиск атрибутов в дереве наследова ния, который мы получаем при использовании классов, обеспечивает возможности по адаптации программного обеспечения более широкие, чем это возможно с помощью модулей и функций. Кроме того, классы представляют собой удобную структуру, обеспечивающую компактное размещение выполняемого кода и переменных, что помогает в отладке. Например, методы – это обычные функции со специальным первым аргументом, поэтому мы можем подражать некоторым чертам их по ведения, вручную передавая объекты для обработки обычным функ 572 Глава 22. ООП: общая картина циям. Однако участие методов в наследовании классов позволяет нам естественным образом адаптировать существующее программное обес печение, создавая новые подклассы, определяющие новые методы, вместо того чтобы изменять существующий программный код. Подоб ное невозможно в случае с модулями и функциями. В качестве примера предположим, что вас привлекли к реализации приложения базы данных, где хранится информация о служащих. Как программист, использующий объектноориентированные особен ности языка Python, вы могли бы начать работу с реализации общего суперкласса, который определяет поведение, общее для всех катего рий служащих в вашей организации: class Employee: # Общий суперкласс def computeSalary(self): ... # Общее поведение def giveRaise(self): ... def promote(self): ... def retire(self): ... Реализовав это общее поведение, можно специализировать его для ка ждой категории служащих, чтобы отразить отличия разных катего рий от стандарта. То есть можно создать подклассы, которые изменяют лишь ту часть поведения, которая отличает их от типового представле ния служащего, – остальное поведение будет унаследовано от общего класса. Например, если зарплата инженеров начисляется в соответст вии с какимито особыми правилами (то есть не по почасовому тари фу), в подклассе можно переопределить всего один метод: class Engineer(Employee): # Специализированный подкласс def computeSalary(self): ... # Особенная реализация Поскольку эта версия computeSalary находится в дереве классов ниже, она будет замещать (переопределять) общую версию метода в классе Employee . Затем можно создать экземпляры разновидностей классов служащих в соответствии с принадлежностью имеющихся служащих классам, чтобы обеспечить корректное поведение: bob = Employee() # Поведение по умолчанию mel = Engineer() # Особые правила начисления зарплаты Обратите внимание, что существует возможность создавать экземпля ры любых классов в дереве, а не только тех, что находятся внизу, – класс, экземпляр которого создается, определяет уровень, откуда бу дет начинаться поиск атрибутов. В перспективе эти два объекта экзем пляров могли бы быть встроены в больший контейнерный объект (на пример, в список или в экземпляр другого класса), который представ ляет отдел или компанию, реализуя идею композиции, упомянутую в начале главы. Когда позднее вам потребуется узнать размер зарплаты этих служа щих, их можно будет вычислить в соответствии с правилами классов, ООП с высоты 30 000 футов 573 из которых были созданы объекты, благодаря поиску в дереве наследо вания: 1 company = [bob, mel] # Составной объект for emp in company: print emp.computeSalary() # Вызвать версию метода для данного объекта Это еще одна разновидность полиморфизма – идеи, которая была пред ставлена в главе 4 и повторно рассматривалась в главе 15. Вспомните, полиморфизм означает, что смысл операции зависит от объекта, над которым она выполняется. Здесь метод computeSalary определяется в хо де поиска в каждом объекте в дереве наследования, прежде чем он бу дет вызван. В других приложениях полиморфизм может также ис пользоваться для сокрытия (то есть для инкапсуляции) различий ин терфейсов. Например, программа, которая обрабатывает потоки дан ных, может работать с объектами, имеющими методы ввода и вывода, не заботясь о том, что эти методы делают в действительности: def processor(reader, converter, writer): while 1: data = reader.read() if not data: break data = converter(data) writer.write(data) Передавая экземпляры классов с необходимыми интерфейсными ме тодами read и write, специализированными под различные источники данных, мы можем использовать одну и ту же функцию processor для работы с любыми источниками данных, как уже существующими, так и с теми, что появятся в будущем: class Reader: def read(self): ... # Поведение и инструменты по умолчанию def other(self): ... class FileReader(Reader): def read(self): ... # Чтение из локального файла class SocketReader(Reader): def read(self): ... # Чтение из сокета processor(FileReader(...), Converter, FileWriter(...)) processor(SocketReader(...), Converter, TapeWriter(...)) processor(FtpReader(...), Converter, XmlWriter(...)) 1 Обратите внимание, что список company из этого примера мог бы сохранять ся в файле, чтобы обеспечить постоянное хранение базы данных с информа цией о служащих (с помощью модуля pickle, представленного в главе 9 в разделе «Использование файлов»). Кроме того, в состав Python входит мо дуль shelve, который мог бы позволить сохранять экземпляры классов в файлах с доступом по ключу, – сторонняя разработка, система ZODB, по зволяет реализовать то же самое, но имеет более качественную поддержку объектноориентированных баз данных для промышленного использования. 574 Глава 22. ООП: общая картина Кроме того, благодаря тому, что внутренняя реализация этих методов read и write была разделена по типам источников данных, их можно из менять, не трогая программный код, подобный приведенному, который использует их. Фактически функция processor сама может быть клас сом, реализующим логику работы функции преобразования converter, которую могут унаследовать подклассы, и позволяющим встраивать экземпляры, выполняющие чтение и запись, в соответствии с принци пом композиции (далее в этой части книги будет показано, как это реа лизуется). Когда вы привыкнете программировать в этом стиле (адаптации про граммного обеспечения), то обнаружите, начиная писать новую про грамму, что большая часть вашей задачи уже реализована, и ваша за дача в основном сводится к тому, чтобы подобрать уже имеющиеся су перклассы, которые реализуют поведение, требуемое вашей програм ме. Например, возможно, ктото другой, для совершенно другой программы, уже написал классы Employee, Reader и Writer из данного примера. В этом случае вы сможете воспользоваться уже готовым про граммным кодом «за так». На практике во многих прикладных областях вы можете получить или купить коллекции суперклассов, известных как платформы, в кото рых реализованы наиболее часто встречающиеся задачи программиро вания на основе классов, готовые к использованию в ваших приложе ниях. Такие платформы могут предоставлять интерфейсы к базам дан ных, протоколы тестирования, средства создания графического интер фейса и т. д. В среде такой платформы вам часто будет достаточно создать свой подкласс, добавив в него одиндва своих метода, а основ ная работа будет выполняться классами платформы, расположенны ми выше в дереве наследования. Программирование в мире ООП – это лишь вопрос сборки уже отлаженного программного кода и настройки его путем написания своих собственных подклассов. Безусловно, на то чтобы понять, как использовать классы для дости жения такого объектноориентированного идеала, потребуется время. На практике ООП влечет за собой большой объем предварительного проектирования, на этапе которого осмысливаются преимущества, по лучаемые от использования классов; с этой целью программисты нача ли составлять список наиболее часто встречающихся решений в ООП, известных как шаблоны проектирования – помогающих решать про блемы проектирования. При этом объектноориентированный про граммный код на языке настолько прост, что он сам по себе не будет препятствием в освоении ООП. Чтобы убедиться в этом, вам следует перейти к главе 23. |