Выборочный запуск нескольких функций, в зависимости от внешней конфигурации

Коллеги! Даны несколько функций, которые принимают один и тот же аргумент.

def func1(a):
    print('Первая функция')
    return a

def func2(a):
    print('Вторая функция')
    return a

Откуда-то (например, из файла, хранящего конфигурацию или из стандартного ввода, неважно) скрипт получает настройку, какие функции запускать, какие нет.

dic_flag = {'fun1': True, 'fun2': False}

Можно написать кучу условных операторов, чтобы запускать эти функции.

if dic_flag['fun1']:
    print(func1(a))

if dic_flag['fun2']:
    print(func2(a))

Но хочется процесс автоматизировать, например, вот так:

def func1(a):
    print('Первая функция')
    return a

def func2(a):
    print('Вторая функция')
    return a

dic_flag = {'fun1': True, 'fun2': False}
dic_func = {'fun1': func1, 'fun2': func2}

a = 10

for flag in dic_flag:
    if dic_flag[flag]:
        print(dic_func[flag](a))

Вопросы

Что можно придумать красивого для данной задачи? Может в ООП что-то? Или какие-то стандартные библиотеки есть? В реальности функций несколько десятков.

Опционально:

А если основной аргумент один, а какие-то дополнительные параметры разные?


Ответы (3 шт):

Автор решения: Amgarak

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

class FunctionManager:
    def __init__(self, functions, flags):
        self.functions = functions
        self.flags = flags

    def run_functions(self, argument):
        for func_name, flag in self.flags.items():
            if flag and func_name in self.functions:
                print(self.functions[func_name](argument))

def func1(a):
    print('Первая функция')
    return a

def func2(a):
    print('Вторая функция')
    return a

dic_flag = {'func1': True, 'func2': False}
dic_func = {key: globals()[key] for key, value in dic_flag.items() if value and key in globals() and callable(globals()[key])}


manager = FunctionManager(dic_func, dic_flag)
a = 10
manager.run_functions(a)

Бонусом добавил что словарь dic_func собирается сам по заданным в dic_flag ключам. Ключи должны совпадать с названиями функций. Выключенные функции в словарь не попадают.


Так же можно поизврощаться, если в словаре fun1, а функция должна иметь другое название:

def func1(a):
    print('Первая функция')
    return a

def func2(a):
    print('Вторая функция')
    return a

dic_flag = {'fun1': True, 'fun2': True}
a = 10

for flag, value in dic_flag.items():
    if value:
        func_name = globals().get(f'func{flag[3:]}')  # Извлечение функции по имени
        if func_name:
            func_name(a)

Еще немного подумав, могу предложить такое направление мысли:

def func1(a, **kwargs):
    print('Первая функция')
    for k, v in kwargs.items(): print(f'Key={k} and Value={v}')
    return a

def func2(a, **kwargs):
    print('Вторая функция')
    for k, v in kwargs.items(): print(f'Key={k} and Value={v}')
    return a

def func3(a, **kwargs):
    print('Третья функция')
    for k, v in kwargs.items(): print(f'Key={k} and Value={v}')
    return a

dic_flag = {'fun1': [True, {'name': 'Pankaj', 'age': 34}], 'fun2': [False, {'name': 'J', 'age': 14}], 'fun3': [True, {'name': 'Pank', 'age': 54}]}
dic_func = {'fun1': func1, 'fun2': func2, 'fun3': func3}

a = 10

# Фильтруем функции по флагам
funcs_to_run = filter(lambda f: dic_flag[f][0], dic_func)
for value in funcs_to_run:
    print(dic_func[value](a,**dic_flag[value][1]))

Позвольте еще немного извращения, теперь уже с дампом самих функций:

import dill # pip install dill ,можно обойтись и стандартным import pickle

def save_locals_to_pkl(file_path):
    def func1(a, **kwargs):
        print('Первая функция')
        for k, v in kwargs.items(): print(f'Key={k} and Value={v}')
        return a

    def func2(a, **kwargs):
        print('Вторая функция')
        for k, v in kwargs.items(): print(f'Key={k} and Value={v}')
        return a

    def func3(a, **kwargs):
        print('Третья функция')
        for k, v in kwargs.items(): print(f'Key={k} and Value={v}')
        return a

    local_funcs = {'func1': func1, 'func2': func2, 'func3': func3}

    # Сохраняем словарь функций в файл с использованием dill
    with open(file_path, 'wb') as file:
        dill.dump(local_funcs, file)

def load_locals_from_pkl(file_path):
    with open(file_path, 'rb') as file:
        local_funcs = dill.load(file)
    # Так как мы возвращаем словарь, можем добавить функции в глобальную область видимости
    globals().update(local_funcs)
    # Пример вывода информации о функциях
    for func in local_funcs:
        print(f"Загруженная функция: {func}")

# Пример сохранения функций в дамп
save_locals_to_pkl('local_funcs.pkl')

# Загружаем и восстанавливаем локальные функции из дампа
load_locals_from_pkl('local_funcs.pkl')

a = 10

# Теперь функции доступны в глобальной области видимости
dic_flag = {'fun1': [True, {'name': 'Pankaj', 'age': 34}], 'fun2': [False, {'name': 'J', 'age': 14}], 'fun3': [True, {'name': 'Pank', 'age': 54}]}
func1(a,**dic_flag['fun1'][1])
func2(a,**dic_flag['fun2'][1])
func3(a,**dic_flag['fun3'][1])

или так:

def load_locals_from_pkl(file_path):
    with open(file_path, 'rb') as file:
        local_funcs = dill.load(file)

    # Пример вывода информации о функциях
    for func_name, func in local_funcs.items():
        print(f"Загруженная функция {func_name}: {func}")

    return local_funcs

# Пример сохранения
save_locals_to_pkl('local_funcs.pkl')

# Загружаем и восстанавливаем локальные функции
local_funcs = load_locals_from_pkl('local_funcs.pkl')

# Теперь функции доступны в глобальной области видимости
a = 10
dic_flag = {'fun1': [True, {'name': 'Pankaj', 'age': 34}], 'fun2': [False, {'name': 'J', 'age': 14}], 'fun3': [True, {'name': 'Pank', 'age': 54}]}
local_funcs['func1'](a, **dic_flag['fun1'][1])
local_funcs['func2'](a, **dic_flag['fun2'][1])
local_funcs['func3'](a, **dic_flag['fun3'][1])

А еще можно сохранять не словарь функций, а список функций:

# local_funcs = {'func1': func1, 'func2': func2, 'func3': func3}
# Создаем список с локальными функциями
local_funcs = [func1, func2, func3]

И вызывать нужные функции уже по индексу:

def load_locals_from_pkl(file_path):
    with open(file_path, 'rb') as file:
        local_funcs = dill.load(file)

    return local_funcs

local_funcs = load_locals_from_pkl('local_funcs.pkl')
local_funcs[0](a, **dic_flag['fun1'][1])
local_funcs[1](a, **dic_flag['fun2'][1])
local_funcs[2](a, **dic_flag['fun3'][1])

По итогу можно разбить функции на отдельные дампы и грузить только нужные из них.

import glob
import os

def load_locals_from_folder(folder_path):
    # Получаем список файлов в заданной папке с расширением .pkl
    pkl_files = glob.glob(os.path.join(folder_path, '*.pkl'))

    for pkl_file in pkl_files:
        with open(pkl_file, 'rb') as file:
            local_funcs = dill.load(file)
            globals().update(local_funcs)


Можно вообще уйти в дебри и использовать строковое представление для создания функций. Для этого можно использовать exec() или eval():

def add_exec_function(func_name):
    # Создаем строковое представление функции
    function_code = f'''
def {func_name}(a, *args):
    print('{func_name.capitalize()}')
    for v in args: print(f'Value={{v}}')
    return a
'''
    # Выполняем строковое представление кода
    exec(function_code, globals(), locals())

    # Возвращаем созданную функцию
    return locals()[func_name]

# Пример добавления новой функции
func4 = add_exec_function('func4')

# Теперь можно вызывать новую функцию
result = func4(42, 'value3', 56, 72)
print(result)

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

def add_evil_function(func_name):
    # Создаем строковое представление функции
    function_code = f'''
def {func_name}(a, **kwargs):
    print('{func_name.capitalize()}')
    for k, v in kwargs.items(): print(f'Key={{k}} and Value={{v}}')
    return a
'''
    # Выполняем строковое представление кода с использованием eval()
    eval(compile(function_code, '<string>', 'exec'))
    # Возвращаем созданную функцию
    return locals()[func_name]


# Пример добавления новой функции
func4 = add_evil_function('func4')

# Теперь можно вызывать новую функцию
result = func4(42, param1='value1', param2='value2')
print(result)

Запускать функции можно вообще как угодно:

def func1(a):
    print('Первая функция')
    return a

def func2(a):
    print('Вторая функция')
    return a

# Cловарь функций
functions = {'fun1': func1, 'fun2': func2}

# Настройки из словаря
dic_flag = {'fun1':[True, {'name': 'Pankaj', 'age': 34}], 'fun2': [False, {'name': 'kaj', 'age': 24}]}

# Запускаем функции хехе
results = [func(dic_flag[func_name][1]) if dic_flag[func_name][0] else None for func_name, func in functions.items()]
print(results)
→ Ссылка
Автор решения: Oopss

У тебя и так все работает, тебе лень проверить.

def get_function_name(func):
    return str(func.__name__)

def func1(a):
    print('Первая функция')
    return a

def func2(a):
    print('Вторая функция')
    return a
s=[func1, func2]
dic_flag = {'func1': True, 'func2': False}
x=10
for f in s:
   if dic_flag[get_function_name(f)]:
       f(x)

Первая функция
→ Ссылка
Автор решения: DeNRuDi

Как python-программисты мы хотим сделать код более лаконичным и красивым. Но самое главное правило - чтобы он был понятным и рабочим. Я сделал более универсальный способ, несмотря на то, что код может показаться сложным на первый взгляд. В целом, чтобы разобраться в этом коде - важно понимать основы ООП. Также чуть-чуть переработал вашу конфигурацию - у вас есть возможность нормально добавлять разные аргументы в функции, либо вообще их не добавлять. Код предусматривает также работу с методами в описанных классах в вашем файле, если они там конечно есть, то они также будут корректно распознаны, хоть это обёрнутый @staticmethod, хоть @classmethod, либо вообще ничего - в таком случае в качестве self я передаю класс, то есть самого себя, в моем контексте это будет func(class_ref).

Сам код:

from itertools import chain
import inspect


class FunctionRunner:

    def __init__(self, module, configuration: dict):
        self.module = module
        self.configuration = configuration

        module_name = self.module.__name__

        dev_functions = list(
            chain(
                inspect.getmembers(self.module, inspect.isfunction),
                inspect.getmembers(self.module, inspect.ismethod),
                inspect.getmembers(self.module, inspect.isclass),
            )
        )

        functions = {}
        for func in dev_functions:
            if func[1].__module__ == module_name:
                if inspect.isclass(func[1]):
                    dev_methods = list(
                        chain(
                            inspect.getmembers(func[1], inspect.isfunction),
                            inspect.getmembers(func[1], inspect.ismethod)
                        )
                    )
                    methods = {
                        method[0]: {'class': func[1], 'method': method[1]} for method in dev_methods
                        if func[1].__module__ == module_name
                    }
                    functions.update(**methods)
                else:
                    functions.update({func[0]: func[1]})

        self.func_for_run = functions

    def run(self):
        filtered_dict = {
            name: func for name, func in self.func_for_run.items()
            if name in self.configuration and self.configuration[name]['run']
        }

        for name, func in filtered_dict.items():

            class_ref = func
            if isinstance(func, dict):
                class_ref = func['class']
                func = func['method']

            args = self.configuration[name].get('args')
            func_args = inspect.getfullargspec(func).args
            if args is None:
                if 'self' in func_args:
                    func(class_ref)
                else:
                    func()
            elif isinstance(args, (list, tuple)):
                if 'self' in func_args:
                    args.insert(0, class_ref)
                func(*args)
            else:
                if 'self' in func_args:
                    args = [class_ref, args]
                    func(*args)
                else:
                    func(args)

Ваш файл с функциями function_module.py

def func1():
    print('Первая функция')
    return ''


def func2(a):
    print('Вторая функция')
    return a


def func3(a, b):
    print('Третья функция')
    return a, b


class Test:

    @staticmethod
    def method1(a):
        print('Первый метод')
        return a

    def method2(self, a, b):
        print('Второй метод')
        return a, b

    @classmethod
    def method3(cls):
        print('Третий метод')
        return ''

Ваш файл вызова логики app.py

import function_module # ваше имя файла, где находятся функции
from func_runner import FunctionRunner


dict_flag = {
    'func1': {'run': True, 'args': None},
    'func2': {'run': False, 'args': 10},
    'func3': {'run': True, 'args': [10, '']},
    'method1': {'run': True, 'args': [1]},
    'method2': {'run': False, 'args': [1, 2]},
    'method3': {'run': True, 'args': None},
}
fr = FunctionRunner(function_module, configuration=dict_flag)
fr.run()

Вывод:

Первая функция
Третья функция
Первый метод
Третий метод

Преимущества:

  1. Подключить мой класс для вас будет очень просто, так как все что вы делаете, так это в конструктор передаете сам импорт вашего модуля, и конфигурацию.

  2. Вам не нужно заморачиваться с логикой, которая написана внутри моего класса в отличие от других приведенных ответов где логику придется применять каждый раз для разных вызовов. Плюс там много ненужных вариантов, которые может быть написал вообще chatgpt (извините, если кого-то обидел :) ).

  3. Мой класс вызывает только те функции, которые вы сами явно прописали в модуле, исключая импорты. Это означает, что вам не нужно руками прописывать все функции и методы, получать ссылки на них в словарь - класс сделает все сам.

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

В целом я вообще не рекомендую использовать такой подход в программировании, так как логика выглядит достаточно запутанной и другим программистам будет сложно разобраться, где и какая функция вызывается. Не рекомендую и вашу идею с получением с внешнего источника и запуск таким образом функций. Лучше всего сделать какой-нибудь главный метод, который принимает аргументы, и уже на основе них запускать ваши функции. В качестве альтернативы посмотрите на match-case, который добавили в python 3.10 он может показаться вам полезным.

UPDATE:

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

class FunctionRunner:

    def __init__(self, module, configuration: dict):
        self.module = module
        self.configuration = configuration

        module_name = self.module.__name__

        dev_functions = list(
            chain(
                inspect.getmembers(self.module, inspect.isfunction),
                inspect.getmembers(self.module, inspect.ismethod),
            )
        )
        functions = {}
        for func in dev_functions:
            if func[1].__module__ == module_name:
                functions.update({func[0]: func[1]})

        self.func_for_run = functions

    def run(self, arguments: dict):
        filtered_dict = {
            name: func for name, func in self.func_for_run.items()
            if name in self.configuration and self.configuration[name]
        }

        for name, func in filtered_dict.items():
            func_args = arguments.get(name)
            if func_args is None:
                func()
            elif isinstance(func_args, (list, tuple)):
                func(*func_args)
            else:
                func(func_args)


dict_flag = {'func1': True, 'func2': True, 'func3': True}
func_arguments = {'func1': None, 'func2': 10, 'func3': [5, 10]}
# если аргументов нет для функций, просто отправьте пустой словарь

fr = FunctionRunner(function_module, configuration=dict_flag)
fr.run(arguments=func_arguments)

В целом задача оказалось интересной, был рад решить ее и помочь вам. Желаю успехов

→ Ссылка