0

Динамическое создание методов (генерация кода) в Python на этапе выполнения

10

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

Я придумал решение, комбинируя exec и setattr. Вот пример:

class Viking(object):
    def __init__(self):
        code = '''
            def dynamo(self, arg):
                """ dynamo's a dynamic method!
                """
                self.weight += 1
                return arg * self.weight
            '''
        self.weight = 50

        d = {}
        exec code.strip() in d
        setattr(self.__class__, 'dynamo', d['dynamo'])


if __name__ == "__main__":
    v = Viking()
    print(v.dynamo(10))
    print(v.dynamo(10))
    print(v.dynamo.__doc__)

Есть ли лучший, более безопасный или идиоматичный способ добиться того же результата?

5 ответ(ов)

0

На основе кода Терана, вот как можно расширить его, добавив методы в классы:

class Dynamo(object):
    pass

def add_dynamo(cls, i):
    def innerdynamo(self):
        print("in dynamo %d" % i)
    innerdynamo.__doc__ = "docstring for dynamo%d" % i
    innerdynamo.__name__ = "dynamo%d" % i
    setattr(cls, innerdynamo.__name__, innerdynamo)

for i in range(2):
    add_dynamo(Dynamo, i)

d = Dynamo()
d.dynamo0()
d.dynamo1()

Этот код добавляет методы к классу Dynamo динамически с помощью функции add_dynamo. Мы создаем функцию innerdynamo, которая принимает аргумент self, и внутри нее выводим строку с номером динамо. Затем мы устанавливаем документацию и имя метода, прежде чем добавлять его в класс через setattr.

В результате, создавая экземпляр класса Dynamo и вызывая методы dynamo0() и dynamo1(), мы получаем следующий вывод:

in dynamo 0
in dynamo 1

Таким образом, код демонстрирует способ динамического добавления методов в класс в Python.

0

Функции, такие как docstrings и имена, являются изменяемыми свойствами. Вы можете делать в вложенной функции что угодно, или даже иметь несколько версий этой вложенной функции, между которыми будет выбирать makedynamo(). Нет необходимости строить какой-либо код из строк.

Вот отрывок из интерпретатора:

>>> def makedynamo(i):
...     def innerdynamo():
...         print("in dynamo %d" % i)
...     innerdynamo.__doc__ = "docstring for dynamo%d" % i
...     innerdynamo.__name__ = "dynamo%d" % i
...     return innerdynamo

>>> dynamo10 = makedynamo(10)
>>> help(dynamo10)
Help on function dynamo10 in module __main__:

dynamo10()
    docstring for dynamo10

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

0

В Python действительно можно объявлять функции внутри других функций, поэтому вам не нужно прибегать к трюкам с exec.

Вот пример, как можно это сделать:

def __init__(self):

    def dynamo(self, arg):
        """Метод dynamo — динамический метод!
        """
        self.weight += 1
        return arg * self.weight
    self.weight = 50

    setattr(self.__class__, 'dynamo', dynamo)

Если вам нужно создать несколько версий функции, вы можете поместить этот код в цикл и варьировать имена в функции setattr:

def __init__(self):

    for i in range(0, 10):

        def dynamo(self, arg, i=i):
            """Метод dynamo — динамический метод!
            """
            self.weight += i
            return arg * self.weight

        setattr(self.__class__, 'dynamo_' + str(i), dynamo)
        self.weight = 50

(Я понимаю, что это не самый лучший код, но он донесет суть). Что касается установки строки документации, это действительно возможно, но мне потребуется посмотреть документацию.

Редактирование: Вы можете установить строку документации через dynamo.__doc__, так что в теле вашего цикла можно сделать что-то вроде этого:

dynamo.__doc__ = "Добавляет %s к весу" % i

Еще одно редактирование: С помощью @eliben и @bobince проблема с замыканием должна быть решена.

0

В приведённом коде создаётся класс Dynamo, в котором реализован статический метод init. Этот метод принимает необязательный аргумент initData, который представляет собой словарь. Если передано значение initData, создаётся экземпляр класса Dynamo, и для каждой пары ключ-значение в словаре создаётся метод с именем, соответствующим ключу, и телом, соответствующим значению.

Вот подробный разбор работы кода:

  1. Определение класса:

    class Dynamo(object):
        def __init__(self):
            pass
    

    Здесь определяется класс Dynamo, который, в текущем виде, не инициализирует никаких данных.

  2. Статический метод init:

    @staticmethod
    def init(initData=None):
    

    Этот метод статический, что означает, что его можно вызывать без создания экземпляра класса. Он принимает один аргумент initData, который по умолчанию равен None.

  3. Обработка входных данных:

    if initData is not None:
        dynamo = Dynamo()
    

    Если initData не равно None, создаётся новый экземпляр класса Dynamo.

  4. Динамическое создание методов:

    for name, value in initData.items():
        code = '''
    

def %s(self, *args, **kwargs): %s ''' % (name, value) result = exec code.strip() in result setattr(dynamo.class, name, result[name])


Для каждого ключа и значения в `initData` создаётся строка, представляющая код нового метода. Затем этот код выполняется с помощью функции `exec`, что позволяет добавлять метод к классу `Dynamo` с именем, заданным в строчном ключе.

5. **Возврат экземпляра**:
```python
return dynamo

После добавления всех методов экземпляр класса Dynamo возвращается.

Если initData равно None, метод просто возвращает None.

  1. Пример использования:

    service = Dynamo.init({'fnc1': 'pass'})
    service.fnc1()
    

    Здесь создаётся экземпляр service, в который добавляется метод fnc1, который не выполняет никаких действий (использует оператор pass). Затем этот метод вызывается.

Таким образом, код динамически создает методы в классе на основе переданных данных, что может быть полезно для задания функций на лету, но следует быть осторожным с использованием exec, так как это может привести к проблемам с безопасностью, если код не контролируется.

0

Вот более общий способ решения задачи:

Вы можете вызывать любой метод экземпляра класса Dummy. Докстрока генерируется на основе имени метода. Обработка любых входных аргументов демонстрируется простым их возвратом.

Код

class Dummy(object):

    def _mirror(self, method, *args, **kwargs):
        """doc _mirror"""
        return args, kwargs

    def __getattr__(self, method):
        "doc __getattr__"

        def tmp(*args, **kwargs):
            """doc tmp"""
            return self._mirror(method, *args, **kwargs)
        tmp.__doc__ = (
                'сгенерированная строка документации, доступная через {:}.__doc__'
                .format(method))
        return tmp

d = Dummy()    
print(d.test2('asd', level=0), d.test2.__doc__)
print(d.whatever_method(7, 99, par=None), d.whatever_method.__doc__)

Вывод

(('asd',), {'level': 0}) сгенерированная строка документации, доступная через test2.__doc__
((7, 99), {'par': None}) сгенерированная строка документации, доступная через whatever_method.__doc__

Таким образом, класс Dummy позволяет создавать методы на лету и легко отслеживать их аргументы и документацию.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь