справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
Управление памятью объектов Когда объявляется класс, в результате получается класс, который служит фабрикой по производству экземпляров новых экземпляров. Например: class Circle(object): def __init__(self,radius): self.radius = radius ёё # Создать несколько экземпляров класса Circle c = Circle(4.0) d = Circle(5.0) Создание экземпляра выполняется в два этапа, с использованием специ- ального метода __new__(), создающего новый экземпляр, и метода __init__(), Управление памятью объектов 173 инициализирующего его. Например, операция c = Circle(4.0) выполняется в два этапа: c = Circle.__new__(Circle, 4.0) if isinstance(c,Circle): Circle.__init__(c,4.0) Метод __new__() класса достаточно редко определяется в программах. Если он определяется, то обычно его объявление следует шаблону __new__(cls, *args, **kwargs) , где args и kwargs – это те же самые аргументы, что пере- даются методу __init__(). Метод __new__() всегда является методом класса, который принимает объект класса в первом аргументе. Хотя метод __new__ () создает экземпляр, он не вызывает метод __init__() автоматически. Если вам доведется увидеть объявление метода __new__() в классе, обыч- но это может означать одно из двух. Во-первых, класс может наследовать базовый класс, экземпляры которого относятся к разряду неизменяемых объектов. Эта ситуация характерна для классов, которые наследуют встро- енные неизменяемые типы, такие как целые числа, строка или кортеж, потому что метод __new__() является единственным методом, вызываемым перед созданием экземпляра, и единственным, где такое значение может быть изменено (в этом смысле метод __init__() вызывается слишком позд- но). Например: class Upperstr(str): def __new__(cls,value=””): return str.__new__(cls, value.upper()) ёё u = Upperstr(“hello”) # Вернет значение “HELLO” Другой случай, когда может потребоваться переопределить метод __new__ () , – при объявлении метаклассов. Подробнее о них рассказывается в кон- це этой главы. После создания экземпляра управление им осуществляется за счет под- счета ссылок. Когда счетчик ссылок уменьшается до нуля, экземпляр тут же уничтожается. Когда происходит уничтожение экземпляра, интер- претатор сначала отыскивает метод __del__() объекта и вызывает его. На практике необходимость в определении метода __del__() возникает редко. Единственное исключение – когда в процессе уничтожения объекта необ- ходимо выполнить некоторые завершающие действия, например закрыть файл, разорвать сетевое соединение или освободить другие системные ресурсы. Однако даже в таких случаях нельзя полностью полагаться на метод __del__(), потому что нет никаких гарантий, что этот метод будет вызван при завершении работы интерпретатора. Для этих целей лучше определить метод, такой как close(), который программа сможет явно ис- пользовать при завершении. Иногда для удаления ссылки на объект в программах используется ин- струкция del. Если в этом случае счетчик ссылок уменьшается до нуля, вызывается метод __del__(). Однако в общем случае вызов инструкции del не обязательно приводит к вызову метода __del__(). 174 Глава 7. Классы и объектно-ориентированное программирование С уничтожением объектов связана одна малозаметная проблема, которая обусловлена тем, что не все ситуации, когда должен вызываться метод __del__() , распознаются циклическим сборщиком мусора (что является веской причиной не определять метод __del__() без серьезных на то осно- ваний). Программисты, пришедшие из других языков программирования, в которых отсутствует механизм автоматического сбора мусора (например, C++), должны стараться уклониться от стиля программирования с не- оправданным использованием метода __del__(). Ситуации, когда объявле- ние метода __del__() может нарушить работу сборщика мусора, возникают редко, тем не менее существуют определенные программные случаи, в ко- торых это может вызывать ошибки, особенно когда речь заходит об отноше- ниях родитель-потомок или о графах. Например, предположим, что име- ется объект, реализованный в соответствии с шаблоном проектирования «наблюдатель». class Account(object): def __init__(self,name,balance): self.name = name self.balance = balance self.observers = set() def __del__(self): for ob in self.observers: ob.close() del self.observers def register(self,observer): self.observers.add(observer) def unregister(self,observer): self.observers.remove(observer) def notify(self): for ob in self.observers: ob.update() def withdraw(self,amt): self.balance -= amt self.notify() ёё class AccountObserver(object): def __init__(self, theaccount): self.theaccount = theaccount theaccount.register(self) def __del__(self): self.theaccount.unregister(self) del self.theaccount def update(self): print(“Баланс: %0.2f” % self.theaccount.balance) def close(self): print(“Наблюдение за счетом окончено”) ёё # Пример создания a = Account(‘Дейв’,1000.00) a_ob = AccountObserver(a) Управление памятью объектов 175 В этом фрагменте объявляется класс Account, позволяющий устанавливать множество объектов класса AccountObserver для наблюдения за экземпля- ром Account, которые извещаются о любых изменениях баланса. Для это- го каждый экземпляр Account сохраняет ссылки на множество объектов- наблюдателей, а каждый экземпляр AccountObserver хранит ссылку на объ- ект счета. Каждый из этих классов объявляет метод __del__() в надежде получить возможность выполнять некоторые заключительные операции (такие как закрытие связи между объектами и так далее). Однако это про- это про- это про- про- про- сто не работает. Дело в том, что между объектами образуются циклические ссылки, вследствие этого счетчик ссылок никогда не достигает нуля и за- ключительные операции не выполняются. Но и это еще не все, сборщик мусора (модуль gc) не может даже удалить эти объекты, что приводит к по- стоянной утечке памяти. Один из способов исправить эту проблему, приведенный в следующем при- мере, состоит в том, чтобы с помощью модуля weakref создать слабую ссыл- ку из одного класса на другой класс. Слабая ссылка – это такая ссылка на объект, создание которой не приводит к увеличению счетчика ссылок. При использовании слабых ссылок необходимо добавлять дополнительный программный код, который будет проверять наличие объекта по ссылке. Ниже приводится пример измененного класса наблюдателя: import weakref class AccountObserver(object): def __init__(self, theaccount): self.accountref = weakref.ref(theaccount) # Создаст слабую ссылку theaccount.register(self) def __del__(self): acc = self.accountref() # Вернет объект счета if acc: # Прекратить наблюдение, если существует acc.unregister(self) def update(self): print(“Баланс: %0.2f” % self.accountref().balance) def close(self): print(“Наблюдение за счетом окончено”) ёё # Пример создания a = Account(‘Дейв’,1000.00) a_ob = AccountObserver(a) В этом примере создается слабая ссылка accountref. Чтобы получить объект Account , на который она ссылается, к ней следует обращаться, как к функ- ции. Это вызов вернет либо объект Account, либо None, если наблюдаемый объект больше не существует. После внесения этих изменений цикличе- ские ссылки больше не создаются. Если теперь объект Account будет уни- чтожен, интерпретатор вызовет его метод __del__, и все наблюдатели полу- чат извещение. Кроме того, исчезнут препятствия в работе модуля gc. Более подробная информация о модуле weakref приводится в главе 13 «Службы Python времени выполнения». 176 Глава 7. Классы и объектно-ориентированное программирование Представление объектов и связывание атрибутов А трибуты объектов внутри реализованы с использованием словаря, до- ступного в виде атрибута __dict__. Этот словарь содержит уникальные дан- ные для каждого экземпляра. Например: >>> a = Account(‘Гвидо’, 1100.0) >>> a.__dict__ {‘balance’: 1100.0, ‘name’: ‘Гвидо’} Новые атрибуты могут быть добавлены к экземпляру в любой момент, на- пример: a.number = 123456 # Добавит в словарь a.__dict__ атрибут ‘number’ Любые модификации в экземпляре всегда отражаются на содержимом локального атрибута __dict__. Точно так же любые модификации, вы- полненные непосредственно в словаре __dict__, отразятся на содержимом атрибутов. Экземпляры содержат ссылки на свои классы в специальном атрибуте __class__ . Сам по себе класс также является лишь тонкой оберткой во- круг словаря, доступного в виде атрибута __dict__. Словарь класса – это место, где хранятся ссылки на методы. Например: >>> a.__class__ >>> Account.__dict__.keys() [‘__dict__’, ‘__module__’, ‘inquiry’, ‘deposit’, ‘withdraw’, ‘__del__’, ‘num_accounts’, ‘__weakref__’, ‘__doc__’, ‘__init__’] >>> Наконец, классы хранят ссылки на свои базовые классы в специальном атрибуте __bases__, который является кортежем базовых классов. Эта структура составляет основу всех операций чтения, записи и удаления атрибутов объектов. Всякий раз, когда с помощью инструкции obj.name = value выполняется запись значения в атрибут, вызывается специальный метод obj.__setattr__ (“ name”, value) . Когда атрибут удаляется c помощью инструкции del obj. name , вызывается специальный метод obj.__delattr__(“name”). По умолча- нию эти методы изменяют или удаляют значения из локального словаря __dict__ экземпляра obj, если требуемый атрибут не является свойством или дескриптором. В противном случае выполняются операции записи или удаления, реализованные в виде функций, связанных со свойством. При обращении к атрибуту экземпляра, такому как obj.name, вызывается специальный метод obj.__getattribute__(“name”). Этот метод осуществляет поиск атрибута, в процессе которого выясняется, не является ли атрибут свойством, проверяется содержимое локального атрибута __dict__, содер- жимое словаря класса и при необходимости выполняется поиск в базо- вых классах. Если поиск не увенчался успехом, производится последняя попытка отыскать атрибут вызовом метода __getattr__() класса (если он __slots__ 177 определен). Если и эта попытка оканчивается неудачей, возбуждается ис- ключение AttributeError. Пользовательские классы могут реализовать собственные версии функций доступа к атрибутам, если это необходимо. Например: class Circle(object): def __init__(self,radius): self.radius = radius def __getattr__(self,name): if name == ‘area’: return math.pi*self.radius**2 elif name == ‘perimeter’: return 2*math.pi*self.radius else: return object.__getattr__(self,name) def __setattr__(self,name,value): if name in [‘area’,’perimeter’]: raise TypeError(“%s is readonly” % name) object.__setattr__(self,name,value) Класс, реализующий собственные версии этих методов, для выполнения основной работы, вероятно, должен опираться на реализацию по умолча- нию. Это обусловлено тем, что реализация по умолчанию учитывает массу дополнительных особенностей классов, таких как дескрипторы и свойства. Вообще говоря, в классах достаточно редко возникает необходимость пе- реопределять операции обращения к атрибутам. Единственная область, где такая потребность возникает достаточно часто, – это разработка уни- версальных оберток для существующих объектов. Переопределяя методы __getattr__() , __setattr__() и __delattr__(), обертка может перехватывать обращения к атрибутам и прозрачно переадресовывать эти операции в дру- гой объект. __slots__ Существует возможность ограничить класс определенным набором имен атрибутов, определив специальную переменную __slots__. Например: class Account(object): __slots__ = (‘name’,’balance’) ... Если в классе определена переменная __slots__, экземпляры такого класса смогут иметь атрибуты только с указанными именами. В противном случае будет возбуждаться исключение AttributeError. Это ограничение исключает возможность добавления новых атрибутов к существующим экземплярам и решает проблему присваивания значений атрибутам, в именах которых допущена опечатка. В действительности переменная __slots__ задумывалась совсем не в ка- честве меры предосторожности. Фактически это инструмент оптимиза- ции по объему занимаемой памяти и скорости выполнения. Экземпляры классов, где определена переменная __slots__, уже не используют словарь 178 Глава 7. Классы и объектно-ориентированное программирование для хранения данных экземпляра. Вместо него используется более ком- пактная структура данных, в основе которой лежит массив. Применение переменной __slots__ в программах, создающих огромное число объектов, может существенно уменьшить объем потребляемой памяти и увеличить скорость выполнения. Следует заметить, что переменная __slots__ по-особенному воздействует на наследование. Если в базовом классе используется переменная __slots__, производный класс также должен объявлять переменную __slots__ со спи- ском имен своих атрибутов (даже если он не добавляет новых атрибутов), чтобы иметь возможность использовать преимущества, предоставляемые этой переменной. Если этого не сделать, производный класс будет работать медленнее и занимать памяти даже больше, чем в случае, когда перемен- ная __slots__ не используется ни в одном из классов! Кроме того, использование переменной __slots__ может нарушить работо- способность программного кода, который ожидает, что экземпляры будут иметь атрибут __dict__. Хотя такая ситуация не характерна для приклад- ного программного кода, тем не менее вспомогательные библиотеки и дру- гие инструменты поддержки объектов могут использовать словарь __dict__ для отладки, сериализации объектов и выполнения других операций. Наконец, наличие объявления переменной __slots__ не требует переопреде- ления в классе таких методов, как __getattribute__(), __getattr__() и __set- attr__() . По умолчанию эти методы учитывают возможность наличия пере- умолчанию эти методы учитывают возможность наличия пере- умолчанию эти методы учитывают возможность наличия пере- эти методы учитывают возможность наличия пере- эти методы учитывают возможность наличия пере- методы учитывают возможность наличия пере- методы учитывают возможность наличия пере- учитывают возможность наличия пере- учитывают возможность наличия пере- возможность наличия пере- возможность наличия пере- наличия пере- наличия пере- пере- пере- менной __slots__. Кроме того, следует подчеркнуть, что нет никакой необ- Кроме того, следует подчеркнуть, что нет никакой необ- ходимости добавлять имена методов и свойств в переменную __slots__, так как они хранятся не на уровне экземпляров, а на уровне класса. Перегрузка операторов Пользовательские объекты можно заставить работать со всеми встроенны- ми операторами языка Python, добавив в класс реализации специальных методов, описанных в главе 3. Например, чтобы добавить в язык Python поддержку нового типа чисел, можно было бы объявить класс со специаль- ными методами, такими как __add__(), определяющими порядок работы стандартных математических операторов с экземплярами. Следующий ниже пример демонстрирует использование этого приема, объ- являя класс, который реализует комплексные числа с поддержкой некото- рых стандартных математических операторов. Примечание Поддержка комплексных чисел уже имеется в языке Python, поэтому данный класс приводится исключительно в демонстрационных целях. class Complex(object): def __init__(self,real,imag=0): self.real = float(real) self.imag = float(imag) def __repr__(self): Перегрузка операторов 179 return “Complex(%s,%s)” % (self.real, self.imag) def __str__(self): return “(%g+%gj)” % (self.real, self.imag) # self + other def __add__(self,other): return Complex(self.real + other.real, self.imag + other.imag) # self - other def __sub__(self,other): return Complex(self.real - other.real, self.imag - other.imag) В этом примере метод __repr__() возвращает строку, которая может быть использована для повторного создания объекта с помощью функции eval() (а именно, “Complex(real,imag)”). Этому соглашению должны следовать все объекты по мере возможности. Метод __str__(), напротив, создает строку, предназначенную для форматированного вывода (именно эту строку выво- дит инструкция print). Другие специальные методы, такие как __add__() и __sub__(), реализуют математические операции. Реализация этих операторов тесно связана с по- рядком следования операндов и с их типами. В предыдущем примере, где реализованы методы __add__() и __sub__(), соответствующие им математи- ческие операторы могут применяться, только если комплексное число сто- ит слева от оператора. Они не будут вызываться, если комплексное число находится справа, а слева стоит операнд, который не является объектом типа Complex. Например: >>> c = Complex(2,3) >>> c + 4.0 Complex(6.0,3.0) >>> 4.0 + c Traceback (most recent call last): File “ TypeError: unsupported operand type(s) for +: ‘int’ and ‘Complex’ (Перевод: Трассировочная информация (самый последний вызов – самый нижний): Файл “ TypeError: неподдерживаемые типы операндов для +: ‘int’ и ‘Complex’ ) >>> Отчасти операция c + 4.0 выполнилась случайно. Просто все встроенные числовые типы в языке Python уже имеют атрибуты .real и .imag, поэто- му они могут участвовать в вычислениях. Если бы объект не имел этих атрибутов, попытка выполнить операцию окончилась бы неудачей. Если бы потребовалось обеспечить в классе Complex возможность выполнения операций с объектами, не имеющими этих атрибутов, можно было бы реа- лизовать дополнительные преобразования (которые могут зависеть от типа объекта, представляющего другой операнд). Попытка выполнить операцию 4.0 + c провалилась, потому что встроен- ный тип чисел с плавающей точкой ничего не знает о классе Complex. Чтобы исправить эту проблему, можно добавить в класс Complex методы для рабо- ты с операндами, стоящими в обратном порядке: 180 Глава 7. Классы и объектно-ориентированное программирование class Complex(object): ... def __radd__(self,other): return Complex(other.real + self.real, other.imag + self.imag) def __rsub__(self,other): return Complex(other.real - self.real, other.imag - self.img) ... Э ти методы играют роль запасного варианта. Когда операция 4.0 + c потер- пит неудачу, прежде чем возбудить исключение TypeError, интерпретатор попробует вызвать метод c.__radd__(4.0). В более старых версиях Python использовались различные подходы, осно- Python использовались различные подходы, осно- использовались различные подходы, осно- ванные на приведении типов операндов. Например, в устаревшем про- граммном коде на языке Python можно встретить классы, реализующие метод __coerce__(). Он больше не используется в версиях Python 2.6 и Py- больше не используется в версиях Python 2.6 и Py- больше не используется в версиях Python 2.6 и Py- не используется в версиях Python 2.6 и Py- не используется в версиях Python 2.6 и Py- используется в версиях Python 2.6 и Py- используется в версиях Python 2.6 и Py- в версиях Python 2.6 и Py- в версиях Python 2.6 и Py- версиях Python 2.6 и Py- версиях Python 2.6 и Py- Python 2.6 и Py- и Py- Py- thon 3. Кроме того, вас не должны вводить в заблуждение такие специ- Кроме того, вас не должны вводить в заблуждение такие специ- альные методы, как __int__(), __float__() или __complex__(). Эти методы вызываются в операциях явного преобразования типа, таких как int(x) или float(x), но они никогда не вызываются для неявного преобразования типов в арифметических операциях. Поэтому при создании классов с опе- раторами, допускающими использование операндов различных типов, не- обходимо предусматривать явное приведение типов в реализации каждого оператора. Типы и проверка принадлежности к классу Когда создается экземпляр класса, типом этого экземпляра становится сам класс. Для проверки принадлежности к классу используется встроенная функция isinstance(obj,cname). Эта функция возвращает значение True, если объект obj принадлежит классу cname или любому другому классу, произ- водному от cname. Например: class A(object): pass class B(A): pass class C(object): pass ёё a = A() # Экземпляр класса ‘A’ b = B() # Экземпляр класса ‘B’ c = C() # Экземпляр класса ‘C’ ёё type(a) # Вернет класс объекта: A isinstance(a,A) # Вернет True isinstance(b,A) # Вернет True, класс B – производный от класса A isinstance(b,C) # Вернет False, класс C не является производным от класса A Аналогично встроенная функция issubclass(A,B) вернет True, если класс A является подклассом класса B. Например: issubclass(B,A) # Вернет True issubclass(C,A) # Вернет False Одна из проблем, связанных с проверкой типов объектов, состоит в том, что программисты часто не используют механизм наследования, а просто соз- Типы и проверка принадлежности к классу 181 дают объекты, имитирующие поведение другого объекта. В качестве при- мера рассмотрим два следующих класса: class Foo(object): def spam(self,a,b): pass ёё class FooProxy(object): def __init__(self,f): self.f = f def spam(self,a,b): return self.f.spam(a,b) В этом примере класс FooProxy функционально идентичен классу Foo. Он реализует те же самые методы, а его внутренняя реализация даже исполь- зует класс Foo. Однако для системы типов классы FooProxy и Foo являются совершенно разными. Например: f = Foo() # Создаст объект класса Foo g = FooProxy(f) # Создаст объект класса FooProxy isinstance(g, Foo) # Вернет False Если в программе будет предусмотрена явная проверка на принадлеж- ность к классу Foo с помощью функции isinstance(), программа определен- но не будет работать с объектом FooProxy. Однако в действительности совсем не всегда требуется соблюдение такой степени строгости. Иногда бывает достаточно убедиться, что некоторый объект может использоваться вме- сто объекта Foo, так как он реализует точно такой же интерфейс. Сделать это возможно, создав объект, который переопределяет поведение функций isinstance() и issubclass(), позволяя сгруппировать объекты вместе и про- верить их типы. Например: class IClass(object): def __init__(self): self.implementors = set() def register(self,C): self.implementors.add(C) def __instancecheck__(self,x): return self.__subclasscheck__(type(x)) def __subclasscheck__(self,sub): return any(c in self.implementors for c in sub.mro()) ёё # Пример использования объекта IFoo = IClass() IFoo.register(Foo) IFoo.register(FooProxy) В этом примере создается объект класса IClass, который просто объеди- няет в множество коллекцию разных классов. Метод register() добавля- добавля- ет в множество новый класс. Специальный метод __instancecheck__() вы- зывается при выполнении любой операции isinstance(x, IClass). Специ- альный метод __subclasscheck__() вызывается при выполнении операции issubclass( C,IClass) 182 Глава 7. Классы и объектно-ориентированное программирование Те перь с помощью объекта IFoo с зарегистрированными в нем классами можно выполнять проверку типов, как показано ниже: f = Foo() # Создаст объект класса Foo g = FooProxy(f) # Создаст объект класса FooProxy isinstance(f, IFoo) # Вернет True isinstance(g, IFoo) # Вернет True issubclass(FooProxy, IFoo) # Вернет True Следует отметить, что в этом примере не производится строгая проверка типов. Операции проверки экземпляров в объекте IFoo перегружены так, что проверяют лишь принадлежность к группе классов. Здесь не делается никаких утверждений о фактическом программном интерфейсе и не вы- полняется никаких других проверок. В действительности, в этом объекте можно зарегистрировать любые классы по своему выбору, независимо от отношений между ними. Обычно группировка классов основана на неко- тором критерии, например все классы реализуют один и тот же программ- ный интерфейс. Однако нельзя с уверенностью делать подобные выводы при использовании перегруженных методов __instancecheck__() или __sub- sub- classcheck__() . Фактическая интерпретация этого факта остается за при- интерпретация этого факта остается за при- интерпретация этого факта остается за при- этого факта остается за при- этого факта остается за при- факта остается за при- факта остается за при- остается за при- остается за при- за при- за при- при- при- ложением. В языке Python имеется более формальный механизм группировки объ- Python имеется более формальный механизм группировки объ- имеется более формальный механизм группировки объ- ектов, определения интерфейсов и проверки типов. Все это может быть достигнуто за счет определения абстрактных базовых классов, о которых рассказывается в следующем разделе. Абстрактные базовые классы В предыдущем разделе была продемонстрирована возможность перегрузки операций isinstance() и issubclass(). Благодаря ей можно создавать объек- ты, группирующие похожие классы и выполняющие различные операции по проверке типов. Абстрактные базовые классы построены на основе этой концепции и реализуют механизм организации объектов в иерархии, позволяющий утверждать о наличии требуемых методов и делать другие выводы. Для определения абстрактного базового класса используется модуль abc. Этот модуль определяет метакласс (ABCMeta) и группу декораторов (@abstract- abstract- method и @abstractproperty), использование которых демонстрируется ниже: from abc import ABCMeta, abstractmethod, abstractproperty class Foo: # В Python 3 используется синтаксис __metaclass__ = ABCMeta # class Foo(metaclass=ABCMeta) @abstractmethod def spam(self,a,b): pass @abstractproperty def name(self): pass В определении абстрактного класса должна быть объявлена ссылка на ме- такласс ABCMeta, как показано выше (обратите также внимание на разли- Абстрактные базовые классы 183 чия в синтаксисе между Python 2 и 3). Это совершенно необходимо, потому что реализация абстрактных классов опирается на метаклассы (подробнее о метаклассах рассказывается в следующем разделе). Внутри абстрактного класса определения методов и свойств, которые должны быть реализованы в подклассах, выполняются с помощью декораторов @abstractmethod и @ab- ab- stractproperty класса Foo. Абстрактный класс не может использоваться непосредственно для созда- ния экземпляров. Если попытаться создать экземпляр предыдущего клас- са Foo, будет возбуждено исключение: >>> f = Foo() Traceback (most recent call last): File “ TypeError: Can’t instantiate abstract class Foo with abstract methods spam (Перевод: Трассировочная информация (самый последний вызов – самый нижний): Файл “ TypeError: Невозможно создать экземпляр абстрактного класса Foo с абстрактным методом spam ) >>> Это ограничение переносится и на производные классы. Например, если представить, что имеется класс Bar, наследующий класс Foo, но не имею- щий реализации одного или более абстрактных методов, то попытка соз- дать экземпляр класса Bar завершится неудачей с аналогичной ошибкой. Благодаря этой дополнительной проверке абстрактные классы являют- ся удобным инструментом, когда необходимо гарантировать реализацию свойств и методов в подклассах. Абстрактный класс определяет, какие свойства и методы должны быть реализованы в подклассах, но он не предъявляет никаких требований к аргументам или возвращаемым значениям. То есть абстрактный класс не может потребовать, чтобы метод в подклассе принимал те же самые ар- гументы, что и абстрактный метод. То же относится и к свойствам – аб- страктный класс требует определить свойство, но не требует, чтобы в под- классе была реализована поддержка тех же самых операций над свойством (get, set и delete), что и в базовом классе. Абстрактный класс не может использоваться для создания экземпляров, но он может определять свойства и методы для использования в подклас- сах. Кроме того, из подкласса допускается вызывать методы, которые были объявлены абстрактными в базовом классе. Например, вызов Foo.spam(a,b) в подклассе считается допустимым. Абстрактные базовые классы позволяют регистрировать существующие классы как наследующие этот базовый класс. Делается это с помощью ме- тода register(), как показано ниже: class Grok(object): def spam(self,a,b): print(“Grok.spam”) ёё 184 Глава 7. Классы и объектно-ориентированное программирование Foo.register(Grok) # Зарегистрирует Grok, как наследующий # абстрактный базовый класс Foo Когда производится регистрация класса в некотором абстрактном базо- вом классе, операции проверки типа (такие как isinstance() и issubclass()) с привлечением абстрактного базового класса будут возвращать True для экземпляров зарегистрированных классов. В ходе регистрации класса в аб- страктном базовом классе не проверяется, действительно ли регистрируе- мый класс реализует абстрактные свойства и методы. Процедура регистра- ции оказывает влияние только на операции проверки типа и не производит дополнительных проверок на наличие ошибок в регистрируемом классе. В отличие от многих других объектно-ориентированных языков програм- мирования, встроенные типы в языке Python организованы в виде доста- Python организованы в виде доста- организованы в виде доста- точно простой иерархии. Например, если взглянуть на такие вcтроенные типы, как int или float, можно заметить, что они являются прямыми на- следниками класса object, родоначальника всех объектов, а не какого-то промежуточного базового класса, представляющего числа. Это усложняет разработку программ, которые проверяют и манипулируют объектами, опираясь на их принадлежность к некоторой категории, такой как числа. Механизм абстрактных классов позволяет решить эту проблему за счет включения существующих объектов в иерархии типов, определяемых пользователем. Более того, некоторые библиотечные модули организуют встроенные типы по категориям, в соответствии с различными особенно- стями, которыми они обладают. Модуль collections содержит абстрактные базовые классы, реализующие различные операции над последовательно- стями, множествами и словарями. Модуль numbers содержит абстрактные базовые классы, которые могут использоваться для организации иерархии числовых типов. Дополнительные подробности по этой теме приводят- ся в главе 14 «Математика» и в главе 15 «Структуры данных, алгоритмы и утилиты». Метаклассы Когда программа на языке Python объявляет класс, само определение это- Python объявляет класс, само определение это- объявляет класс, само определение это- го класса становится объектом. Например: class Foo(object): pass isinstance(Foo,object) # Вернет True Если задуматься над этим примером, можно понять, что что-то должно было создать объект Foo. Созданием объектов такого рода управляет спе- циальный объект, который называется метаклассом. Проще говоря, мета- класс – это объект, который знает, как создавать классы и управлять ими. В предыдущем примере метаклассом, под управлением которого создается объект Foo, является класс с именем type(). Если попытаться вывести тип объекта Foo, можно увидеть, что он имеет тип type: >>> type(Foo) Метаклассы 185 Когда с помощью инструкции class определяется новый класс, выполня- ется определенный набор действий. Во-первых, тело класса выполняется интерпретатором, как последовательность инструкций, с использованием отдельного частного словаря. Инструкции выполняются точно так же, как и в обычном программном коде, кроме того, что дополнительно производит- ся изменение имен частных членов класса (начинающихся с префикса __). В заключение имя класса, список базовых классов и словарь передаются конструктору метакласса, который создает соответствующий объект клас- са. Следующий пример демонстрирует, как это делается: class_name = “Foo” # Имя класса class_parents = (object,) # Базовые классы class_body = “”” # Тело класса def __init__(self,x): self.x = x def blah(self): print(“Hello World”) “”” class_dict = { } # Выполнить тело класса с использованием локального словаря class_dict exec(class_body,globals(),class_dict) ёё # Создать объект класса Foo Foo = type(class_name,class_parents,class_dict) Заключительный этап создания класса, когда вызывается метакласс type() , можно изменить. Повлиять на события, происходящие на заклю- чительном этапе определения класса, можно несколькими способами. На- пример, в классе можно явно указать его метакласс либо установив пере- менную класса __metaclass__ (в Python 2), либо добавив именованный аргу- Python 2), либо добавив именованный аргу- 2), либо добавив именованный аргу- мент metaclass в кортеж с именами базовых классов (в Python 3). class Foo: # В Python 3 используется синтаксис __metaclass__ = type # class Foo(metaclass=type) ... Если метакласс явно не указан, инструкция class проверит первый эле- мент в кортеже базовых классов (если таковой имеется). В этом случае ме- таклассом будет тип первого базового класса. То есть инструкция class Foo(object): pass создаст объект Foo того же типа, которому принадлежит класс object. Если базовые классы не указаны, инструкция class проверит наличие гло- бальной переменной с именем __metaclass__. Если такая переменная при- сутствует, она будет использоваться при создании классов. С помощью этой переменной можно управлять созданием классов, когда используется про- стая инструкция class. Например: __metaclass__ = type class Foo: pass 186 Глава 7. Классы и объектно-ориентированное программирование Наконец, если переменная __metaclass__ не будет найдена, интерпретатор будет использовать метакласс по умолчанию. В Python 2 таким метаклас- Python 2 таким метаклас- 2 таким метаклас- сом по умолчанию является types.ClassType, который известен, как класс старого стиля . Этот вид класса не рекомендуется к использованию, начи- ная с версии Python 2.2, и соответствует оригинальной реализации клас- Python 2.2, и соответствует оригинальной реализации клас- 2.2, и соответствует оригинальной реализации клас- сов в языке Python. Эти классы по-прежнему поддерживаются, но они не должны использоваться в новых программах и в этой книге рассматри- ваться не будут. В Python 3 метаклассом по умолчанию является type. В основном метаклассы используются во фреймворках, когда требуется бо- лее полный контроль над определениями пользовательских объектов. Ког- да определяется нестандартный метакласс, он обычно наследует класс type и переопределяет такие методы, как __init__() или __new__(). Ниже приво- дится пример метакласса, который требует, чтобы все методы снабжались строками документирования: class DocMeta(type): def __init__(self,name,bases,dict): for key, value in dict.items(): # Пропустить специальные и частные методы if key.startswith(“__”): continue # Пропустить любые невызываемые объекты if not hasattr(value,”__call__”): continue # Проверить наличие строки документирования if not getattr(value,”__doc__”): raise TypeError(“%s must have a docstring” % key) type.__init__(self,name,bases,dict) В этом метаклассе реализован метод __init__(), который проверяет содер- жимое словаря класса. Он отыскивает в словаре методы и проверяет, име- ют ли они строки документирования. Если в каком-либо методе строка до- кументирования отсутствует, возбуждается исключение TypeError. В про- тивном случае для инициализации класса вызывается реализация метода type.__init__() Чтобы воспользоваться этим метаклассом, класс должен явно выбрать его. Обычно для этой цели сначала определяется базовый класс, такой, как по- казано ниже: class Documented: # В Python 3 используется синтаксис __metaclass__ = DocMeta # class Documented(metaclass=DocMeta) А затем этот базовый класс используется, как родоначальник всех объек- тов, которые должны включать в себя описание. Например: class Foo(Documented): spam(self,a,b): “Метод spam делает кое-что” pass Этот пример иллюстрирует одно из основных применений метаклассов, со- стоящий в проверке и сборе информации об определениях классов. Мета- класс ничего не изменяет в создаваемом классе, он просто выполняет не- которые дополнительные проверки. Метаклассы 187 В более сложных случаях перед тем, как создать класс, метакласс может не только проверять, но и изменять содержимое его определения. Если предполагается вносить какие-либо изменения, необходимо переопреде- лить метод __new__(), который выполняется перед созданием класса. Этот прием часто объединяется с приемом обертывания атрибутов дескриптора- ми или свойствами, потому что это единственный способ получить имена, использованные в классе. В качестве примера ниже приводится модифици- рованная версия дескриптора TypedProperty, который был реализован в раз- деле «Дескрипторы»: class TypedProperty(object): def __init__(self,type,default=None): self.name = None self.type = type if default: self.default = default else: self.default = type() def __get__(self,instance,cls): return getattr(instance,self.name,self.default) def __set__(self,instance,value): if not isinstance(value,self.type): raise TypeError(“Значение должно быть типа %s” % self.type) setattr(instance,self.name,value) def __delete__(self,instance): raise AttributeError(“Невозможно удалить атрибут”) В данном примере атрибуту name дескриптора просто присваивается значе- ние None. Заполнение этого атрибута будет поручено метаклассу. Например: class TypedMeta(type): def __new__(cls,name,bases,dict): slots = [ ] for key,value in dict.items(): if isinstance(value,TypedProperty): value.name = “_” + key slots.append(value.name) dict[‘__slots__’] = slots return type.__new__(cls,name,bases,dict) ёё # Базовый класс для объектов, определяемых пользователем class Typed: # В Python 3 используется синтаксис __metaclass__ = TypedMeta # class Typed(metaclass=TypedMeta) В этом примере метакласс просматривает словарь класса с целью отыскать экземпляры класса TypedProperty. В случае обнаружения такого экземпля- ра он устанавливает значение атрибута name и добавляет его в список имен slots . После этого в словарь класса добавляется атрибут __slots__ и вызы- вается метод __new__() метакласса type, который создает объект класса. Ниже приводится пример использования нового метакласса: class Foo(Typed): name = TypedProperty(str) num = TypedProperty(int,42) Метаклассы способны коренным образом изменять поведение и семан- тику пользовательских классов. Однако не следует злоупотреблять этой 188 Глава 7. Классы и объектно-ориентированное программирование возможностью и изменять поведение классов так, чтобы оно существенно отличалось от поведения, описанного в стандартной документации. Поль- зователи будут обескуражены, если создаваемые ими классы не будут при- держиваться обычных правил программирования для классов. Декораторы классов В предыдущем примере было показано, как с помощью метаклассов можно управлять процессом создания классов. Однако иногда бывает достаточно выполнить некоторые действия уже после того, как класс будет определен, например добавить класс в реестр или в базу данных. Альтернативный подход к решению подобных задач заключается в использовании декора- тора класса. Декоратор класса – это функция, которая принимает и воз- вращает класс. Например: registry = { } def register(cls): registry[cls.__clsid__] = cls return cls В этом примере функция register отыскивает в классе атрибут __clsid__. Если этот атрибут определен, он используется для добавления класса в сло- варь, который служит для отображения идентификаторов классов в объек- ты классов. Эту функцию можно использовать как декоратор, поместив его непосредственно перед определением класса. Например: @register class Foo(object): __clsid__ = “123-456” def bar(self): pass Здесь к синтаксису с использованием декоратора мы прибегли, главным об- разом, ради удобства. Того же самого результата можно добиться другим способом: class Foo(object): __clsid__ = “123-456” def bar(self): pass register(Foo) # Зарегистрировать класс Можно до бесконечности придумывать всякие дьявольские ухищрения, которые можно было бы реализовать в функции-декораторе класса, одна- ко лучше все-таки избегать чрезмерных превращений, таких как создание обертки вокруг класса или переопределение его содержимого. |