Динамическое определение класса в Python

    Под динамическим определением объекта можно понимать определение во время исполнения. В отличие от статического определения, которое используется в привычном определении класса с помощью ключевого слова class, динамическое определение использует встроенный класс type.

    Метакласс type


    Класс type часто используется для получения типа объекта. Например так:

    h = "hello"
    type(h)
    <class 'str'>

    Но у него есть другое применение. Он может инициализировать новые типы. Как известно, всё в Python – объект. Из этого следует, что у всех определений имеются типы, включая классы и объекты. Например:

    class A:
        pass
    type(A)
    <class 'type'>
    

    Может быть не совсем понятно, почему классу присваивается тип класса type, в отличие от его экземпляров:

    a = A()
    type(a)
    <class '__main__.A'>
    

    Объекту a в качестве типа присваивается класс. Так интерпретатор обрабатывает объект как экземпляр класса. Сам же класс имеет тип класса type потому, что он наследует его от базового класса object:

    A.__bases__
    (<class 'object'>,)
    

    Тип класса object:

    type(object)
    <class 'type'>
    

    Класс object наследуют все классы по умолчанию, то есть:

    class A(object):
        pass
    

    Тоже самое, что:

    class A:
        pass
    

    Определяемый класс наследует базовый в качестве типа. Однако, это не объясняет, почему базовый класс object имеет тип класса type. Дело в том, что type – это метакласс. Как это уже известно, все классы наследуют базовый класс object, который имеет тип метакласса type. Поэтому, все классы так же имеют этот тип, включая сам метакласс type:

    type(type)
    <class 'type'>
    

    Это «конечная точка типизации» в Python. Цепочка наследования типов замыкается на классе type. Метакласс type служит базой для всех классов в Python. В этом несложно убедиться:

    builtins = [list, dict, tuple]
    for obj in builtins:
        type(obj)
    <class 'type'>
    <class 'type'>
    <class 'type'>
    

    Класс – это абстрактный тип данных, а его экземпляры имеют ссылку на класс в качестве типа.

    Инициализация новых типов с помощью класса type


    При проверке типов класс type инициализируется с единственным аргументом:

    type(object) -> type
    

    При этом он возвращает тип объекта. Однако в классе реализован другой способ инициализации с тремя аргументами, который возвращает новый тип:

    type(name, bases, dict) -> new type
    

    Параметры инициализации класса type


    • name
      Строка, которая определяет имя нового класса (типа).
    • bases
      Кортеж базовых классов (классов, которые унаследует новый класс).
    • dict
      Словарь с атрибутами будущего класса. Обычно со строками в ключах и вызываемых типах в значениях.

    Динамическое определение класса


    Инициализируем класс нового типа, предоставив все необходимые аргументы и вызываем его:

    MyClass = type("MyClass", (object, ), dict())
    MyClass
    <class '__main__.MyClass'>

    С новым классом можно работать как обычно:

    m = MyClass()
    m
    <__main__.MyClass object at 0x7f8b1d69acc0>

    Причём, способ эквивалентен обычному определению класса:

    class MyClass:
        pass

    Динамическое определение атрибутов класса


    В пустом классе мало смысла, поэтому возникает вопрос: как добавить атрибуты и методы?

    Чтобы ответить на этот вопрос, рассмотрим изначальный код инициализации:

    MyClass = type(“MyClass”, (object, ), dict())
    

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

    MyClass = type(“MyClass”, (object, ), dict(foo=“bar”)
    m = MyClass()
    m.foo
    'bar'

    Динамическое определение методов


    В словарь можно передать и вызываемые объекты, например методы:

    def foo(self):
        return “bar”
    MyClass = type(“MyClass”, (object, ), dict(foo=foo))
    m = MyClass()
    m.foo
    'bar'

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

    MyClass = type(“MyClass”, (object, ), dict())
    

    После инициализации пустого класса, можно добавить в него методы динамически, то есть, без явного статического определения:

    code = compile('def foo(self): print(“bar”)', "<string>", "exec")
    

    compile – это встроенная функция, которая компилирует исходный код в объект. Код можно выполнить функциями exec() или eval().

    Параметры функции compile


    • source
      Исходный код, может быть ссылкой на модуль.
    • filename
      Имя файла, в который скомпилируется объект.
    • mode
      Если указать "exec", то функция скомпилирует исходный код в модуль.

    Результатом работы compile является объект класса code:

    type(code)
    <class 'code'>

    Объект code нужно преобразовать в метод. Так как метод – это функция, то начнём с преобразования объекта класса code в объект класса function. Для этого импортируем модуль types:

    from types import FunctionType, MethodType
    

    Я импортирую MethodType, так как он понадобится в дальнейшем для преобразования функции в метод класса.

    function = FunctionType(code.co_consts[0], globals(), “foo”)
    

    Параметры метода инициализации класса FunctionType


    • code
      Объект класса code. code.co_consts[0] – это обращение к дискриптору co_consts класса code, который представляет из себя кортеж с константами в коде объекта. Представьте себе объект code как модуль с одной единственной функцией, которую мы пытаемся добавить в качестве метода класса. 0 – это её индекс, так как она единственная константа в модуле.
    • globals()
      Словарь глобальных переменных.
    • name
      Необязательный параметр, определяющий название функции.

    В результате получилась функция:

    function
    <function foo at 0x7fc79cb5ed90>
    type(function)
    <class 'function'>

    Далее необходимо добавить эту функцию в качестве метода класса MyClass:

    MyClass.foo = MethodType(function, MyClass)
    

    Достаточно простое выражение, которое назначает нашу функцию методом класса MyClass.

    m = MyClass()
    m.foo()
    bar

    Предупреждение


    В 99% случаев можно обойтись статическим определением классов. Однако концепция метапрограммирования хорошо раскрывает внутреннее устройство Python. Скорее всего вам будет сложно найти применение описанных здесь методов, хотя в моей практике такой случай, все же, был.

    А вы работали с динамическими объектами? Может быть в других языках?

    Ссылки


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 16

      0
      habr.com/ru/post/205944
      Тут все давно есть
      +1

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

        0

        Вы правы, ему можно скормить объект класса code, и он скомпилирует все его константы. Однако, если есть готовый класс, почему бы не импортировать его явно? Данный метод хорошо подходит для генерации API, когда названия классов и методов задаются из пространства имён, а не вручную.

        0

        Баловство все это.
        Может кроме метаклассов. Да и они большинству разработчиков не известны, и это к счастью.
        Почему это плохо:


        • если вам нужна подобная магия, вам стоит подумать над проектом еще раз
        • чем грозит использование eval и ему подобных вызовов (в разных языках они называются по разному) для безопасности в истории есть достаточно примеров, особенно для сервисов
        • вам нужно корректно отрабатывать ошибки, начиная с банальных опечаток, нюансов разных версий синтаксиса и отсутствия зависимостей или их некорректную версию
        • вы теряете оптимизацию самого питона. он не зря сохраняет байткод. компилировать свой код налету — это может быть очень дорого и долго.

        PS. По поводу, если не создания, то динамической привязки методов, поищите информацию об unbound методах в питоне. В тройке от них отказались, оставив bounded и static, но зато вы окажитесь рядом с информацией, как добавить в класс метод даже не прибегая к метапрограммированию.

          –2

          Блин, зачем эта статья нужна? Есть же документация. Да и тема давно много раз обсосона с разных сторон. Ещё одна статья про метаклассы...

            +1

            Я не заставляю вас читать.

            +2
            Спасибо, не знал про FunctionType, когда понадобилась ast-магия подменял function.__code__.

            Однако концепция динамического программирования хорошо раскрывает внутренне устройство Python.


            Думаю, что Вы не имели ввиду устоявшийся термин.
              –2

              Спасибо! Вы правы, я не имел ввиду классический термин. Оставлю как есть, времена меняются)

              0
              Зачем? Новичкам этого не надо, а более опытные и сами знают где найти исчерпывающую документацию. И, могу конечно ошибаться, но type вроде не абстрактный.
                0

                Type – метакласс, спасибо, что поправили.

                0
                Не натыкался раньше на FunctionType и MethodType, спасибо!
                  0
                  Почему у Вас:
                  builtins = [list(), dict(), tuple()]
                  for obj in builtins:
                      print(type(obj))
                  <class 'type'>
                  <class 'type'>
                  <class 'type'>

                  А у меня:
                  builtins = [list(), dict(), tuple()]
                  >>> for obj in builtins:
                  ...     print(type(obj))
                  ...
                  <class 'list'>
                  <class 'dict'>
                  <class 'tuple'>

                  ?
                  Add: Да, теперь норм
                    0
                    Спасибо, исправил. Случайно прописал вызовы классов. Попробуйте снова исправленный код.
                    +2
                    думаю, что в контексте задач динамического программирования

                    Должен вас огорчить, к задачам динамического программирования это не имеет никакого отношения, это скорее из серии метапрограммирования.

                      0

                      Спасибо, я догадывался, но поспешил с терминологическими определениями.

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

                    Самое читаемое