Динамическое создание методов (генерация кода) в Python на этапе выполнения
Я пытаюсь сгенерировать код для метода во время выполнения. Важно, чтобы я мог запускать произвольный код и иметь строку документации для метода.
Я придумал решение, комбинируя 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 ответ(ов)
На основе кода Терана, вот как можно расширить его, добавив методы в классы:
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.
Функции, такие как 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
Таким образом, вы можете динамически определять свойства функции, что делает ваш код более гибким и удобным.
В 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 проблема с замыканием должна быть решена.
В приведённом коде создаётся класс Dynamo
, в котором реализован статический метод init
. Этот метод принимает необязательный аргумент initData
, который представляет собой словарь. Если передано значение initData
, создаётся экземпляр класса Dynamo
, и для каждой пары ключ-значение в словаре создаётся метод с именем, соответствующим ключу, и телом, соответствующим значению.
Вот подробный разбор работы кода:
Определение класса:
class Dynamo(object): def __init__(self): pass
Здесь определяется класс
Dynamo
, который, в текущем виде, не инициализирует никаких данных.Статический метод
init
:@staticmethod def init(initData=None):
Этот метод статический, что означает, что его можно вызывать без создания экземпляра класса. Он принимает один аргумент
initData
, который по умолчанию равенNone
.Обработка входных данных:
if initData is not None: dynamo = Dynamo()
Если
initData
не равноNone
, создаётся новый экземпляр классаDynamo
.Динамическое создание методов:
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
.
Пример использования:
service = Dynamo.init({'fnc1': 'pass'}) service.fnc1()
Здесь создаётся экземпляр
service
, в который добавляется методfnc1
, который не выполняет никаких действий (использует операторpass
). Затем этот метод вызывается.
Таким образом, код динамически создает методы в классе на основе переданных данных, что может быть полезно для задания функций на лету, но следует быть осторожным с использованием exec
, так как это может привести к проблемам с безопасностью, если код не контролируется.
Вот более общий способ решения задачи:
Вы можете вызывать любой метод экземпляра класса 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
позволяет создавать методы на лету и легко отслеживать их аргументы и документацию.
В чем разница между eval, exec и compile?
Python: как получить вывод print в выражении exec
Как изменить порядок столбцов в DataFrame?
'pip' не распознан как командa внутреннего или внешнего формата
Почему statistics.mean() работает так медленно?