python статьи

Python: сложные аспекты

Dmitry Vasiliev 23:20, 2012 2 6 12:12, 2009 8 3

Впервые было опубликовано в журнале "Системный администратор" #5 за 2009 год

Рассматриваем метаклассы, дескрипторы атрибутов и менеджеры контекста.

В этой статье мы рассмотрим некоторые достаточно сложные аспекты языка Python, а именно:

  • метаклассы, позволяющие создавать классы с необычным поведением;
  • дескрипторы атрибутов, предоставляющие наиболее гибкий контроль доступа к атрибутам объектов и классов;
  • менеджеры контекста, объекты, позволяющие управлять поведением ключевого слова with;

Метаклассы

В общем случае, как и следует из названия, метаклассы — это классы классов. Таким образом классы являются экземплярами метаклассов. Начиная с Python 2.2 стандартным метаклассом является type, который служит метаклассом для всех встроенных типов. Это можно увидеть на следующем примере:

>>> ().__class__
<type 'tuple'>
>>> ().__class__.__class__
<type 'type'>

Здесь классом для создания кортежа является tuple и соответственно классом для создания tuple является type. В этой статье мы рассматриваем только, так называемые «новые» классы, т.е. классы которые наследуются от встроенного класса object. На данный момент «старые» (или «классические») классы должны представлять только исторический интерес, хотя они еще используются в некоторых проектах.

В Python при выполнении выражения описывающего класс, интерпретатор сначала определяет соответствующий классу метакласс M и затем вызывает M(name, bases, dict) для создания класса. Это происходит после того как было обработано тело класса, где определены его методы и атрибуты. Аргументами при вызове метакласса являются:

  1. "name" - имя класса, строка получаемая из выражения описывающего класс;
  2. "bases" - кортеж базовых классов, получаемый в начале обработки выражения класса, или () если класс не определил базовых классов;
  3. "dict" - словарь с методами и атрибутами класса которые были определены в теле класса;

Затем результат вызова M присваивается переменной с именем класса. Описание вызова метакласса для создания класса можно проиллюстрировать следующим примером:

>>> T = type("test", (object,), {"name": "Test"})
>>> T
<class '__main__.test'>
>>> T.name
'Test'
>>> t = T()
>>> t
<__main__.test object at 0x2863550>
>>> t.name
'Test'

После того как мы рассмотрели как метакласс создает класс, остается понять, как выбирается метакласс. Для выбора метакласса используются следующие шаги:

  1. Если определен dict['__metaclass__'] (т.е. в теле класса был определен атрибут __metaclass__), то он используется;
  2. Иначе, если определен хотя бы один базовый класс, используются метакласс базового класса;
  3. Иначе, будет использоваться глобальная переменная __metaclass__, если она определена;
  4. В противном случае, будет использоваться метакласс для «классических» классов 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__ в теле класса, как уже было описано выше, это один из способов присвоения метакласса классу. Таким образом, мы видим последовательность вызова методов метакласса:

  1. __new__() вызывается для создания класса;
  2. __init__() для инициализации класса;
  3. __call__() вызывается при создании объектов класса;

Нужно так же отметить, что атрибуты и методы, определенные в метаклассе являются статическими, т.е. доступны только на уровне класса, но не на уровне объектов класса:

>>> class MetaTest(type):
...     def test(cls):
...         print "test()"
...
>>> class Test(object):
...     __metaclass__ = MetaTest
...
>>> Test.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()
'BA'

Таким образом при работе с классом и его подклассами можно везде заменить вызов встроенной функции 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
'text'
>>> msg.text
'text body'
>>> row = ResultRow(id=1, name='John')
>>> row.id
1
>>> row.name
'John'

Такой метакласс может быть полезен для создания классов объекты которых служат в основном как хранилище атрибутов. Например, классов описывающих передаваемые по сети пакеты данных, или строки результата запроса к базе данных к полям которых удобнее обращаться как к атрибутам.

Таким образом, метаклассы позволяют создавать классы с достаточно необычным поведением, но в тоже время вряд ли стоит их использовать в каждой программе.

Дескрипторы атрибутов

Дескрипторы атрибутов (далее просто дескрипторы) описывают протокол доступа к атрибутам объекта, или класса. В общем случае дескрипторы — это объекты, в которых определен один из методов: __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__(), который должен вернуть объект присваиваемый переменной, после чего выполняется блок кода. Если блок кода выкидывает исключение, то вызывается метод __exit__() с информацией об исключении. Если выполнение блока завершилось успешно, вся информация об исключении равна 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) предоставляет функции и декораторы, упрощающие создание и работу с менеджерами контекста. На данный момент модуль предоставляет три функции:

  • contextmanager(функция) — декоратор, упрощающий создание менеджеров контекста. Вместо создания класса, предоставляющего интерфейс менеджера контекста, можно использовать декоратор с функцией-генератором, например:

    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/.