Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Python: Общие вопросы > Динамический вызов класса


Автор: WolfAlone 23.12.2010, 17:07
Доброго времени суток!

Есть некий класс, например class1, у него есть метод: method1, который принимает 1 или более параметров (param1, ... paramX). Метод возвращает число или строку (не принципиально).

Есть список, list1, со следующим содержимым (по индексу):
0 - имя класса;
1 - имя метода;
2 - * параметр1, 2, 3 и т.д.

Необходимо, обработать список и на основе его содержимого обратиться к указанному в нём классу, затем методу и передать ему параметры. Результат работы - вернуть в вызывающую функцию.

Объясню суть вопроса.
В PHP для подобных целей есть, например функция: __autoload(), которая позволяет подгружать файл, в котором содержится объявление необходимого класса. Имя класса и его метод могут содержаться в переменной в виде строки. Параметры в функцию или метод класса можно передавать в виде массива, при этом, каждый элемент массива будет равен 1-му параметру функции/метода.

Хотелось бы узнать, как сделать подобное в Python?

Автор: skyboy 30.12.2010, 00:26
Цитата(WolfAlone @  23.12.2010,  16:07 Найти цитируемый пост)
В PHP для подобных целей есть

http://php.net/call_user_func_array есть в PHP
а для Python, как я понял. можно http://stackoverflow.com/questions/3061/calling-a-function-from-a-string-with-the-functions-name-in-python

Добавлено через 1 минуту и 32 секунды
а вот как передать массив параметров, как совокупность - не знаю.

Автор: av0000 30.12.2010, 12:49
Ну, например: как-то так (без проверок на ошибки)

Код

import my_module

for x in list1:
  cls = getattr(my_module, x[0]) # класс
  c = cls() # экземпляр класса
  m = getattr(c, x[1]) # метод

  ret = m(*x[2:]) # остатки массива как развёрнутый список параметров, для именованных (dict) - **{}


UPD: Для динамической загрузке модулей смотри в сторону imp
Вот кусок из моего проектика, где загружаются плагины, так, для иллюстрации:
Код

    def load_plugins(self):
        """Enumerate and load plugin modules without creation instances."""

        dirs = [os.path.join(config.APP_PATH, config.DIR_PLUGINS)]
        s = config.ini.value("pluginspath").toString()
        if s:
            #dirs.extend([os.path.join(unicode(s),x) for x in os.listdir(s)])
            dirs.append(unicode(s))
        plgs = []
        for d in set(dirs): # set prevents double-includes
            for x in os.listdir(d):
                dd = os.path.join(d, x)
                if os.path.isdir(dd):
                    try:
                        f = imp.find_module(x, [d])
                    except:
                        self.log.debug("Non-plugin folder in plugins tree: '%s'" % (dd))
                        f = None
                    if f:
                        self.log.debug("Loading plugin '%s' from '%s'..." % (x, dd))
                        try:
                            m = imp.load_module(x, *f)
                            self.log.debug("The plugin module '%s' loaded." % (x,))
                            self.plug_modules.append(m) # array of modules
                        except Exception as e:
                            self.log.exception("Error loading plugin '%s':\n%s" % (x, e))


    def create_plugins(self):
        """Enumerate plugin modules and create plugin classes.

        Should be done AFTER loading translations."""
        for m in self.plug_modules:
            for mm in dir(m):
                cl = getattr(m, mm)
                if inspect.isclass(cl) and issubclass(cl, base.BasePlugin):
                    self.log.debug("Create plugin instance %s:%s" % (m.__name__, mm))
                    self.addPlugin(cl) # exception-safe

Автор: bilbobagginz 7.1.2011, 18:48
Цитата(WolfAlone @  23.12.2010,  16:07 Найти цитируемый пост)
Хотелось бы узнать, как сделать подобное в Python? 

обычно так не работают в питоне, но если уж не в терпеж:
Код

class A:
   def func(self, a):
      print 'called to A::func(%s)' %a

class B:
   def funcb(self, b):
      print 'called to B::funcb(%s)' %b


l1 = [ 'A', 'func', 'par1', 'par2', 'parN']
l2 = [ 'B', 'funcb', 'par3', 'par5', 'parZ']

def test_dyncall(l):
   cn = eval(l[0]) # находим имя класса А
   args = l[2:] # список параметров
   b = cn() #создаем объект класса А
   b.fn = eval("b.%s" %l[1]) # определим метод по имени fn
   for i in args:
      b.fn(i) # de facto вызовы A.func(i) по указателю fn.

for l in (l1, l2):
   test_dyncall(l)


ессно надо протестить на всякие бяки....



Автор: WolfAlone 18.1.2011, 12:16
Цитата(bilbobagginz @  7.1.2011,  18:48 Найти цитируемый пост)
обычно так не работают в питоне, но если уж не в терпеж:

Подскажите пожалуйста, как правильно решать подобную задачу?

В своё время работал с такой штукой на PHP как CodeIgniter, там очень удобно прасится URL. Выглядит это примерно так:
example.com/class/func/param1/param2/param3/etc

где:
example.com - адрес сайта
class - пользовательский контроллер (класс)
func - функция в этом классе (метод класса)
param 1-3 (всё остальное) - входные параметры для этой функции

Задумка мне очень понравилась, решил сделать нечто аналогичное на Python...

Автор: WolfAlone 19.1.2011, 22:50
Писал я тут писал... Много написал smile Как вдруг до меня стало доходить!
av0000, спасибо огромное, Вы натолкнули меня на верное направление!
bilbobagginz, премного благодарен! Из двух примеров выше сейчас буду пробовать собрать 1  smile 

Автор: WolfAlone 19.1.2011, 23:16
Остался пожалуй только 1 вопрос:
Как защититься от "кул хацкеров", т.е. проще говоря как "найти" класс, зная его имя записанное в строковую переменную без eval() или как проверить, то ли (что нужно/можно) выполняется в eval(), что может выполняться? Если создать список с допустимыми значениями - это будет надёжной защитой?

Я немного поясню:
Код

s = 'class1'
c = eval(s)
z = c() #Этот код прекрасно работает, именно так - как я хотел.
#Проблема заключается только в том, что s - в скрипт передаёт пользователь, и соответственно, передать он суда может всё, что угодно!

Автор: av0000 20.1.2011, 11:33
Ну, во-первых я сильно не люблю пользоваться eval по двум причинам - выжирание памяти "как под целый питон" на время запуска (можно сказать, что на вызов eval "запускается" новый интерпретатор) и как раз эта дыра с запуском произвольного кода.

решение "раз" - сделать словарь с именами классов и классами и искать допустимые только в нем:
Код

AAA = {
    'class1': Class1, # 
    'class2': MySuperPuperClass,
}
# юзер вводит класс
s = rawinput()
cls = AAA.get(s, None)
if cls:
   instance = cls()


решение "два" если охота таки использовать eval:
Код

try:
   inst = eval(s + '()', {}, {})
except:
   pass # как-то обрабатываем

"фишка" второго решения - во-первых принудительно создаём экземпляр внутри eval - какую-то часть "мусора" удастся этим отсечь, во-вторых - ограничиваем список того, чем может пользоваться eval (см. доку на её параметры) НО! нормального сандбокса таким ограничением не сделать :( и при желании юзер может ввести что-то типа "import os; os.system('rm -rf /')" и кому-то будет очень весело ;)

PS: прошу не воспринимать строку с import буквально smile прям так, конечно никто сделать не даст, но обойти "защиту" в виде {} можно, такая инфа мне попадалась в и-нете, так что стОит быть аккуратным
PPS: а заполнять словарь из первого примера можно по аналогии с тему куском кода, что я привёл раньше
Код

import mmm
import inspect
AAA = {}
for x in dir(mmm):
    a = getattr(mmm, x)
    if inspect.isclass(a):
        AAA[a.__name__] = a


Автор: stalk13 14.5.2011, 16:04
Я бы в этом случае использовал декораторы:

Код

"""
Examples:

>>> @controller
... class default:
...
...     @action
...     def index(self, arg1=100):
...         return arg1
...
...     @action
...     def other_action(self, arg1, arg2='test'):
...         return 'Other action: %s, %s' % (arg1, arg2)
...
...     def no_action_method(self, *args):
...         pass

>>> route('default', 'index')
100

>>> params = ['default', 'other_action', 'foo', 'bar']
>>> route(*params)
'Other action: foo, bar'

>>> params = 'default/no_action_method/param1'.split('/')
>>> route(*params)
Traceback (most recent call last):
    ...
RuntimeError: Action "no_action_method" of controller "default" is not found
"""

def route(controller, action, *args):
    if not controller in route.controllers:
        raise RuntimeError('Controller "%s" is not found' % controller)

    cls = route.controllers[controller]()

    if not action in cls._actions:
        raise RuntimeError('Action "%s" of controller "%s" is not found' % (action, controller))

    return cls._actions[action](cls, *args)

def controller(cls):
    """ Class decorator """

    cls._actions = controller.actions
    controller.actions = {}
    route.controllers[cls.__name__] = cls
    return cls

def action(func):
    """ Method decorator """

    controller.actions[func.__name__] = func
    return func

route.controllers = {}
controller.actions = {}

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Автор: bilbobagginz 15.5.2011, 07:49
согласен, что eval ресурсы хавает.
но:
Цитата(av0000 @  20.1.2011,  10:33 Найти цитируемый пост)
и при желании юзер может ввести что-то типа "import os; os.system('rm -rf /')"

не напугал. при этом желании юзер должен стать root-ом. ну или вылезти из jail. что не так уж и просто...

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)