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