IBM®
Перейти к тексту
    в России и странах СНГ [изменить]    Условия использования
 
 
   
    Главная страница    Продукты    Услуги и решения    Поддержка и загрузка    Мой профиль    
Перейти к тексту

developerWorks Россия  >  Linux | Open source  >

Знакомство с Python 3: Часть 2. Более сложные темы

Метаклассы, декораторы и другие загадочные вещи

developerWorks
Опции документа
PDF format - Fits A4 and Letter

PDF - Fits A4 and Letter
52KB (15 страница)

Загрузить Adobe® Reader®

Опции документа, требующие включения JavaScript, не отображаются

Обсудить


Выскажите мнение об этой странице

Помогите нам улучшить содержание


Уровень сложности: средний

Сезар Отеро, независимый консультант, IBM

28.04.2009

Python 3 - это последняя версия мощного языка программирования общего назначения, автором которого является Гвидо ван Россум. Эта версия не имеет обратной совместимости с версиями 2.x, но зато в ней исправлены несколько синтаксических проблем предыдущих версий. Это вторая и последняя статья серии основана на предыдущей статье; в ней рассказывается об изменениях и новых возможностях в таких продвинутых аспектах Python, как абстрактные базовые классы, метаклассы и декораторы.

В предыдущей статье о Python 3—также известном как Python 3000 или Py3K— обсуждались некоторые простые изменения языка, нарушающие обратную совместимость, такие как новая функция print(), тип данных bytes и изменения в типе string. Здесь, во второй части, изучаются более сложные темы, большинство из которых также нарушает обратную совместимость с версиями 2.х: абстрактные базовые классы (abstract base classes или ABC), метаклассы, аннотации функций и декораторы, поддержка целочисленных констант, иерархия числовых типов и изменения в возбуждении и перехвате исключительных ситуаций.

Декораторы классов

В предыдущих версиях Python любые трансформации метода можно было делать только после определения этого метода. В длинных методах это отодвигало друг от друга такие важные компоненты, как определение метода и определение внешнего интерфейса (см. предложение о расширении Python (Python Enhancement Proposal, PEP) 318 в разделе Ресурсы). Следующий фрагмент кода иллюстрирует это требование:


Листинг 1. Трансформация методов в предыдущих версиях Python
        
def myMethod(self):
    # что-то делаем

myMethod = transformMethod(myMethod)

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

Декораторы - это методы, которые изменяют другие методы и возвращают либо метод, либо какой-нибудь другой вызываемый объект. Они обозначаются символом @ перед именем декоратора — как аннотации в языке Java™. В листинге 2 показан пример работы с декораторами.


Листинг 2. Декоратор метода
        
@transformMethod
def myMethod(self):
    # что-то делаем

Декораторы – это чистый «синтаксический сахар», т.е., согласно Википедии, "добавление к синтаксису языка программирования, которое не придает новой функциональности, но делает его приятней в использовании". Типичное применение декораторов — это аннотации статических методов. Листинги 1 и 2 делают одно и то же, но листинг 2 легче читать.

Декораторы определяются точно так же, как другие методы:

def mod(method):
    method.__name__ = "John"
    return method

@mod
def modMe():
    pass

print(modMe.__name__)

Больше того: в Python 3 имеются декораторы не только для методов, но и для классов. Поэтому теперь вместо такого кода:

class myClass:
    pass

myClass = doSomethingOrNotWithClass(myClass)

можно писать так:

@doSomethingOrNotWithClass
class myClass:
    pass



В начало


Метаклассы

Метаклассы - это классы, экземплярами которых являются другие классы. В Python 3 был сохранен встроенный тип данных metaclass, использовавшийся для создания других метаклассов или для динамического создания классов во время исполнения. В Python 3 по прежнему можно использовать синтаксис:

>>>aClass = type('className', 
   (object,), 
   {'magicMethod': lambda cls : print("blah blah")})

в котором в метод type() в качестве аргументов передаются строка с именем класса, кортеж наследуемых классов (который может быть пустым) и словарь, содержащий методы, добавляемые к этому классу (который также может быть пустым). Конечно, также вы можете наследовать от какого-либо типа и создавать свои собственные метаклассы:

class meta(type):
    def __new__(cls, className, baseClasses, dictOfMethods):
        return type.__new__(cls, className, baseClasses, dictOfMethods)

Только именованные аргументы

В Python 3 был изменен механизм распределения параметров функции по "слотам параметров". Вы можете использовать звездочку (*) в списке параметров, чтобы показать, что функция не принимает переменное количество аргументов. В таком случае именованные параметры следуют за символом *— например, def meth(*, arg1): pass. За более подробной информацией обращайтесь к документации Python или смотрите документ PEP 3102 (см. ссылку в разделе Ресурсы).

Замечание: Если два приведенных выше примера ничего вам не объяснили, я очень рекомендую почитать серию статей Дэвида Мерца и Мишеля Симионато о метаклассах. См. ссылку в разделе Ресурсы.

Обратите внимание, что теперь в определении класса после именованных аргументов может находиться список базовых классов — например, class Foo(*bases, **kwds): pass. Метакласс можно передавать в определение класса с помощью именованного параметра metaclass. Например:

>>>class aClass(baseClass1, baseClass2, metaclass = aMetaClass): pass

В старом синтаксисе метакласс приходилось передавать через встроенный атрибут __metaclass__:

class Test(object):
    __metaclass__ = type

Также был добавлен новый атрибут —__prepare__. Этот атрибут используется для создания словаря пространства имен нового класса. Он вызывается, как показано в листинге 3, перед созданием самого класса.


Листинг 3. Простой метакласс, использующий атрибут __prepare__
        
def meth():
    print("Calling method")

class MyMeta(type):
    @classmethod
    def __prepare__(cls, name, baseClasses):
        return {'meth':meth}

    def __new__(cls, name, baseClasses, classdict):
        return type.__new__(cls, name, baseClasses, classdict)

class Test(metaclass = MyMeta):
    def __init__(self):
        pass

    attr = 'an attribute'

t = Test()
print(t.attr)

В листинге 4 показан более интересный пример, скопированный из PEP 3115, в котором создается метакласс со списком имен его методов, причем с сохранением того порядка методов, в котором они были объявлены.


Листинг 4. Метакласс, сохраняющий порядок следования членов класса
        
# The custom dictionary
class member_table(dict):
    def __init__(self):
        self.member_names = []

    def __setitem__(self, key, value):
        # if the key is not already defined, add to the
        # list of keys.
        if key not in self:
            self.member_names.append(key)

        # Call superclass
        dict.__setitem__(self, key, value)

# The metaclass
class OrderedClass(type):
    # The prepare function
    @classmethod
    def __prepare__(metacls, name, bases): # No keywords in this case
        return member_table()

    # The metaclass invocation
    def __new__(cls, name, bases, classdict):
        # Note that we replace the classdict with a regular
        # dict before passing it to the superclass, so that we
        # don't continue to record member names after the class
        # has been created.
        result = type.__new__(cls, name, bases, dict(classdict))
        result.member_names = classdict.member_names
        return result

Изменениям, сделанным в метаклассах, есть несколько причин. Методы объектов хранятся в словаре, а, значит, неупорядоченно. Однако бывают случаи, в которых полезно сохранять порядок объявления членов класса (например, при создании C-структур). Это реализуется с помощью метаклассов, которые привлекаются к созданию объектов на ранней стадии, когда информация о порядке объявления членов еще доступна. Этот новый механизм позволит в будущем реализовать и другие интересные возможности, например, вставку символов в тело пространства имен класса на этапе его создания и опережающие ссылки. В PEP 3115 также говорится о том, что для изменения синтаксиса есть и эстетические причины, но это спорный вопрос, который сложно решить объективно. Смотрите ссылку на РЕР3115 в разделе Ресурсы.



В начало


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

Как я упоминал в предыдущей статье этой серии, абстрактные базовые классы (abstract base classes, ABC) - это классы, экземпляры которых нельзя создать. Программистам, знающим Java или C++ должна быть знакома эта концепция. В Python 3 добавлена новая инфраструктура — abc — которая облегчает работу с абстрактными базовыми классами.

Модуль abc содержит метакласс (ABCMeta) и декораторы (@abstractmethod и @abstractproperty). Если класс имеет декоратор @abstractmethod или @abstractproperty, значит он является абстрактным базовым классом и его необходимо переопределить в классе-наследнике. Например, так написать можно:

>>>from abc import *
>>>class C(metaclass = ABCMeta): pass
>>>c = C()

а вот так уже нет:

>>>from abc import *
>>>class C(metaclass = ABCMeta): 
...    @abstractmethod
...    def absMethod(self):
...        pass
>>>c = C() 
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class C with abstract methods absMethod

Вместо этого надо делать так:

>>>class B(C):
...    def absMethod(self):
...        print("Now a concrete method")
>>>b = B()
>>>b.absMethod()
Now a concrete method

Класс ABCMeta переопределяет атрибуты класса __instancecheck__ и __subclasscheck__, предоставляя таким образом возможность для перегрузки встроенных функций isinstance() и issubclass(). Чтобы добавить виртуальный подкласс к абстрактному базовому классу, используйте метод register() класса ABCMeta. Вот простой пример:

>>>class TestABC(metaclass=ABCMeta): pass
>>>TestABC.register(list)
>>>TestABC.__instancecheck__([])
True

Этот код равнозначен использованию isinstance(list, TestABC). Вы могли заметить, что в Python 3 используются метод __instancecheck__ вместо __issubclass__ и метод __subclasscheck__ вместо __issubclass__, что кажется более естественным. Причиной этому стало то, что изменение порядка следования аргументов с isinstance(subclass, superclass) скажем на superclass.__isinstance__(subclass) могло вызвать путаницу. Поэтому был выбран синтаксис superclass.__instancecheck__(subclass).

Для практики можно взять какой-нибудь абстрактный базовый класс из модуля collections и проверить, предоставляет ли он некоторый интерфейс:

>>>from collections import Iterable
>>>issubclass(list, Iterable)
True

В таблице 1 показаны абстрактные базовые классы инфраструктуры коллекций.


Таблица 1. Абстрактные базовые классы инфраструктуры коллекций.
ABCНаследует
Container
Hashable
Iterable
Iterator Iterable
Sized
Callable
Sequence Sized, Iterable, Container
MutableSequence Sequence
Set Sized, Iterable, Container
MutableSet Set
Mapping Sized, Iterable, Container
MutableMapping Mapping
MappingView Sized
KeysView MappingView, Set
ItemsView MappingView, Set
ValuesView MappingView
Коллекции

Инфраструктура коллекций состоит из типов-контейнеров, двусторонних очередей и словаря по умолчанию (известного как defaultdict). Двусторонняя очередь поддерживает добавление и извлечение элементов как с начала, так и с конца очереди. Контейнер defaultdict - подкласс встроенного словаря, который (согласно документации Python 3) переопределяет один метод и добавляет одну переопределяемую переменную. В остальном, он работает как словарь. Инфраструктура коллекций также предоставляет фабрику объектов, называемую namedtuple().

Иерархии абстрактных базовых классов

В python 3 появилась поддержка иерархии абстрактных базовых классов, представляющих числа. Эти абстрактные базовые классы находятся в модуле numbers и включают в себя классы Number, Complex, Real, Rational и Integral. На рисунке 1 показана иерархия чисел. Вы можете использовать эти метаклассы для создания своего собственного числового типа или еще одного числового абстрактного класса.


Рисунок 1. Иерархия числовых типов
The numeric hierarchy
Числовая башня

Идея числовой иерархии в Python была навеяна числовой башней языка Scheme.

Новый модуль fractions реализует числовой абстрактный базовый класс Rational. Этот модуль поддерживает арифметику рациональных чисел. Запустив dir(fractions.Fraction), можно увидеть, что у этого класса есть такие атрибуты, как imag, real и __complex__. Это связано с тем, что в соответствии с иерархией числовых типов Rationals наследуется от Reals, который наследуется от Complex.



В начало


Возбуждение и перехват исключительных ситуаций

В Python 3 был изменен вид блока except для борьбы с синтаксической неоднозначностью. Например если в Python 2.5 приведенную ниже конструкцию try . . . except:

>>>try:
...    x = float('not a number')
... except (ValueError, NameError):
...    print "can't convert type"

по ошибке записать так:

>>> try:
...    x = float('not a number')
... except ValueError, NameError:
...    print "can't convert type"

, то исключение ValueError никогда не будет перехвачено, потому что интерпретатор будет перехватывать исключение ValueError и привязывать объект исключения к имени NameError. Это иллюстрируется в следующем примере:

>>> try:
...    x = float('blah')
... except ValueError, NameError:
...    print "NameError is ", NameError
...
NameError is invalid literal for float(): not a number

Поэтому, чтобы сделать синтаксис менее двусмысленным, запятую (,), применявшуюся при привязывании объекта исключения к имени, заменили ключевым словом as. Если вы хотите перехватывать несколько исключений, необходимо использовать скобки (()). В листинге 5 показаны два примера правильной работы с исключениями в Python 3.


Листинг 5. Обработка исключений в Python 3
        
# привязываем объект исключения ValueError к локальному имени ex
try:
    x = float('blah')
except ValueError as ex:
    print("value exception occurred ", ex)
 
# перехватываем одновременно две исключительные ситуации
try:
    x = float('blah')
except (ValueError, NameError):
    print("caught both types of exceptions")

Другое изменение в обработке исключений - это появление цепочек исключений, - либо явных, либо неявных. В листинге 6 показан пример неявной цепочки исключений.


Листинг 6. Неявная цепочка исключений в Python 3
        
def divide(a, b):
    try:
        print(a/b)
    except Exception as exc:
        def log(exc):
        fid = open('logfile.txt') # забываем 'w'
        print(exc, file=fid)
        fid.close()

        log(exc)

divide(1,0)

В методе divide()выполняется деление на ноль, в результате чего возбуждается исключение ZeroDivisionError. Но в блоке обработки исключения метод log() инструкцией print(exc, file=fid) пытается писать в файл, который не был открыт для записи. Python 3 возбуждает исключения так, как показано в листинге 7.


Листинг 7. Пример трассировки вызовов для неявной цепочки исключений
        
Traceback (most recent call last):
  File "chainExceptionExample1.py", line 3, in divide
  print(a/b)
ZeroDivisionError: int division or modulo by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "chainExceptionExample1.py", line 12, in <module>
    divide(1,0)
  File "chainExceptionExample1.py", line 10, in divide
    log(exc)
  File "chainExceptionExample1.py", line 7, in log
    print(exc, file=fid)
  File "/opt/python3.0/lib/python3.0/io.py", line 1492, in write
    self.buffer.write(b)
  File "/opt/python3.0/lib/python3.0/io.py", line 696, in write
    self._unsupported("write")
  File "/opt/python3.0/lib/python3.0/io.py", line 322, in _unsupported
    (self.__class__.__name__, name))
io.UnsupportedOperation: BufferedReader.write() not supported

Заметьте, что оба исключения были обработаны. В предыдущих версиях Python исключение ZeroDivisionError было бы потеряно. Как это достигается? Теперь все объекты исключения имеют атрибут __context__. В нашем случае атрибут __context__ возбужденного исключения IOError сохранил в себе информацию о предыдущем исключении ZeroDivisionError.

Кроме атрибута __context__, исключения имеют атрибут __cause__, который всегда инициализируется значением None. Назначение этого атрибута в том, чтобы предоставить способ для записи причины возбуждения исключительной ситуации. Задать значение атрибута __cause__ можно следующим образом:

>>> raise EXCEPTION from CAUSE

Это равносильно следующему коду:

>>>exception = EXCEPTION
>>>exception.__cause__ = CAUSE
>>>raise exception

но выглядит более элегантно. В листинге 8 показан пример явной цепочки исключений.


Листинг 8. Явная цепочка исключений в Python 3
        
class CustomError(Exception): 
    pass

try:
    fid = open("aFile.txt") # опять забываем 'w'
    print("blah blah blah", file=fid)
except IOError as exc:
    raise CustomError('something went wrong') from exc

Также как и в предыдущем примере, функция print() возбуждает исключение, потому что файл не был открыт для записи. В листинге 9 показана трассировка вызовов.


Листинг 9. Трассировка вызовов исключения
        
Traceback (most recent call last):
  File "chainExceptionExample2.py", line 5, in <module>
    fid = open("aFile.txt")
  File "/opt/python3.0/lib/python3.0/io.py", line 278, in __new__
    return open(*args, **kwargs)
  File "/opt/python3.0/lib/python3.0/io.py", line 222, in open
    closefd)
 File "/opt/python3.0/lib/python3.0/io.py", line 615, in __init__
    _fileio._FileIO.__init__(self, name, mode, closefd)
IOError: [Errno 2] No such file or directory: 'aFile.txt'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "chainExceptionExample2.py", line 8, in <modulei>
  raise CustomError('something went wrong') from exc
__main__.CustomError: something went wrong

Обратите внимание, что первая трассировка заканчивается строкой "The above exception was the direct cause of the following exception", за которой следует еще одна трассировка - вызовов, приведших к возбуждению исключения CustomError, "something went wrong."

И наконец, еще один атрибут, добавленный к объекту исключения - это атрибут __traceback__. Если перехваченное исключение не имеет атрибута __traceback__, то задается новая трассировка. Вот простой пример:

from traceback import format_tb

try:
    1/0
except Exception as exc:
    print(format_tb(exc.__traceback__)[0])

Обратите внимание, что format_tb возвращает список, состоящий только из одного исключения.

Инструкция with

Начиная с Python 2.6, инструкция with является ключевым словом, которое больше не нужно импортировать из __future__.



В начало


Целочисленные константы

Python поддерживает целочисленные строковые константы в различных системах счисления — восьмеричной, десятичной (конечно же!) и шестнадцатеричной, — а теперь к ним добавилась и двоичная. Изменилось представление констант в восьмеричной системе: теперь они начинаются префиксом 0o или 0O (т.е. ноль и за ним маленькая или большая буква "o") и затем значение константы. Например восьмеричное 13 или десятичное 11 представляется так:

>>>0o13
11

Новые двоичные константы начинаются префиксом 0b или 0B (т.е. ноль и за ним маленькая или большая "b"). Десятичное 21 в двоичном виде представляется так:

>>>0b010101
21

Методы oct() и hex() были удалены.



В начало


Аннотации функций

Аннотации функцийАннотации функций ассоциируют некоторые выражения с частью функции, например параметрами, во время компиляции. Сами по себе аннотации функций не имеют смысла — они обрабатываются только в том случае, если это делает некая сторонняя библиотека. Целью аннотаций функций является стандартизация подхода к аннотированию параметров функции и возвращаемых значений. Синтаксис аннотаций функций выглядит так:

def methodName(param1: expression1, ..., paramN: expressionN)->ExpressionForReturnType:
    ...

Вот, например, аннотация параметров функции:

def execute(program:"name of program to be executed", error:"if something goes wrong"):
    ...

В следующем примере аннотируется возвращаемое значение функции. Это может быть полезно для проверки типа возвращаемого значения:

def getName() -> "isString":
     ...

Полную грамматику аннотаций функций можно найти в PEP 3107 (см. ссылку в разделе Ресурсы).



В начало


Заключение

Пасхальное яйцо

По моему мнению, лучший модуль, добавленный в Python 3 - это модуль antigravity. Запустите Python и введите в строке интерпретатора import antigravity. Вы не разочаруетесь.

Финальный релиз Python 3 вышел в начале декабря 2008 года. С этого времени я наблюдал за тем, как люди в некоторых блогах реагируют на проблему обратной совместимости версий. Хотя я не могу утверждать, что владею какой-либо информацией об официальном консенсусе, мнения в блогах, которые я читал, сильно поляризованы. Некоторым в сообществе Linux® разработчиков не нравится идея перехода на версию 3 ввиду большого количества кода, который нужно будет портировать. С другой стороны, многие Web-разработчики будут переходить на новую версию хотя бы из-за поддержки unicode-строк.

Прежде чем отказываться от новой версии, я рекомендую вам просмотреть документы PEP и почтовые рассылки разработки. В документах PEP разъясняется рациональность каждого конкретного изменения, связанные с ним преимущества, а также рассказывается о его реализации. Все эти изменения действительно хорошо обдумывались и долго обсуждались. Темы, раскрытые в этой серии, были представлены так, чтобы рядовой программист Python мог быстро почувствовать суть изменений, не просматривая все документы PEP.



Ресурсы

Научиться

Получить продукты и технологии

Обсудить


Об авторе

Сезар Отеро - независимый консультант, специализирующийся на Java и Python. Сезар имеет ученую степень по электротехнике, а его вторая специализация - математика.




Выскажите мнение об этой странице


Пожалуйста, найдите минутку и заполните форму, чтобы повысить уровень сервиса.



 


 


 


Поделиться этой статьей:

забобрить забобрить memori сохранить в memori




В начало


IBM обладает всеми авторскими правами касательно информации, расположенной на developerWorks. Использование информации приведенной на этом ресурсе без явного письменного разрешения от IBM или первоначального автора запрещены. Если Вы желаете использовать информацию с developerWorks, пожалуйста воспользуйтесь регистрационной формой для того, чтобы связаться с нами запрос на использование материалов developerWorks Россия.
    IBM в России Конфиденциальность Контакты