Математический анализ. 3е издание
Скачать 4.86 Mb.
|
Явное разрешение конфликтов имен Проблема с предположениями – в том, что они всего лишь предположе ния. Если такое отклонение в процедуре поиска кажется вам слишком трудным для запоминания или вам требуется более полное управление процедурой поиска, вы всегда можете произвести выбор желаемого ат рибута из любого места в дереве, выполнив присваивание или както иначе обозначив его там, где может возникнуть смешение классов: >>> class A: attr = 1 # Классическая модель >>> class B(A): pass >>> class C(A): attr = 2 >>> class D(B,C): attr = C.attr # Выбрать C, справа >>> x = D() >>> x.attr # Работает как класс нового стиля 2 Здесь дерево классических классов имитирует порядок поиска, приня тый в модели классов нового стиля: присваивание атрибуту attr в классе D явно выбирает версию атрибута из класса C, благодаря чему нарушается обычный порядок поиска в дереве наследования (атрибут D.attr находится ниже в дереве). Точно так же классы нового стиля мо гут имитировать порядок поиска в классической модели, выбирая тре буемый атрибут там, где может происходить смешение: >>> class A(object): attr = 1 # Новый стиль >>> class B(A): pass >>> class C(A): attr = 2 >>> class D(B,C): attr = B.attr # Выбрать A.attr, выше >>> x = D() >>> x.attr # Работает как классический класс 1 Если вам необходимо всегда явно разрешать конфликты, подобные этим, вы можете просто игнорировать различия в порядке поиска и не полагаться на предположения о том, что имеется в виду, когда вы пи шете свои классы. Естественно, такой способ выбора атрибутов может также применяться и к методам, потому что методы – это обычные объекты: >>> class A: ... def meth(s): print 'A.meth' >>> class C(A): ... def meth(s): print 'C.meth' 672 Глава 26. Дополнительные возможности классов >>> class B(A): ... pass >>> class D(B,C): pass # Использовать порядок поиска по умолчанию >>> x = D() # Зависит от типа класса >>> x.meth() # По умолчанию используется классический порядок поиска A.meth >>> class D(B,C): meth = C.meth # Выбрать метод класса C: новый стиль >>> x = D() >>> x.meth() C.meth >>> class D(B,C): meth = B.meth # Выбрать метод класса B: # классическая модель >>> x = D() >>> x.meth() A.meth Здесь мы явно выбираем методы, выполняя присваивание именам, на ходящимся ниже в дереве. Мы могли бы просто вызвать метод желае мого класса явно; на практике этот подход, возможно, является более принятым, в особенности при работе с конструкторами: class D(B,C): def meth(self): # Переопределяется ниже C.meth(self) # Вызовом выбрать метод класса C Такой выбор путем присваивания или вызова в точках смешения мо жет эффективно обезопасить ваш программный код от возможных различий между разными моделями классов. Явное разрешение кон фликтов таким способом гарантирует, что правильная работа вашего программного кода не будет зависеть от версии Python в будущем (не зависимо от необходимости наследовать встроенные типы, чтобы ис пользовать новую модель). 1 Итак, поиск в ромбоидальной схеме наследования выполняется по разному в классической и в новой моделях, и это изменение нарушает обратную совместимость. Однако имейте в виду, что это изменение за 1 Даже если не учитывать расхождения между классической и новой моде лями, данная методика иногда может пригодиться в случаях множествен ного наследования. Если вам необходимо получить часть атрибутов от су перкласса слева, а часть – от суперкласса справа, вам может потребоваться указать интерпретатору Python, какие именно атрибуты следует выбирать, выполняя явное присваивание в подклассах. Мы еще вернемся к этому во просу в разделе с описанием типичных проблем в конце этой главы. Кроме того, обратите внимание, что ромбоидальные схемы наследования в неко торых случаях могут доставлять еще больше хлопот, чем я описал здесь (например, что если оба конструктора классов B и C вызывают конструктор класса A, и при этом необходимо вызывать оба наследуемых конструктора в подклассе?), но это лежит за пределами рассмотрения данной книги. Классы нового стиля 673 трагивает только ромбоидальные схемы наследования – во всех дру гих схемах принцип действия модели наследования нового стиля не изменился. Кроме того, вполне возможно, что вся эта проблема будет носить скорее теоретический характер, чем практический, – с выхо дом версии Python 2.2 эти изменения не оказали достаточно сущест венного влияния, и потому маловероятно, что они затронут значитель ную часть программного кода на языке Python. Другие расширения в классах нового стиля Помимо изменения порядка поиска в ромбоидальной схеме (что само по себе слишком неясно, чтобы иметь большое значение для большин ства читателей этой книги) классы нового стиля привносят несколько еще более сложных возможностей. Ниже приводится краткий обзор каждой из них. Статические методы и методы класса Начиная с версии Python 2.2 появилась возможность определять мето ды класса, которые могут вызываться без участия экземпляра: ста+ тические методы работают почти так же, как обычные функции, толь ко расположенные внутри класса, а методы класса получают сам класс вместо экземпляра. Чтобы сделать возможными эти режимы ра боты методов, внутри класса должны вызываться специальные встро енные функции staticmethod и classmethod. Несмотря на то, что эта осо бенность была добавлена вместе с классами нового стиля, ее можно ис пользовать и в классических классах тоже. Вследствие этого отложим обсуждение данной темы до следующего раздела. Слоты экземпляров Присваивая список имен атрибутов в виде строк специальному атрибу ту __slots__ класса, в классах нового стиля можно ограничить множе ство разрешенных атрибутов для экземпляров класса. Обычно этот ат рибут устанавливается присваиванием переменной __slots__ на верх нем уровне в инструкции class: только имена, перечисленные в списке __slots__ , смогут использоваться как атрибуты экземпляра. Однако, как и в случае с любыми именами в языке Python, прежде чем полу чить доступ к атрибутам экземпляра им должны быть присвоены зна чения, даже если они перечислены в списке __slots__. Например: >>> class limiter(object): ... __slots__ = ['age', 'name', 'job'] >>> x = limiter() >>> x.age # Присваивание должно быть выполнено раньше использования AttributeError: age >>> x.age = 40 >>> x.age 40 674 Глава 26. Дополнительные возможности классов >>> x.ape = 1000 # Недопустимое имя: отсутствует в слотах AttributeError: 'limiter' object has no attribute 'ape' (AttributeError: объект 'limiter' не имеет атрибута 'ape') Эта особенность должна помочь ликвидировать ошибки, обусловлен ные простыми «опечатками» (обнаруживается попытка присваивания атрибутам, отсутствующим в списке __slots__) и обеспечить некото рую оптимизацию (атрибуты слота могут сохраняться не в словаре, а в кортеже, обеспечивая тем самым более высокую скорость поиска). Однако слоты – это своего рода нарушение динамической природы языка Python, которая диктует, что операция присваивания может создавать любые имена. Кроме того, у них имеются дополнительные ограничения и следствия, которые слишком сложны, чтобы обсуж дать их здесь. Например, некоторые экземпляры со слотами могут не иметь атрибут словаря __dict__, что может сделать некоторые метапро граммы, которые мы создавали в этой книге, более сложными, напри мер, инструментам, выполняющим поиск атрибутов, скорее всего при дется искать имена в двух источниках вместо одного. За дополнитель ной информацией обращайтесь к документации к выпуску Python 2.2 и стандартному набору руководств по языку Python. Свойства класса Механизм, известный как свойства, обеспечивает в классах нового стиля еще один способ определения методов, вызываемых автоматиче ски при обращении или присваивании атрибутам экземпляра. Эта осо бенность во многих случаях представляет собой альтернативу методам перегрузки операторов __getattr__ и __setattr__, которые мы рассмат ривали в главе 24. Свойства обладают тем же эффектом, что и эти два метода, только в этом случае выполняется вызов метода даже при про стом обращении к атрибуту, что бывает полезно для атрибутов, значе ния которых вычисляются динамически. Свойства (и слоты) основаны на новом понятии дескрипторов атрибутов – темы слишком сложной, чтобы обсуждать ее здесь. Проще говоря, свойства – это тип объектов, который присваивается именам атрибутов класса. Они создаются вызовом встроенной функ ции property с тремя методами (обработчиками операций получения, присваивания и удаления) и строкой документирования – если в ка комлибо аргументе передается значение None, следовательно, эта опе рация не поддерживается. Определение свойств обычно производится на верхнем уровне в инструкции class (например, name = property(...)). Когда выполняется такое присваивание, при попытке доступа к атри буту класса (то есть, obj.name) автоматически будет вызываться один из методов доступа. Например, метод __getattr__ позволяет классам перехватывать попытки доступа к неопределенным атрибутам класса: >>> class classic: ... def __getattr__(self, name): Классы нового стиля 675 ... if name == 'age': ... return 40 ... else: ... raise AttributeError >>> x = classic() >>> x.age # Запустит метод __getattr__ 40 >>> x.name # Запустит метод __getattr__ AttributeError Ниже тот же пример, но уже с использованием свойств: >>> class newprops(object): ... def getage(self): ... return 40 ... age = property(getage, None, None, None) # get,set,del,docs >>> x = newprops() >>> x.age # Запустит метод getage 40 >>> x.name # Нормальная операция извлечения AttributeError: newprops instance has no attribute 'name' (AttributeError: экземпляр newprops не имеет атрибута 'name') В некоторых случаях свойства могут быть менее сложными и работать быстрее, чем при использовании традиционных подходов. Например, когда добавляется поддержка операции присваивания атрибуту, свой ства становятся более привлекательными – программный код выгля дит компактнее и в операцию присваивания не вовлекаются дополни тельные вызовы методов, если не требуется производить дополнитель ных вычислений: >>> class newprops(object): ... def getage(self): ... return 40 ... def setage(self, value): ... print 'set age:', value ... self._age = value ... age = property(getage, setage, None, None) >>> x = newprops() >>> x.age # Запустит метод getage 40 >>> x.age = 42 # Запустит метод setage set age: 42 >>> x._age # Нормальная операция извлечения; нет вызова getage 42 >>> x.job = 'trainer' # Нормальная операция присваивания; нет вызова setage >>> x.job # Нормальная операция извлечения; нет вызова getage 'trainer' 676 Глава 26. Дополнительные возможности классов При эквивалентном классическом решении проблемы класс мог бы производить лишние вызовы метода и, возможно, выполнять присваи вание значения атрибуту с использованием словаря, чтобы избежать зацикливания: >>> class classic: ... def __getattr__(self, name): # При ссылке на неопределенный атрибут ... if name == 'age': ... return 40 ... else: ... raise AttributeError ... def __setattr__(self, name, value): # Для всех операций присваивания ... print 'set:', name, value ... if name == 'age': ... self.__dict__['_age'] = value ... else: ... self.__dict__[name] = value >>> x = classic() >>> x.age # Запустит метод __getattr__ 40 >>> x.age = 41 # Запустит метод __setattr__ set: age 41 >>> x._age # Определен: нет вызова __getattr__ 41 >>> x.job = 'trainer' # Запустит метод __setattr__ опять >>> x.job # Определен: нет вызова __getattr__ Для этого примера свойства обладают неоспоримым преимуществом. Однако в некоторых приложениях методы __getattr__ и __setattr__ по прежнему могут быть востребованы для обеспечения более динамич ных или универсальных интерфейсов, чем можно реализовать с помо щью свойств. Например, во многих случаях невозможно заранее опре делить набор поддерживаемых атрибутов, которые могут даже не суще ствовать вообще в какомлибо виде на момент написания класса (напри мер, при делегировании ссылок на произвольные методы в обернутых/ встроенных объектах). В таких случаях использование более универ сальных методов обслуживания атрибутов __getattr__ и __setattr__, ко торым передаются имена атрибутов, может оказаться предпочтитель нее. Кроме того, простейшие ситуации могут обслуживаться этими об работчиками, поэтому свойства следует рассматривать как дополни тельное и необязательное к использованию расширение. Новый метод перегрузки _ _getattribute_ _ Метод __getattribute__ имеется только в классах нового стиля и позво ляет классам перехватывать все попытки обращения к атрибутам, а не только к неопределенным (как метод __getattr__). Кроме того, этот ме тод более сложен в обращении, чем __getattr__ и __setattr__ (изза бо лее высокой вероятности зацикливания). Полное описание этого мето да я оставляю за стандартной документацией по языку Python. Статические методы и методы класса 677 Помимо всех этих особенностей классы нового стиля интегрируются с понятием расширения типов, упоминавшимся выше в этой главе, – возможность расширения типов и классы нового стиля были введены вместе с разделением тип/класс в версии Python 2.2 и выше. Поскольку особенности классов нового стиля – это достаточно слож ные темы, мы не будем углубляться в подробности в этой книге. За до полнительной информацией обращайтесь к документации к выпуску Python 2.2 и справочным руководствам по языку. Статические методы и методы класса До выхода версии Python 2.2 методы класса никогда нельзя было вызы вать без передачи экземпляра в качестве аргумента. В Python 2.2 и более поздних версиях такое поведение также используется по умолчанию, но существует возможность изменить такое положение дел с помощью но вой дополнительной особенности, получившей название статические методы – простые функции без аргумента self, вложенные в определе ние класса и предназначенные для работы с атрибутами класса, а не эк земпляра. Такие методы обычно используются для обработки информа ции, которая имеет отношение ко всем экземплярам (например, число созданных экземпляров), а не для реализации поведения экземпляров. В предыдущей главе мы говорили о несвязанных методах: когда мы из влекаем функцию по имени класса (а не экземпляра), мы получаем объект несвязанного метода. Хотя объекты несвязанных методов опре деляются с помощью инструкции def, они не являются простыми функ циями – они не могут вызываться без передачи им экземпляра класса. Например, предположим, что необходимо использовать атрибуты класса для подсчета числа экземпляров, созданных из класса (как по казано в следующем файле spam.py). Не забывайте, что атрибуты клас са совместно используются всеми экземплярами, поэтому мы можем хранить счетчик непосредственно в объекте класса: class Spam: numInstances = 0 def __init__(self): Spam.numInstances = Spam.numInstances + 1 def printNumInstances(): print "Number of instances created: ", Spam.numInstances Но такая реализация не будет работать – метод printNumInstances по прежнему ожидает получить экземпляр при вызове, потому что функ ция ассоциирована с классом (даже при том, что в заголовке инструк ции def отсутствуют какиелибо аргументы): >>> from spam import * >>> a = Spam() >>> b = Spam() >>> c = Spam() 678 Глава 26. Дополнительные возможности классов >>> Spam.printNumInstances() Traceback (innermost last): File " TypeError: unbound method must be called with class instance 1st argument (TypeError: несвязанный метод должен вызываться с экземпляром класса в 1м аргументе) Проблема состоит в том, что несвязанные методы экземпляра – это не то же самое, что простые функции. Эта проблема главным образом яв ляется проблемой знания особенностей языка. Самая простая мысль, которая приходит в голову, – сделать метод обычной функцией, а не методом класса. При таком способе функции не требуется передавать экземпляр класса: def printNumInstances(): print "Number of instances created: ", Spam.numInstances class Spam: numInstances = 0 def __init__(self): Spam.numInstances = Spam.numInstances + 1 >>> import spam >>> a = spam.Spam() >>> b = spam.Spam() >>> c = spam.Spam() >>> spam.printNumInstances() Number of instances created: 3 >>> spam.Spam.numInstances 3 Поскольку имя класса доступно простой функции в виде глобальной переменной, все работает прекрасно. Кроме того, обратите внимание, что имя самой функции также является глобальным, но только в этом единственном модуле – оно не будет конфликтовать с именами в дру гих модулях программы. Мы могли бы сделать то же самое, вызывая функцию через экземпляр, как обычно, хотя это не очень удобно, особенно если создание экземп ляра приводит к изменениям в данных класса: class Spam: numInstances = 0 def __init__(self): Spam.numInstances = Spam.numInstances + 1 def printNumInstances(self): print "Number of instances created: ", Spam.numInstances >>> |