Цей переклад - продовження циклу про внутрішнє влаштування деяких фіч в Python. Сьогодні ми поговоримо про метакласи.
Класи як об'єкти
Як ви знаєте, в Python все є об'єктами: виявляється, це справедливо і для класів. Погляньте нижче:
Створимо пустий клас
class DoNothing(object):
pass
Якщо створити об'єкт, то ми зможемо використати функцію type
щоб побачити тип нашого об'єкту:
>>> d = DoNothing()
>>> type(d)
__main__.DoNothing
Як бачите, наша змінна d
це екземпляр класу __main__.DoNothing
.
Також це працює і для вбудованих типів:
>>> L = [1, 2, 3]
>>> type(L)
list
Список, як ви можете здогадатися, є об'єктом типу list
.
Але давайте копнемо глибше: якого типу сам клас DoNothing
?
>>> type(DoNothing)
type
Тип класу DoNothing
це type
. Це вказує на те, що клас DoNothing
сам є об'єктом і має тип type
.
Те саме з вбудованими типами:
>>> type(tuple), type(list), type(int), type(float)
(type, type, type, type)
Це показує, що в Python класи є об'єктами, і вони мають тип type
. type
це метаклас — клас, що описує інші класи. Всі класи нового стилю Python є екземплярами метакласу type
, включаючи сам type
:
>>> type(type)
type
Так, ви все правильно прочитали: тип type
це type
. Іншими словами, type
це екземпляр самого себе. Такий вид замикання неможливо (наскільки я знаю) написати самому на чистому Python, і така поведінка є невеличким хаком на рівні інтерпретатора.
Метапрограмування: створення класів на льоту
Тепер, коли ви зрозуміли всю епопею з класами та об'єктами, поговоримо про високе, про метапрограмування. Ви, скоріше всього, писали функції, які повертали об'єкти. Ми можемо прийняти такі функції за фабрику об'єктів (пер. object factory, один з Design Patterns): вона приймає аргументи, створює об'єкт і повертає його. Ось невеличкий приклад:
>>> def int_factory(s):
>>> i = int(s)
>>> return i
>>> i = int_factory('100')
>>> print(i)
100
Це дуже примітивно, але всі функції схожі між собою: беруть деякі аргументи, виконують деякі обчислення, створюють та повертають об'єкт. І нічого не стримує нас від створення об'єкту типу type
(так, класу), і його повернення. Це і є метафункція:
>>> def class_factory():
>>> class Foo(object):
>>> pass
>>> return Foo
>>>
>>> F = class_factory()
>>> f = F()
>>> print(type(f))
<class '__main__.foo'="">
Так само як функція int_factory
створює і повертає int
, а class_factory
повертає клас.
Але така конструкція трохи безглузда: зокрема, якщо ми захочемо написати більш складу логіку при створенні Foo
. Було б непогано уникнути додаткових вкладених блоків і генерувати класи більш динамічним способом. Це можна зробити так:
>>> def class_factory():
>>> return type('Foo', (), {})
>>>
>>> F = class_factory()
>>> f = F()
>>> print(type(f))
<class '__main__.foo'="">
Конструкція
class MyClass(object):
pass
є еквівалентною цій:
MyClass = type('MyClass', (), {})
MyClass
- об'єкт типу type
, що можна побачити у другому об'явленні. Вас може заплутати те, що type
також функція для визначення типу об'єкта, але потрібно відрізняти ці два використання одного й того ж ключового слова: тут type
- це клас (точніше метаклас), а MyClass
це об'єкт класу/типу type
.
Конструктор приймає такі аргументи:
type(name, bases, dct)
-
name
- рядок, що визначає ім'я майбутнього класу -
bases
- кортеж, що містить батьківські класи, які потрібно успадкувати -
dct
- словник методів та властивостей майбутнього класу
Наприклад, два наступні методи ідентичні:
>>> class Foo(object):
>>> i = 4
>>>
>>> class Bar(Foo):
>>> def get_i(self):
>>> return self.i
>>>
>>> b = Bar()
>>> print(b.get_i())
4
>>> Foo = type('Foo', (), dict(i=4))
>>>
>>> Bar = type('Bar', (Foo,), dict(get_i = lambda self: self.i))
>>>
>>> b = Bar()
>>> print(b.get_i())
4
Можливо ці приклади виглядають заплутаними та надуманими, але це може бути дуже потужним засобом динамічного створення нових класів на льоту.
Роблячи світ цікавішим: довільні метакласи
А от тут вже починаються веселощі. Ми можемо успадкуватися і розширити клас, що створили. Так само можна робити і з метакласами, щоб створити потрібну вам поведінку.
Приклад 1: модифікування атрибутів
Давайте використаємо простий приклад, де ми хочемо побудувати API, за допомогою якого користувач зможе створити інтерфейси, що будуть зберігати файловий об'єкт. Кожен інтерфейс повинен мати унікальний рядок-ідентифікатор і, власне, сам відкритий файловий об'єкт. Користувач може потім написати спеціалізовані методи для виконання певних завдань. Є гарний спосіб вирішити це і без метакласів, але він не для нас.
Спочатку створимо метаклас інтерфейсу, успадковуючи його від type
:
class InterfaceMeta(type):
def __new__(cls, name, parents, dct):
# створюємо class_id, якщо він не заданий
if 'class_id' not in dct:
dct['class_id'] = name.lower()
# відкриваємо потрібний файл на запис
if 'file' in dct:
filename = dct['file']
dct['file'] = open(filename, 'w')
# нам потрібно викликати type.__new__ для завершення ініціалізації
return super(InterfaceMeta, cls).__new__(cls, name, parents, dct)
Зауважте, що ми модифікували вхідний словник (атрибути і методи класу), щоб додати ідентифікатор, якщо він не заданий, та замінити ім'я файлу на відповідний файловий об'єкт.
А зараз ми використаємо InterfaceMeta
щоб створити об'єкт Interface
:
>>> Interface = InterfaceMeta('Interface', (), dict(file='tmp.txt'))
>>> print(Interface.class_id)
interface
>>> print(Interface.file)
<open 'tmp.txt',="" 'w'="" 0x21b8810="" at="" file="" mode="">
Все проходить як потрібно: створюється змінна class_id
, а змінна з назвою файла заміняється на сам файловий об'єкт. Проте, створення класу Interface
безпосередньо з використанням InterfaceMeta
виглядає трохи незграбним і важкозрозумілим. Саме тут на сцену виходить __metaclass__
. Ось так це можна зробити за допомогою цієї змінної:
>>> class Interface(object):
>>> __metaclass__ = InterfaceMeta
>>> file = 'tmp.txt'
>>>
>>> print(Interface.class_id)
interface
>>> print(Interface.file)
<open 'tmp.txt',="" 'w'="" 0x21b8ae0="" at="" file="" mode="">
При вказанні атрибуту __metaclass__
ми кажемо інтерпретатору, що клас повинен буди створений за допомогою InterfaceMeta
замість стандартного type
. Щоб наочно в цьому переконатися, давайте перевіримо тип Interface
:
>>> type(Interface)
__main__.InterfaceMeta
І надалі всі класи, успадковуючи Interface
будуть створюватися за допомогою того самого метакласу:
>>> class UserInterface(Interface):
>>> file = 'foo.txt'
>>>
>>> print(UserInterface.file)
<open 'foo.txt',="" 'w'="" 0x21b8c00="" at="" file="" mode="">
>>> print(UserInterface.class_id)
userinterface
Цей простий приклад показує як саме можна використовувати метакласи, і що вони можуть допомогти в створенні гарного і адаптивного API для проектів. Наприклад, Django project використовують схожі конструкції, щоб дозволити робити дуже масштабні розширення.
Приклад 2: реєстрація підкласів
Іншим способом використання метакласів є реєстрація всіх підкласів, успадкованих від базового класу. Наприклад, ви маєте базовий клас-інтерфейс для доступу до БД і хочете дозволити користувачу писати власні інтерфейси, що будуть зберігатися у головному реєстрі.
Ви можете зробити так:
class DBInterfaceMeta(type):
# тут ми використовуємо __init__ замість __new__ тому що хочемо
# модифікувати атрибути класу *після* того,
# як він буде створений
def __init__(cls, name, bases, dct):
if not hasattr(cls, 'registry'):
# Це базовий клас. Створюємо пустий реєстр
cls.registry = {}
else:
# А це вже успадкований, додаємо до реєстру
interface_id = name.lower()
cls.registry[interface_id] = cls
super(DBInterfaceMeta, cls).__init__(name, bases, dct)
Наш метаклас лише додає словник-реєстр, якщо його не було, та додає в нього нові класи. Давайте подивимось як це працює:
>>> class DBInterface(object):
>>> __metaclass__ = DBInterfaceMeta
>>>
>>> print(DBInterface.registry)
{}
А тепер давайте створимо декілька підкласів і перевіримо чи потрапили вони в реєстр:
>>> class FirstInterface(DBInterface):
>>> pass
>>>
>>> class SecondInterface(DBInterface):
>>> pass
>>>
>>> class SecondInterfaceModified(SecondInterface):
>>> pass
>>>
>>> print(DBInterface.registry)
{'firstinterface': <class '__main__.firstinterface'="">, 'secondinterface': <class '__main__.secondinterface'="">, 'secondinterfacemodified': <class '__main__.secondinterfacemodified'="">}
Ну ось, все працює як потрібно!
Підсумок: коли треба використовувати метакласи?
На прикладах я розповів, що таке метакласи і як їх можна використати для створення API. Так чи інакше, метакласи стоять за всім, що ви робите в Python, але середньому програмісту рідко доводиться стикатися з ними.
Лишається питання: коли саме ви повинні використовувати довільні метакласи в своєму проекті?
Це складне питання, але є цитата, що дуже лаконічно на нього відповідає:
Метакласи - магія. Магія, складніша чим 99% того, що може колись знадобитися користувачу. Якщо ви не знаєте чи потрібні вони вам - то ні, вони вам не потрібні. Той, кому вони будуть дійсно корисні, сам знає про це без всіляких пояснень.
Дійсно, є багато проблем, які можна вирішити метакласами. Але велику більшість з них можна вирішити іншим, більш чистим способом. Перш ніж знайти задачу, яку можна було вирішити лише метакласами, я витратив 6 років на наукову роботу з Python. Але коли я її знайшов — я знав. Просто знав.
Ще немає коментарів