Лутц М. - Изучаем Python (1077325), страница 147
Текст из файла (страница 147)
паве ) ве1(.тцпс(*агре) этгасег бег враю(а, ь, с); з Обертывает зраю е объект-декоратор рг1пт а, Ь, с З В действительности вызывается объект-обертка З То есть вызывается метод са!1 в классе З Метод сз11 выполняет дополнительные действия д и вызывает оригинальную функцию враю(1, 2, 3) враю('а', 'Ь', 'с') враю(4, 5, 6) сап 1 тс враю 1 2 3 са11 2 то враз а Ь с са11 3 то враю 4 5 6 Функция враз передается декоратору 1гасег, поэтому, когда производится вызов к оригинальному имени враз, в действительности вызывается метод са11 в классе. Этот метод подсчитывает и регистрирует вызовы, а затем вызывает оригинальную обернутую функцию. Обратите внимание, как используется синтаксис аргумента *паве для упаковки аргументов, передаваемых функции, — благодаря этому данный декоратор может использоваться для обертывания любой функции с любым числом аргументов.
В результате к оригинальной функции врав добавляется слой дополнительной логики. Ниже приводится вывод, полученный от сценария,— первая строка создана классом Сгасег, а вторая — функцией враа: 684 Глава 26. Дополнительные возможности классов Типичные проблемы при работе с классами Большая часть типичных проблем, связанных с классами, сводится к проблемам, связанным с пространствами имен (особенно если учесть, что классы — это всего лишь пространства имен с некоторыми дополнительными особенностями). Некоторые темы, которые мы затронем в этом разделе, скорее являются передовыми приемами использования классов, чем проблемами, а решение одной-двух их этих проблем было упрощено в последних версиях Ру1Ьоп.
Изменение атрибутов класса может приводить к побочным эффектам Теоретически классы (и экземпляры классов) относятся к категории изменяемых объектов. Подобно таким встроенным типам, как списки и словари, они могут изменяться непосредственно, путем присваивания значений атрибутам и, как и в случае со списками и словарями, это означает, что изменение класса или экземпляра может оказывать влияние на множественные ссылки на них.
Обычно это именно то, что нам требуется (так объекты изменяют свое состояние), но, изменяя атрибуты, об этом необходимо помнить. Все экземпляры класса совместно используют одно и то же пространство имен класса, поэтому любые изменения на уровне класса будут отражаться на всех экземплярах, если, конечно, они не имеют собственных версий атрибутов класса. Классы, модули и экземпляры — это всего лишь объекты с пространствами имен атрибутов, поэтому во время выполнения они обычно изменяются с помощью операций присваивания. Рассмотрим следующий класс. В теле класса выполняется присваивание имени а, в результате чего создается атрибут Х, а, который во время выполнения располагается в объекте класса и будет унаследован всеми экземплярами класса Х: »> с1ааа Х: а=1 № Атрибут класса »> 1 Х() »> 1.а 1 »> Х.а № Унаследован зкзеиллирои Исследуйте программный код этого примера повнимательнее, чтобы вникнуть в его суть.
Декораторы функций являют собой универсальный механизм, и, тем не менее, эта дополнительная особенность представляет интерес в первую очередь для разработчиков инструментальных средств, а не для при- кладных программистов, поэтому я снова отсылаю вас за более подроб- ной информацией к стандартному набору руководств по языку РуФ)зоп. 685 Типичные проблемы при работе с классами Пока все неплохо — это обычный случай. Но, обратите внимание, что происходит, когда атрибут класса изменяется динамически, из-за его пределов: это приводит к одновременному изменению атрибута во всех объектах, наследующих его от класса.
Кроме того, новые экземпляры класса, созданные в ходе интерактивного сеанса или во время работы программы, получают динамически установленное значение независимо от того, что написано в исходном программном коде класса: № Иожет измениться не только в классе Х № Я тоже изменился Что это — полезная особенность или опасная ловушка7 Решать вам. Фактически вы можете выполнять все необходимые действия по изменению атрибутов класса, не создавая ни единого экземпляра — с помощью этого приема можно имитировать «записи» и «структуры» данных, имеющиеся в других языках программирования. Чтобы освежить воспоминания, рассмотрим следующую не совсем обычную, но вполне допустимую программу на языке Ру1)топ: с1авв Х; раве № Создать несколько пространств ииен атрибутов с1авв У.
раве Ха=) ХЬ=2 Х.с=3 Уа=Ха«ХЬ«ХС Гог Х т ш галде(т.а), огтпт Х.! № Выведет 0..5 Здесь классы Х и У работают как модули «без файло⻠— пространства имен для хранения переменных, которые не конфликтуют между собой. Это совершенно допустимый прием на языке РуЬ)топ, но он не подходит для применения к классам, написанным другими программистами, — вы не всегда можете быть уверены, что атрибуты класса, которые вы изменяете, не являются критически важными для внутренних механизмов класса, Если вы имитируете структуру на языке С, лучше изменять экземпляры, а не класс, поскольку в этом случае изменения будут касаться единственного объекта: с1азв Песогб: Оазв Х = Ивсогб() Х.паев = 'ЬоЬ' Х.]оЬ = Р1ааа вахег' Множественное наследование: порядок имеет значение Это достаточно очевидно, но тем не менее, стоит подчеркнуть: в случае использования множественного наследования порядок, в котором пе- »> Х а = 2 »> 1.а 2 >» З = Х() »> з.а 2 № и' наследует значение, установленное во вреив выполнения № (но присваивание имени и'.а изменяет а е З, но не в Х или д № Использовать атрибуты класса как переменные № В программе нет ни одного экземпляра класса 686 Глава 26.
Дополнительные возможности классов речислены суперклассы в строке заголовка инструкции с1авв, может иметь критическое значение. В ходе поиска интерпретатор всегда просматривает суперклассы слева направо в соответствии с порядком их следования в заголовке инструкции.
Например, в примере множественного наследования, который был продемонстрирован в главе 25, предположим, что класс Берег тоже реализует метод герт, От какого класса мы унаследовали бы метод — от класса Ьтвтег или Зорег7 Это зависело бы от того, какой класс стоит первым в заголовке объявления класса БЬЬ, так как поиск унаследованных атрибутов производится слева направо. Очевидно, мы поставили бы класс Ьтвтег первым в списке, потому что его основная цель состоит в предоставлении метода герт с1авв отвтег: оет герт (ве1() с1авв Берег: Оет герт (ве1(): с1авв Зоь(с(втег, Берег); а Будет унаследован метод герт класса а (твтвг, так квк он стоит в списке первым А теперь предположим, что классы Берег и Ь(втег имеют свои собственные версии еще одного одноименного атрибута. Если необходимо, чтобы одно имя наследовалось от класса Берег, а другое от класса Ьтвтег, изменение порядка их расположения в заголовке инструкции определения подкласса уже не поможет — мы должны вручную переопределить результат наследования, явно выполнив присваивание имени атрибута в классе БЬЬ: с!авв (твтег; Сет герт (ве1(): .
Оет отлет(ве1(): ... с1авв Зорег: се( герт (ве1(): Оет отлет(ве)Г): ... с1авв зоь(ывтег, зорег): а унаследует герт класса (тагес, а так как он первый в списке о!пег = Зорег.о!лег в явно выбирается версия атрибута ив класса Зорег ОЕ( 1птт (ВЕ1(): к = ЗоЬ() В Поиск снанала выполняется в Зоо и только потом в Порет/Ывтег Здесь присваивание атрибуту с именем о!лег в классе ЗЬЬ создает атрибут БЬЬ, о(пег — ссылку на объект Берег.
о(пег. Поскольку эта ссылка находится ниже в дереве классов, зто не позволит механизму наследования выбрать версию атрибута Ивтег.о!лег, который был обнаружен первым при обычных обстоятельствах. Точно так же, если бы класс Берег стоял первым в списке, то, чтобы атрибут о(лег наследовался обыч- 687 Типичные проблемы при работе с классами ным образом, нам могло бы потребоваться явно выбрать метод герт класса 1.1эсе г: с1авв зоб(ьорвг, с1всвг); а получить Пирог. оговг по наследованию герт = Снпег герт а явно выбрать Ы вгвг гврг Множественное наследование — это довольно сложная тема.
Даже если вы поняли предыдущий параграф, все равно этот прием лучше использовать осторожно и только в случае крайней необходимости. В противном случае могут возникать ситуации, когда значение имени атрибута будет зависеть от порядка следования классов в инструкции определения подкласса. (Еще один пример этого приема в действии приводится в этой же главе в разделе «Классы нового стиля«, где обсуждалось явное разрешение конфликтов имен.) Как правило, множественное наследование дает лучшие результаты, когда суперклассы являются максимально автономными, — поскольку они могут использоваться в разных контекстах, они не должны делать каких-либо предположений об именах, связанных с другими классами в дереве.
Псевдочастные атрибуты, которые были изучены нами ранее, могут помочь в локализации имен, на владение которыми опирается класс, и ограничить вероятность появления конфликтов имен в суперклассах, которые вы добавляете в список наследуемых классов. Например, в данном случае класс 1т все г служит только для того, чтобы экспортировать метод герт, поэтому он мог бы дать своему второму методу имя оспе г, чтобы избежать конфликтов с именами в других классах. Методы, классы и вложенные области видимости Эта проблема была ликвидирована в РуФЬоп 2.2 введением областей видимости вложенных функций, но я сохранил это описание исключительно ради истории, для тех из вас, кому приходилось работать с более старыми версиями Ру1Ьоп и с целью продемонстрировать, что происходит в случае вложения функций, когда один из уровней вложенности является классом. Классы„как и функции, обладают своими локальными областями видимости, поэтому области видимости обладают сходными проявлениями в теле инструкции с1зэз.
Кроме того, методы, по сути, являются вложенными функциями, поэтому здесь имеют место те же самые проблемы. Похоже, что путаница особенно часто возникает, когда имеются классы, вложенные друг в друга. В следующем примере (файл пев1епру) функция оепегасе возвращает экземпляр вложенного класса браэ. Внутри этой функции имя класса Зраэ находится в локальной области видимости функции сепегасе.
Но в версиях Ру$Ьоп, появившихся до версии 2.2, внутри метода эеспоб имя класса Ярая недоступно — эеслоб имеет доступ только к своей локальной области видимости, к области видимости модуля, вмещающего окружающую функцию оепега1е, и к встроенным именам: 688 Глава 26. Дополнительные возможности классов бег двпегате(): с1ввв Брав: сопит = 1 свт щетпоб(ве11): № имя Брав недрстулнр: рг1пт Брав.сопит № нв локальное (бег), не глобальное (модуль), № не яртррвннае гвсигп Брав() делегате().щетпсб() С:'тру(поп'кехавр1ев> рутаоп пввтег.ру тгвсвваск ((ппсгщовт 1ввт): Е11е "певтвг.
ру", 1ше 8, 1п ? депегатв().ветппб() Рт1е 'певтвг.ру", 11пв 5, 1п щетпоб рг1пт Брвщ.спирт № Нв локальное (бвт), не глобальное (модуль), ИвщеЕггаг: Брвщ № не встроенное Если вам приходится работать с версией ниже 2.2, скажу, что существует несколько способов заставить предыдущий пример работать. Самый простой заключается в том, чтобы переместить имя Брав в область видимости вмещающего модуля с помощью глобального объявления. Поскольку методу ее с лоб доступны глобальные имена в модуле, попытка сослаться на Зраэ уже не будет вызывать ошибку: бег делегате(): д1пвв1 Брав с1авв Брав: сопят = т бвт щвтьсб(ве11): рг!пт Брвщ.спорт № Работает глобальное имя (вмещающий модуль) гетигп Брав() № Перенести имя Брам в рбласть видиирсти ипдуля двпегвтв().щвтпаб() № Вмввдет 1 Лучше было бы реструктурировать программный код так, чтобы вместо использования объявления д1ооа1 определение класса Брав находилось на верхнем уровне модуля.