Python: сложные аспекты::Журнал СА 5.2009
www.samag.ru
     
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Журнал "Системный администратор"
Журнал «БИТ»
Наука и технологии
Подписка
Где купить
Авторам
Рекламодателям
Архив номеров
Контакты
   

  Опросы
  Статьи

Электронный документооборот  

5 способов повысить безопасность электронной подписи

Область применения технологий электронной подписи с каждым годом расширяется. Все больше задач

 Читать далее...

Рынок труда  

Системные администраторы по-прежнему востребованы и незаменимы

Системные администраторы, практически, есть везде. Порой их не видно и не слышно,

 Читать далее...

Учебные центры  

Карьерные мечты нужно воплощать! А мы поможем

Школа Bell Integrator открывает свои двери для всех, кто хочет освоить перспективную

 Читать далее...

Гость номера  

Дмитрий Галов: «Нельзя сказать, что люди становятся доверчивее, скорее эволюционирует ландшафт киберугроз»

Использование мобильных устройств растет. А вместе с ними быстро растет количество мобильных

 Читать далее...

Прошу слова  

Твердая рука в бархатной перчатке: принципы soft skills

Лауреат Нобелевской премии, специалист по рынку труда, профессор Лондонской школы экономики Кристофер

 Читать далее...

1001 и 1 книга  
19.03.2018г.
Просмотров: 9951
Комментарии: 0
Потоковая обработка данных

 Читать далее...

19.03.2018г.
Просмотров: 8162
Комментарии: 0
Релевантный поиск с использованием Elasticsearch и Solr

 Читать далее...

19.03.2018г.
Просмотров: 8263
Комментарии: 0
Конкурентное программирование на SCALA

 Читать далее...

19.03.2018г.
Просмотров: 5230
Комментарии: 0
Машинное обучение с использованием библиотеки Н2О

 Читать далее...

12.03.2018г.
Просмотров: 5918
Комментарии: 0
Особенности киберпреступлений в России: инструменты нападения и защита информации

 Читать далее...

Друзья сайта  

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

Архив номеров / 2009 / Выпуск №5 (78) / Python: сложные аспекты

Рубрика: Программирование /  Программирование

Дмитрий Васильев

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

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

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

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

Метаклассы

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

>>> ().__class__

<type 'tuple'>

>>> ().__class__.__class__

<type 'type'>

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

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

  •  name – имя класса, строка, получаемая из выражения, описывающего класс;
  •  bases – кортеж базовых классов, получаемый в начале обработки выражения класса, или () если класс не определил базовых классов;
  •  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'

После того как мы рассмотрели, как метакласс создает класс, остается понять, как выбирается метакласс.

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

  •  Если определен 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().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__(), который должен вернуть объект, присваиваемый переменной, после чего выполняется блок кода. Если блок кода выкидывает исключение, то вызывается метод __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 «старые» классы были удалены и по умолчанию используются «новые» классы (которые уже нет необходимости называть «новыми»).


Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru