Метакласи в Python

10 хв. читання

Цей переклад - продовження циклу про внутрішнє влаштування деяких фіч в 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. Але коли я її знайшов — я знав. Просто знав.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 3.9K
Приєднався: 11 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація