Математический анализ. 3е издание
Скачать 4.86 Mb.
|
from spam import Spam >>> a, b, c = Spam(), Spam(), Spam() >>> a.printNumInstances() Number of instances created: 3 >>> b.printNumInstances() Number of instances created: 3 Статические методы и методы класса 679 >>> Spam().printNumInstances() Number of instances created: 4 До появления статических методов в Python 2.2 некоторые теоретики языка утверждали, что в языке Python отсутствуют методы класса – только методы экземпляра. Я полагаю, в действительности они имели в виду, что классы в языке Python работают совсем не так, как в других языках программирования. Что в действительности имеется в языке Python, так это связанные и несвязанные методы с четкой семантикой – при обращении к методу через имя класса вы получаете несвязанный метод, который представляет собой особую разновидность функций. В языке Python имеются атрибуты класса, но функции в классе ожи дают получить экземпляр в виде аргумента. Кроме того, в языке Python уже имеются модули, которые играют роль инструмента разделения пространства имен, поэтому обычно не возникает необходимости упаковывать функции в классы, если они не реализуют функциональность объектов. Простые функции внутри мо дуля обычно способны решать большую часть задач, которые возлага ются на методы класса. Например, в первом фрагменте в этом разделе функция printNumInstances уже связана с классом, потому что распола гается в том же самом модуле. Единственный недостаток этой функ ции состоит в том, что она имеет более широкую область видимости – весь модуль, а не класс. Использование статических методов и методов класса На сегодняшний день существует еще одна возможность писать про стые функции, связанные с классом. Начиная с версии Python 2.2 имеется возможность создавать классы со статическими методами и с методами класса, ни один из которых не требует передачи экземпля ра класса в виде аргумента. Чтобы определить такие методы, в классах необходимо вызывать встроенные функции staticmethod и classmethod, как упоминалось в обсуждении классов нового стиля. Например: class Multi: def imeth(self, x): # Обычный метод экземпляра print self, x def smeth(x): # Статический метод: экземпляр не передается print x def cmeth(cls, x): # Метод класса: получает класс, но не экземпляр print cls, x smeth = staticmethod(smeth) # Сделать smeth статичнским методом cmeth = classmethod(cmeth) # Сделать cmeth методом класса. Обратите внимание, как две последние операции присваивания в этом фрагменте просто переприсваивают имена методов smeth и cmeth. Ат рибуты создаются и изменяются с помощью операции присваивания в инструкции class, поэтому эти заключительные операции присваи вания переопределяют инструкции def, выполненные ранее. 680 Глава 26. Дополнительные возможности классов С технической точки зрения, язык Python теперь поддерживает три раз новидности методов классов: методы экземпляра, статические методы и методы класса. Методы экземпляра – это обычные (и используемые по умолчанию) методы, которые мы видели в этой книге. Для воздейст вия на объект экземпляра всегда следует вызывать методы экземпляра. Когда методы вызываются через экземпляр, интерпретатор автоматиче ски передает экземпляр в первом аргументе – когда метод вызывается через имя класса, экземпляр необходимо передавать методам вручную: >>> obj = Multi() # Создать экземпляр >>> obj.imeth(1) # Обычный вызов, через экземпляр <__main__.Multi instance...> 1 >>> Multi.imeth(obj, 2) # Обычный вызов, через класс <__main__.Multi instance...> 2 Статические методы , напротив, вызываются без аргумента с экземп ляром класса – их имена ограничены областью видимости класса, в ко тором они определяются и могут отыскиваться механизмом наследо вания. Главным образом они действуют как обычные функции, кото рые просто находятся внутри класса: >>> Multi.smeth(3) # Вызов статического метода, через имя класса 3 >>> obj.smeth(4) # Вызов статического метода, через экземпляр 4 Методы класса похожи на них, но интерпретатор автоматически пе редает методам класса сам класс (а не экземпляр) в первом аргументе: >>> Multi.cmeth(5) # Вызов метода класса, через имя класса __main__.Multi 5 >>> obj.cmeth(6) # Вызов метода класса, через экземпляр __main__.Multi 6 Статические методы и методы класса являются новыми дополнитель ными особенностями языка с узко специализированным назначением, для описания которого у нас недостаточно места в этой книге. Статиче ские методы обычно используются для работы с атрибутами класса и управления информацией, которая касается всех экземпляров, соз данных из класса. Например, чтобы вести подсчет количества создан ных экземпляров класса (как в примере выше), можно было бы ис пользовать статический метод, управляющий счетчиком, присоеди ненным к классу в виде атрибута. Такой счетчик не имеет ничего об щего ни с одним из экземпляров, поэтому было бы неудобно иметь методы, которые обслуживают его, вызываемые через экземпляр (тем более, что создание экземпляра для доступа к счетчику может изме нить этот счетчик). Кроме того, близость статических методов к клас су обеспечивает более естественное решение, чем ориентированные на класс функции за пределами класса. Ниже приводится реализация статического метода, эквивалентного оригинальному примеру этого раздела: Декораторы функций 681 class Spam: numInstances = 0 def __init__(self): Spam.numInstances += 1 def printNumInstances(): print "Number of instances:", Spam.numInstances printNumInstances = staticmethod(printNumInstances) >>> a = Spam() >>> b = Spam() >>> c = Spam() >>> Spam.printNumInstances() Number of instances: 3 >>> a.printNumInstances() Number of instances: 3 По сравнению с простым перемещением printNumInstances за пределы класса, как описывалось ранее, эта версия требует дополнительный вызов функции staticmethod. При этом здесь область видимости имени функции ограничена классом (имя не будет вступать в конфликт с дру гими именами в модуле) и программный код перемещен туда, где он используется (внутрь инструкции class). Судите сами, является ли это чистым усовершенствованием или нет. В последних версиях Python определение статических методов выпол няется еще проще; следующий раздел описывает – как. Декораторы функций Прием с вызовом функции staticmethod, описанный в предыдущем раз деле, выглядит малопонятным для некоторых пользователей, поэтому была добавлена возможность, упрощающая эту операцию. Декорато+ ры функций обеспечивают способ определять специальные режимы работы для функций, обертывая их дополнительным слоем логики, реализованной в виде других функций. Декораторы функций представляют собой более универсальные инст рументы: их удобно использовать для добавления самой разной логи ки к функциям, помимо статических методов. Например, их можно использовать для расширения функций программным кодом, выпол няющим регистрацию вызовов этих функций, проверяющим типы пе редаваемых аргументов в процессе отладки и т. д. В некоторой степени декораторы функций напоминают шаблон проектирования делегирова+ ния , исследованный нами в главе 25, но их главная цель состоит в том, чтобы расширять определенные функции или методы, а не весь интер фейс объекта. Язык Python предоставляет несколько встроенных декораторов функ ций для выполнения таких действий, как создание статических мето дов, но программисты также имеют возможность создавать свои собст венные декораторы. Несмотря на то, что они строго не привязаны 682 Глава 26. Дополнительные возможности классов к классам, тем не менее, пользовательские декораторы функций часто оформляются как классы, в которых сохраняется оригинальная функ ция наряду с другими данными, такими как информация о состоянии. Синтаксически декоратор функции – это разновидность объявления функции времени выполнения. Декоратор функции записывается в строке непосредственно перед строкой с инструкцией def, которая определяет функцию или метод, и состоит из символа @, за которым следует то, что называется метафункцией, – функция (или другой вы зываемый объект), которая управляет другой функцией. В настоящее время статические методы, к примеру, могут быть оформлены в виде декораторов, как показано ниже: class C: @staticmethod def meth(): С технической точки зрения, это объявление имеет тот же эффект, что и фрагмент ниже (передача функции декоратору и присваивание ре зультата первоначальному имени функции): class C: def meth(): meth = staticmethod(meth) # Повторное присваивание имени В результате вызов метода по имени функции фактически будет при водить к вызову результата, полученному от декоратора staticmethod. Декоратор может возвращать объекты любого типа, поэтому данный прием позволяет декоратору вставлять дополнительный уровень логи ки, который будет запускаться при каждом вызове. Декоратор функ ции может возвращать как оригинальную функцию, так и новый объ ект, в котором хранится оригинальная функция, переданная декора тору, которая будет вызываться косвенно после того, как будет выпол нен дополнительный слой логики. Синтаксис декораторов поддерживает добавление нескольких слоев обертывающей логики к декорируемой функции или методу. В этом случае строка с декоратором выглядит следующим образом: @A @B @C def f(): что эквивалентно следующему фрагменту: def f(): f = A(B(C(f))) Здесь оригинальная функция проходит через три различных декора тора, а результат присваивается оригинальному имени. Напомню еще Декораторы функций 683 раз, что в результате всего этого, когда происходит обращение к ориги нальному имени функции, вызываются три слоя логики, дополняю щие оригинальную функцию. Пример декоратора Ниже приводится пример декоратора, определяемого пользователем. Вспомните, как в главе 24 говорилось, что метод перегрузки оператора __call__ реализует интерфейс вызова функций в экземплярах классов. В следующем примере этот метод используется в определении класса, который сохраняет декорируемую функцию в экземпляре и перехва тывает вызовы по оригинальному имени. А так как это класс, кроме всего прочего в нем имеется возможность хранить информацию о со стоянии (счетчик произведенных вызовов): class tracer: def __init__(self, func): self.calls = 0 self.func = func def __call__(self, *args): self.calls += 1 print 'call %s to %s' % (self.calls, self.func.__name__) self.func(*args) @tracer def spam(a, b, c): # Обертывает spam в объектдекоратор print a, b, c spam(1, 2, 3) # В действительности вызывается объектобертка spam('a', 'b', 'c') # То есть вызывается метод __call__ в классе spam(4, 5, 6) # Метод __call__ выполняет дополнительные действия # и вызывает оригинальную функцию Функция spam передается декоратору tracer, поэтому, когда произво дится вызов к оригинальному имени spam, в действительности вызыва ется метод __call__ в классе. Этот метод подсчитывает и регистрирует вызовы, а затем вызывает оригинальную обернутую функцию. Обра тите внимание, как используется синтаксис аргумента *name для упа ковки аргументов, передаваемых функции, – благодаря этому данный декоратор может использоваться для обертывания любой функции с любым числом аргументов. В результате к оригинальной функции spam добавляется слой дополни тельной логики. Ниже приводится вывод, полученный от сценария, – первая строка создана классом tracer, а вторая – функцией spam: call 1 to spam 1 2 3 call 2 to spam a b c call 3 to spam 4 5 6 684 Глава 26. Дополнительные возможности классов Исследуйте программный код этого примера повнимательнее, чтобы вникнуть в его суть. Декораторы функций являют собой универсальный механизм, и, тем не менее, эта дополнительная особенность представляет интерес в первую очередь для разработчиков инструментальных средств, а не для при кладных программистов, поэтому я снова отсылаю вас за более подроб ной информацией к стандартному набору руководств по языку Python. Типичные проблемы при работе с классами Большая часть типичных проблем, связанных с классами, сводится к проблемам, связанным с пространствами имен (особенно если учесть, что классы – это всего лишь пространства имен с некоторыми дополнительными особенностями). Некоторые темы, которые мы за тронем в этом разделе, скорее являются передовыми приемами ис пользования классов, чем проблемами, а решение однойдвух их этих проблем было упрощено в последних версиях Python. Изменение атрибутов класса может приводить к побочным эффектам Теоретически классы (и экземпляры классов) относятся к категории изменяемых объектов. Подобно таким встроенным типам, как списки и словари, они могут изменяться непосредственно, путем присваива ния значений атрибутам и, как и в случае со списками и словарями, это означает, что изменение класса или экземпляра может оказывать влияние на множественные ссылки на них. Обычно это именно то, что нам требуется (так объекты изменяют свое состояние), но, изменяя атрибуты, об этом необходимо помнить. Все экземпляры класса совместно используют одно и то же пространство имен класса, поэтому любые изменения на уровне класса будут отра жаться на всех экземплярах, если, конечно, они не имеют собствен ных версий атрибутов класса. Классы, модули и экземпляры – это всего лишь объекты с пространст вами имен атрибутов, поэтому во время выполнения они обычно изме няются с помощью операций присваивания. Рассмотрим следующий класс. В теле класса выполняется присваивание имени a, в результате чего создается атрибут X.a, который во время выполнения располагает ся в объекте класса и будет унаследован всеми экземплярами класса X: >>> class X: ... a = 1 # Атрибут класса >>> I = X() >>> I.a # Унаследован экземпляром 1 >>> X.a 1 Типичные проблемы при работе с классами 685 Пока все неплохо – это обычный случай. Но, обратите внимание, что происходит, когда атрибут класса изменяется динамически, изза его пределов: это приводит к одновременному изменению атрибута во всех объектах, наследующих его от класса. Кроме того, новые экземпляры класса, созданные в ходе интерактивного сеанса или во время работы программы, получают динамически установленное значение незави симо от того, что написано в исходном программном коде класса: >>> X.a = 2 # Может измениться не только в классе X >>> I.a # Я тоже изменился 2 >>> J = X() # J наследует значение, установленное во время выполнения >>> J.a # (но присваивание имени J.a изменяет a в J, но не в X или I) 2 Что это – полезная особенность или опасная ловушка? Решать вам. Фактически вы можете выполнять все необходимые действия по изме нению атрибутов класса, не создавая ни единого экземпляра – с помо щью этого приема можно имитировать «записи» и «структуры» дан ных, имеющиеся в других языках программирования. Чтобы осве жить воспоминания, рассмотрим следующую не совсем обычную, но вполне допустимую программу на языке Python: class X: pass # Создать несколько пространств имен атрибутов class Y: pass X.a = 1 # Использовать атрибуты класса как переменные X.b = 2 # В программе нет ни одного экземпляра класса X.c = 3 Y.a = X.a + X.b + X.c for X.i in range(Y.a): print X.i # Выведет 0..5 Здесь классы X и Y работают как модули «без файлов» – пространства имен для хранения переменных, которые не конфликтуют между со бой. Это совершенно допустимый прием на языке Python, но он не под ходит для применения к классам, написанным другими программи стами, – вы не всегда можете быть уверены, что атрибуты класса, ко торые вы изменяете, не являются критически важными для внутрен них механизмов класса. Если вы имитируете структуру на языке C, лучше изменять экземпляры, а не класс, поскольку в этом случае из менения будут касаться единственного объекта: class Record: pass X = Record() X.name = 'bob' X.job = 'Pizza maker' Множественное наследование: порядок имеет значение Это достаточно очевидно, но тем не менее, стоит подчеркнуть: в случае использования множественного наследования порядок, в котором пе 686 Глава 26. Дополнительные возможности классов речислены суперклассы в строке заголовка инструкции class, может иметь критическое значение. В ходе поиска интерпретатор всегда про сматривает суперклассы слева направо в соответствии с порядком их следования в заголовке инструкции. Например, в примере множественного наследования, который был про демонстрирован в главе 25, предположим, что класс Super тоже реали зует метод __repr__. От какого класса мы унаследовали бы метод – от класса Lister или Super? Это зависело бы от того, какой класс стоит первым в заголовке объявления класса Sub, так как поиск унаследо ванных атрибутов производится слева направо. Очевидно, мы поста вили бы класс Lister первым в списке, потому что его основная цель состоит в предоставлении метода __repr__: class Lister: def __repr__(self): ... class Super: def __repr__(self): ... class Sub(Lister, Super): # Будет унаследован метод __repr__ класса # Lister, так как он стоит в списке первым А теперь предположим, что классы Super и Lister имеют свои собствен ные версии еще одного одноименного атрибута. Если необходимо, что бы одно имя наследовалось от класса Super, а другое от класса Lister, изменение порядка их расположения в заголовке инструкции опреде ления подкласса уже не поможет – мы должны вручную переопреде лить результат наследования, явно выполнив присваивание имени ат рибута в классе Sub: class Lister: def __repr__(self): ... def other(self): ... class Super: def __repr__(self): ... def other(self): ... class Sub(Lister, Super): # Унаследует __repr__ класса Lister, # так как он первый в списке other = Super.other # Явно выбирается версия атрибута из класса Super def __init__(self): x = Sub() # Поиск сначала выполняется в Sub и только потом в Super/Lister Здесь присваивание атрибуту с именем other в классе Sub создает атри бут Sub.other – ссылку на объект Super.other. Поскольку эта ссылка на ходится ниже в дереве классов, это не позволит механизму наследова ния выбрать версию атрибута Lister.other, который был обнаружен первым при обычных обстоятельствах. Точно так же, если бы класс Su per стоял первым в списке, то, чтобы атрибут other наследовался обыч |