Главная страница
Навигация по странице:

  • Типы и проверка принадлежности к классу

  • Абстрактные базовые классы

  • справочник по Python. мм isbn 9785932861578 9 785932 861578


    Скачать 4.21 Mb.
    Названиемм isbn 9785932861578 9 785932 861578
    Анкорсправочник по Python
    Дата08.05.2022
    Размер4.21 Mb.
    Формат файлаpdf
    Имя файлаBizli_Python-Podrobnyy-spravochnik.440222.pdf
    ТипСправочник
    #518195
    страница15 из 82
    1   ...   11   12   13   14   15   16   17   18   ...   82
    Управление памятью объектов
    Когда объявляется класс, в результате получается класс, который служит фабрикой по производству экземпляров новых экземпляров. Например:
    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 “”, line 1, in
    TypeError: unsupported operand type(s) for +: ‘int’ and ‘Complex’
    (Перевод:
    Трассировочная информация (самый последний вызов – самый нижний):
    Файл “”, строка 1, в
    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 “”, line 1, in
    TypeError: Can’t instantiate abstract class Foo with abstract methods spam
    (Перевод:
    Трассировочная информация (самый последний вызов – самый нижний):
    Файл “”, строка 1, в
    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) # Зарегистрировать класс
    Можно до бесконечности придумывать всякие дьявольские ухищрения, которые можно было бы реализовать в функции-декораторе класса, одна- ко лучше все-таки избегать чрезмерных превращений, таких как создание обертки вокруг класса или переопределение его содержимого.

    1   ...   11   12   13   14   15   16   17   18   ...   82


    написать администратору сайта