Дмитрий Васильев
Python: сложные аспекты
Рассматриваем метаклассы, дескрипторы атрибутов и менеджеры контекста.
В этой статье мы рассмотрим некоторые достаточно сложные аспекты языка Python, а именно:
- метаклассы, позволяющие создавать классы с необычным поведением;
- дескрипторы атрибутов, предоставляющие наиболее гибкий контроль доступа к атрибутам объектов и классов;
- менеджеры контекста, объекты, позволяющие управлять поведением ключевого слова with.
Метаклассы
В общем случае, как и следует из названия, метаклассы – это классы классов. Таким образом, классы являются экземплярами метаклассов. Начиная с Python 2.2 стандартным метаклассом является type, который служит метаклассом для всех встроенных типов. Это можно увидеть на следующем примере:
>>> ().__class__
>>> ().__class__.__class__
Здесь классом для создания кортежа является tuple и соответственно классом для создания tuple является type. В этой статье мы рассматриваем только так называемые новые классы, то есть классы, которые наследуются от встроенного класса object. На данный момент «старые» (или «классические») классы должны представлять только исторический интерес, хотя они еще используются в некоторых проектах.
В Python при выполнении выражения, описывающего класс, интерпретатор сначала определяет соответствующий классу метакласс M и затем вызывает M(name, bases, dict) для создания класса. Это происходит после того как было обработано тело класса, где определены его методы и атрибуты. Аргументами при вызове метакласса являются:
- name – имя класса, строка, получаемая из выражения, описывающего класс;
- bases – кортеж базовых классов, получаемый в начале обработки выражения класса, или () если класс не определил базовых классов;
- dict – словарь с методами и атрибутами класса, которые были определены в теле класса;
Затем результат вызова M присваивается переменной с именем класса. Описание вызова метакласса для создания класса можно проиллюстрировать следующим примером:
>>> T = type("test", (object,), {"name": "Test"})
>>> T
>>> T.name
>>> t = T()
>>> t
<__main__.test object at 0x2863550> |
>>> t.name
После того как мы рассмотрели, как метакласс создает класс, остается понять, как выбирается метакласс.
Для выбора метакласса используются следующие шаги:
- Если определен dict['__metaclass__'] (то есть в теле класса был определен атрибут __metaclass__), то он используется.
- Иначе, если определен хотя бы один базовый класс, используется метакласс базового класса.
- Иначе будет использоваться глобальная переменная __metaclass__, если она определена.
- В противном случае будет использоваться метакласс для «классических» классов types.ClassType и соответственно будет создан «классический» класс.
Начиная с Python 3.0 метакласс можно указывать только как именованный параметр при определении класса, следующим образом:
>>> class Test(metaclass=type):
... pass
...
Основные ограничения, связанные с метаклассами языка Python:
- Нельзя наследоваться одновременно от «классического» и «нового» классов. В этом случае возможности «новых» классов, описанные в этой статье, работать не будут.
- Метакласс класса должен соответствовать метаклассу базового класса или быть его потомком.
Примеры метаклассов
После описания работы метаклассов обратимся к примерам собственных реализаций. Как уже было рассмотрено ранее, класс создается при вызове метакласса следующим образом: M(name, bases, dict). Более детально при создании классов (можно провести аналогию с созданием объектов класса) вызываются методы метакласса __new__() и затем __init__(), как в следующей последовательности строк:
cls = M.__new__(M, name, bases, dict)
assert cls.__class__ is M
M.__init__(cls, name, bases, dict)
Напишем наш первый метакласс, чтобы рассмотреть последовательность вызова методов при создании класса и объекта:
class MetaTest(type):
def __new__(cls, name, bases, dict):
klass = super(MetaTest, cls).__new__(cls, name, bases, dict)
print "__new__(%r, %r, %r) -> %r" % (name, bases, dict, klass)
return klass
def __init__(cls, name, bases, dict):
super(MetaTest, cls).__init__(name, bases, dict)
print "__init__(%r, %r, %r)" % (name, bases, dict)
def __call__(cls, *args, **kwargs):
obj = super(MetaTest, cls).__call__(*args, **kwargs)
print "__call__(%r, %r) -> %r" % (args, kwargs, obj)
return obj
Здесь мы просто выводим информацию о вызове методов __new__(), __init__() и __call__(). Вот как это работает:
>>> from meta import MetaTest
>>> class Test(object):
... __metaclass__ = MetaTest
...
__new__('Test', (<type 'object'>,), {'__module__': '__main__', '__metaclass__': <class 'meta.MetaTest'>}) -> <class '__main__.Test'>
__init__('Test', (<type 'object'>,), {'__module__': '__main__', '__metaclass__': <class 'meta.MetaTest'>})
|
>>> test = Test()
__call__((), {}) -> <__main__.Test object at 0x7f62e95ca650> |
Обратите внимание на атрибут __metaclass__ в теле класса, как уже было описано выше, это один из способов присвоения метакласса классу.
Таким образом, мы видим последовательность вызова методов метакласса:
- __new__() – вызывается для создания класса;
- __init__() – для инициализации класса;
- __call__() – вызывается при создании объектов класса.
Нужно также отметить, что атрибуты и методы, определенные в метаклассе, являются статическими, то есть доступны только на уровне класса, но не на уровне объектов класса:
>>> class MetaTest(type):
... def test(cls):
... print "test()"
...
>>> class Test(object):
... __metaclass__ = MetaTest
...
>>> Test.test()
>>> Test().test()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'test'
|
Рассмотрим примеры более полезных метаклассов. Метакласс AutoSuper добавляет приватный атрибут __super для доступа к атрибутам и методам базовых классов:
class AutoSuper(type):
def __init__(cls, name, bases, dict):
super(AutoSuper, cls).__init__(name, bases, dict)
setattr(cls, "_%s__super" % name, super(cls))
Теперь он может быть использован так:
>>> from super import AutoSuper
>>> class A(object):
... __metaclass__ = AutoSuper
... def method(self):
... return "A"
...
>>> class B(A):
... def method(self):
... return "B" + self.__super.method()
...
>>> B().method()
Таким образом, при работе с классом и его подклассами можно везде заменить вызов встроенной функции super на обращение к приватному атрибуту __super. Это позволяет контролировать доступ к базовым классам на уровне класса или даже объекта. Плюс к этому уменьшается вероятность ошибок, связанных с опечатками, и в случае изменения имени класса нет необходимости изменять имя в нескольких местах.
Следующий пример представляет собой метакласс, устанавливающий атрибуты для объектов, создаваемых классом без необходимости определения конструктора класса:
class AttrInit(type):
def __call__(cls, **kwargs):
obj = super(AttrInit, cls).__call__()
for name, value in kwargs.items():
setattr(obj, name, value)
return obj
Этот метакласс может быть использован так:
>>> from attr import AttrInit
>>> class Message(object):
... __metaclass__ = AttrInit
...
>>> class ResultRow(object):
... __metaclass__ = AttrInit
...
>>> msg = Message(type='text', text='text body')
>>> msg.type
>>> msg.text
>>> row = ResultRow(id=1, name='John')
>>> row.id
>>> row.name
Такой метакласс может быть полезен для создания классов, объекты которых служат в основном как хранилище атрибутов. Например, классов, описывающих передаваемые по сети пакеты данных, или строки результата запроса к базе данных, к полям которых удобнее обращаться как к атрибутам.
Таким образом, метаклассы позволяют создавать классы с достаточно необычным поведением, но в то же время вряд ли стоит их использовать в каждой программе.
Дескрипторы атрибутов
Дескрипторы атрибутов (далее просто дескрипторы) описывают протокол доступа к атрибутам объекта или класса. В общем случае дескрипторы – это объекты, в которых определен один из методов: __get__(), __set__() или __delete__(). Среди уже определенных в Python дескрипторов можно назвать следующие: property, classmethod и staticmethod. Рассмотрим интерфейс дескрипторов на примере:
class TestDescriptor(object):
def __get__(self, obj, type=None):
print "__get__(%r, %r)" % (obj, type)
return "value"
def __set__(self, obj, value):
print "__set__(%r, %r)" % (obj, value)
def __delete__(self, obj):
print "__delete__(%r)" % obj
При доступе к атрибуту методы этого дескриптора вызываются следующим образом:
>>> from desc import TestDescriptor
>>> class Test(object):
... attribute = TestDescriptor()
...
>>> Test.attribute
__get__(None, <class '__main__.Test'>)
'value'
|
>>> t = Test()
>>> t.attribute
__get__(<__main__.Test object at 0x7f757d88d510>,
<class '__main__.Test'>)
'value'
|
>>> t.attribute = "new value"
__set__(<__main__.Test object at 0x7f757d88d510>, 'new value') |
>>> del t.attribute
__delete__(<__main__.Test object at 0x7f757d88d510>) |
Здесь мы видим, что при доступе к атрибуту attribute, являющемуся дескриптором, на самом деле вызываются методы дескриптора. Надо также заметить, что дескрипторы вызываются из метода __getattribute__() (который в свою очередь имеет смысл только для «новых» классов), определенного в классе object, и его переопределение может отменить автоматическое обращение к дескрипторам при доступе к атрибутам. Также следует знать, что если дескриптор определяет только метод __get__(), то атрибут, за которым стоит такой дескриптор, может быть переопределен присваиванием другого значения атрибута объекту. Если же дополнительно определен метод __set__(), то атрибут объекта не может быть переопределен таким образом.
Примеры дескрипторов
Для примера реализуем аналоги встроенных дескрипторов property, classmethod и staticmethod в Python.
Дескриптор, имеющий поведение property, может быть представлен следующим классом:
class Property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, type=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
Здесь операции запроса значения атрибута, установки атрибута и его удаления делегируются функциям, переданным в конструктор.
Поведение classmethod можно эмулировать следующим образом:
class ClassMethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args, **kwargs):
return self.f(klass, *args, **kwargs)
return newfunc
Здесь первый атрибут при вызове метода заменяется классом объекта.
И наконец staticmethod может быть представлен так:
class StaticMethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, type=None):
return self.f
Менеджеры контекста
Менеджеры контекста – это механизм, стоящий за ключевым словом with. Ключевое слово with появилось еще в Python 2.5, но к нему можно было получить доступ только через __future__ импорт: from __future__ import with_statement. Начиная с Python 2.6 ключевое слово with может быть полностью доступно без импортирования из __future__.
Ключевое слово with определяет блоки кода, которые прежде использовали try/finally. Для уверенности в выполнении кода его заключали в блок finally. With имеет следующую форму:
with выражение [as переменная]:
блок with
Здесь «выражение» должно вернуть объект, предоставляющий протокол менеджера контекста. Для некоторых встроенных объектов уже определены менеджеры контекста. Например, такой менеджер определен для файлов, чтобы быть уверенным, что файл будет закрыт при выходе из блока:
with open('file.txt', 'rb') as f:
for line in f:
print line
В простейшем случае такая конструкция эквивалентна следующей:
f = open('file.txt', 'rb')
try:
for line in f:
print line
finally:
f.close()
Протокол менеджера контекста содержит всего два метода: __enter__() и __exit__(). В начале выполнения блока кода вызывается метод __enter__(), который должен вернуть объект, присваиваемый переменной, после чего выполняется блок кода. Если блок кода выкидывает исключение, то вызывается метод __enter__() с информацией об исключении. Если выполнение блока завершилось успешно, вся информация об исключении равна None. Пример работы:
class TestContext(object):
def __init__(self, ignore_error=False):
self.ignore_error = ignore_error
def __enter__(self):
print "__enter__()"
return self
def execute(self, error=False):
print "execute()"
if error:
raise Exception("error")
def __exit__(self, exc_type, exc_val, exc_tb):
print "__exit__(%r, %r, %r)" % (exc_type, exc_val, exc_tb)
return self.ignore_error
Кроме методов, предоставляющих протокол менеджера контекста, здесь также определен вспомогательный метод execute(), который будет представлять код внутри блока:
>>> from context import TestContext
>>> with TestContext() as context:
... context.execute()
...
__enter__()
execute()
__exit__(None, None, None)
|
>>> with TestContext() as context:
... context.execute(error=True)
...
__enter__()
execute()
__exit__(<type 'exceptions.Exception'>, Exception('error',), <traceback object at 0x7f6da88bffc8>)
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "context.py", line 10, in execute
raise Exception("error")
Exception: error
|
В случае, если метод __exit__() возвращает «ложь», исключение будет выкинуто за пределы блока. При этом метод __exit__() никогда не должен сам выкидывать полученное исключение, а управлять этим только через возвращаемое значение:
>>> with TestContext(ignore_error=True) as context:
... context.execute(error=True)
...
__enter__()
execute()
__exit__(<type 'exceptions.Exception'>, Exception('error',), <traceback object at 0x7fa35497a200>)
|
Модуль contextlib
Новый модуль contextlib (появившийся в Python 2.5) предоставляет функции и декораторы, упрощающие создание и работу с менеджерами контекста. На данный момент модуль предоставляет три функции:
Сontextmanager(функция) – декоратор, упрощающий создание менеджеров контекста. Вместо создания класса, предоставляющего интерфейс менеджера контекста, можно использовать декоратор с функцией-генератором, например:
from contextlib import contextmanager
@contextmanager
def test():
print "__enter__()"
try:
yield "execute()"
finally:
print "__exit__()"
Теперь мы можем использовать test() как менеджер контекста. Результат yield будет присвоен переменной:
>>> from context import test
>>> with test() as body:
... print body
...
__enter__()
execute()
__exit__()
|
Nested(менеджер1[, менеджер2[,...]]) – функция, комбинирующая несколько менеджеров контекста в один. Следующий код:
from contextlib import nested
with nested(A(), B(), C()) as (X, Y, Z):
body()
будет эквивалентен коду:
m1, m2, m3 = A(), B(), C()
with m1 as X:
with m2 as Y:
with m3 as Z:
body()
Closing(объект) – функция, возвращающая менеджер контекста, который закрывает объект по завершении блока. Например:
from contextlib import closing
from urllib import urlopen
with closing(urlopen('http://www.python.org')) as page:
for line in page:
print line
В этом примере в конце блока будет вызван метод page.close().
Заключение
В этой статье были рассмотрены достаточно сложные аспекты использования Python, которые вы скорее всего не будете использовать в каждой программе. Но в то же время описанный инструментарий может значительно упростить и сделать более гибким сложный код, что позволит взглянуть по-новому на все разрабатываемое приложение в целом. Плюс знание этих инструментов и описанные особенности внутренней работы интерпретатора, должны поднять на новую ступень ваш уровень как разработчика ПО.
Подробнее про особенности «новых» классов можно прочитать по ссылке: http://www.python.org/doc/newstyle.
Приложение
«Новые» классы
Новая система типов и классов (так называемые новые классы) была добавлена в Python 2.2 для унификации классов и типов. Основная причина их появления – это предоставление унифицированной объектной модели с полноценной моделью метаклассов. «Новые» классы также предоставляют следующие возможности:
- Наследование от встроенных типов, например списков (list) и даже целых (int), которые должны работать везде, где требуется оригинальный тип.
- Создание статических методов и методов класса.
- Вызов методов при доступе к атрибутам. Эта функциональность реализуется с помощью дескрипторов атрибутов.
В Python 2 простейший способ создать «новый» класс – это наследовать его от object:
class Test(object):
pass
Начиная с Python 3.0 «старые» классы были удалены и по умолчанию используются «новые» классы (которые уже нет необходимости называть «новыми»).