Шаг 173.
Python: сборник рецептов.
Метапрограммирование. Программное определение классов

    На этом шаге мы рассмотрим способы решения этой задачи.

Задача

    Вы пишете код, задача которого - создать новый объект класса. Вы подумываете о том, чтобы отправить исходный код класса в строковом формате в функцию exec() для выполнения, но предпочли бы более элегантное решение.

Решение

    Вы можете использовать функцию types.new_class() для создания новых объектов классов. Вам нужно предоставить ей имя класса, кортеж с родительскими классами, именованные аргументы и функцию обратного вызова (callback), который заполнит словарь класса элементами. Например:

# Пример ручного создания класса из частей
# Методы
def __init__(self, name, shares, price):
    self.name = name
    self.shares = shares
    self.price = price

def cost(self):
    return self.shares * self.price

cls_dict = {
    '__init__' : __init__,
    'cost' : cost,
}

# Создание класса
import types

Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict))

if __name__ == '__main__':
    s = Stock('ACME', 50, 91.1)
    print(s)
    print(s.cost())
Архив с файлом можно взять здесь.

    Это создаст обычный объект класса, который работает именно так, как ожидается:

<types.Stock object at 0x0000023C4FAFBFD0>
4555.0

    В решении есть тонкий аспект - присваивание Stock.__module__ после вызова types.new_class(). Когда класс определен, атрибут __module__ содержит имя модуля, в котором он был определен. Это имя используется для вывода, который производят методы типа __repr__(). Оно также используется различными библиотеками, такими как pickle. Так что если вы хотите, чтобы созданный класс был "настоящим", убедитесь, что этот атрибут установлен правильно.

    Если класс, который вы хотите создать, использует метаклассы, это может быть определено в третьем аргументе, передаваемом в types.new_class(). Например:

>>> import abc
>>> Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta},
                           lambda ns: ns.update(cls_dict))
. . .
. . .
>>> Stock.__module__ = __name__
>>> Stock
<class '__main__.Stock'>
>>> type(Stock)
<class 'abc.ABCMeta'>
>>>

    Третий аргумент также может содержать другие именованные аргументы. Например, такое определение класса:

class Spam(Base, debug=True, typecheck=False):
    . . .
аналогично такому вызову new_class():
Spam = types.new_class('Spam', (Base,),
                                {'debug': True, 'typecheck': False}, 
                                 lambda ns: ns.update(cls_dict))

    Четвертый аргумент new_class() - самый загадочный. Это функция, которая принимает на вход объект отображения, использующийся для хранения пространства имен класса. Обычно это словарь, но на самом деле это объект, который возвращается методом prepare(), как описано в 169 шаге. Эта функция должна добавить новые записи к пространству имен, используя метод update() (как показано) или другие операции над объектами отображений.

Обсуждение

    Умение создавать классы таким образом может быть весьма полезно в некоторых обстоятельствах. Один из наиболее знакомых разработчикам примеров - использование функции collections.namedtuple(). Например:

>>> Stock = collections.namedtuple('Stock', ['name', 'shares', 'price'])
>>> Stock
<class '__main__.Stock'>
>>>
namedtuple() использует exec() вместо показанного в решении приема. Однако есть простой вариант, который создает класс напрямую:
import operator
import types
import sys

def named_tuple(classname, fieldnames):
    # Наполняем словарь акцессоров свойств полей
    cls_dict = { name: property(operator.itemgetter(n))
                 for n, name in enumerate(fieldnames) }

    # Создаем функцию 	__new__ и добавляем ее в словарь класса
    def __new__(cls, *args):
        if len(args) != len(fieldnames):
            raise TypeError('Expected {} arguments'.format(len(fieldnames)))
        return tuple.__new__(cls, (args))

    cls_dict['__new__'] = __new__

    # Создаем класс
    cls = types.new_class(classname, (tuple,), {},
                           lambda ns: ns.update(cls_dict))
    # Устанавливаем модуль класса на модуль вызывающего
    cls.__module__ = sys._getframe(1).f_globals['__name__']
    return cls

if __name__ == '__main__':
    Point = named_tuple('Point', ['x', 'y'])
    print(Point)
    p = Point(4, 5)
    print(len(p))
    print(p.x, p[0])
    print(p.y, p[1])
    try:
        p.x = 2
    except AttributeError as e:
        print(e)
    print('%s %s' % p)
Архив с файлом можно взять здесь.

    В последней части этого кода используется так называемый "фреймхак", который применяет sys._getframe() для получения имени модуля вызывавшего (caller). Еще один пример "фреймхака" показан в 37 шаге.

    Результат работы приведенного кода:

<class '__main__.Point'>
2
4 4
5 5
can't set attribute 'x'
4 5

    Важный аспект использованного в этом приеме рецепта - правильная поддержка метаклассов. Вы можете подумывать о создании класса напрямую путем прямого создания экземпляра метакласса. Например:

Stock = type('Stock', (), cls_dict)

    Проблема такого подхода заключается в том, что он пропускает некоторые критически важные шаги, такие как вызов метода метакласса __prepare__(). А вот types.new_class() позволяет удостовериться, что все необходимые шаги инициализации будут произведены. Например, функция обратного вызова, которая передается как четвертый аргумент в types.new_class(), принимает объект отображения, который возвращается методом prepare().

    Если вы всего лишь хотите выполнить подготовительный шаг, используйте types.prepare_class(). Например:

import types

metaclass, kwargs, ns = types.prepare_class('Stock', (), {'metaclass': type})

    Этот код обнаруживает подходящий метакласс и вызывает его метод prepare(). В результе возвращаются метакласс, оставшиеся именованные аргументы и подготовленное пространство имен.

    За дополнительной информацией обратитесь к PEP 3115, а также к документации Python.

    На следующем шаге мы рассмотрим инициализацию членов класса во время определения.




Предыдущий шаг Содержание Следующий шаг