Избегайте избыточности кода/вычислительных издержек с функцией, извлеченной из фрагмента кода внутри цикла

У меня есть цикл, который циклически повторяет дни и дискретные временные интервалы финансового временного ряда, и внутри него находится некоторый код для моей стратегии. Я извлек этот внутренний код в отдельную функцию, чтобы иметь возможность использовать его в определенных ситуациях для тестирования, и только чуть позже обнаружил, что эта операция сильно увеличила общее время вычислений этого цикла. Это имеет смысл, длинная сигнатура функции, вызов (память стека?) Все это должно приводить к некоторым накладным расходам.

# Fast, but cannot use my strategy for specific instances:
for d in data:
    # my strategy code

# Flexible but really slow
def my_strategy(**params):
    # my strategy code
f = partial(my_strategy, **params)
for d in data:
    f(d)

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

Я экспериментировал с inspect.getsource(my_function) и обернул копию внешней функции, чтобы затем использовать eval, но через 5 минут я почувствовал себя идиотом, это совсем не так.

Какова наилучшая практика для этого? Если я дублирую код, я должен убедиться, что если я изменю одну версию, другая всегда будет синхронизирована. Мне это не нравится. Я хотел бы отразить это эффективно.

Как вы могли подумать, одним из вариантов может быть: оставить код там, где он есть, внутри цикла, и обернуть ваш конкретный случай массивом и передать его в цикл, когда вам это нужно. Ну, я не могу этого сделать, главным образом потому, что мне нужно иметь временной контроль над выполнением внутреннего кода (я переношу проверенную стратегию в режим реального времени). Здесь я просто ищу какой-нибудь быстрый трюк для элегантного отражения кода, если он есть.

--- РЕДАКТИРОВАТЬ ---

Итак, чтобы проверить это дальше, я применил тот ужасный хак, о котором я упоминал, и заставил его работать с exec. Благодаря этому теперь я могу гарантировать, что используемый код точно такой же. Однако наблюдаемая разница во времени одинакова. Цикл, содержащий код, занимает примерно ПОЛОВИНУ времени, затрачиваемого циклом с вызовом функции.

# horrible hack, please forgive me
def extract_strategy_function():
    # Start
    source_code = inspect.getsource(fastest_compute_trades)
    # Cutting the code snippet
    source_code = source_code.split('[safe_start - 1:]:', 1)[1].rsplit("if s['pos_open']:", 1)[0]
    # Needs re-indentation so the parser doesn't complain
    source_code = reindent(source_code, -2)
    function_name = 'single_apply_strategy'  # internal, no-one will see this anyway
    function_string = f"""
    def {function_name}(
            # INDICATORS (non mutable)
            i_time, close_price, r2, ma1, ma2, rsi, upper, lower,
            # STATE VARIABLES, TX HISTORY (mutable, 2 dictionaries and 1 list of dicts)
            s, gs, transactions,
            # CONSTANT PARAMETERS (non mutable)
            min_r2, macd_threshold, rsi_bound, waiting_slots, stop_loss,
            take_profit, trail_profit, double_down, dd_qty, commission_fee,
            # Day ID, useful when back-testing/grid-searching, left at the end so it can default to 0
            i_day=0
    ):
    {source_code}
    """.strip()
    # Evaluate it
    exec(function_string)  # Now in locals
    return locals()[function_name]
    
def slow(...):
    
    ...
    
    apply_strategy = partial(extract_strategy_function(), 
        **params, commission_fee=commission_fee, rsi_bound=rsi_bound)
   
    for i_day, day in enumerate(data):
        for i_time, close_price, r2, ma1, ma2, rsi, upper, lower, in list(enumerate(day))[safe_start - 1:]:
            apply_strategy(i_time, close_price, r2, ma1, ma2, rsi, upper, lower, 
                day_state, global_state, transactions, i_day=i_day)

        if day_state['pos_open']:
            ...
    
    ...
    
def fast(..., 
    #some extra parameters are now unpacked here, 
    #since all code is here they are now needed here
):
    ...
    
    for i_day, day in enumerate(data):
        for i_time, close_price, r2, ma1, ma2, rsi, upper, lower, in list(enumerate(day))[safe_start - 1:]:
            # actual code contained in single_apply_strategy written here, the whole strategy (one timestep)

        if day_state['pos_open']:
            ...
            
    ...

Какие-либо предложения?

p.s. Излишне говорить, что выход двух установок точно такой же.


person quan2m    schedule 08.02.2021    source источник
comment
Пожалуйста, опубликуйте свой код - прочитайте Как спросить и предоставьте минимально воспроизводимый пример.   -  person ack    schedule 08.02.2021
comment
Это справедливо для любого кода, я говорю вообще   -  person quan2m    schedule 08.02.2021


Ответы (1)


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

В случае, если вам нужна такая производительность, Python, вероятно, не лучший выбор, в таком случае я бы рекомендовал C++. Вы запрашиваете языковую функцию, которая называется встроенной функцией: https://www.geeksforgeeks.org/inline-functions-cpp/

Дополнительная информация о Python и встраивании: Python эквивалентен встроенным функциям или макросам

В любом случае, я не думаю, что ваши усилия в этом направлении принесут какие-либо улучшения.

person Roman Pavelka    schedule 08.02.2021
comment
Итак, чтобы проверить это дальше, я применил тот ужасный хак, о котором я упоминал, и заставил его работать с exec. Благодаря этому теперь я могу гарантировать, что используемый код точно такой же. Однако наблюдаемая разница во времени одинакова. Цикл, содержащий код, занимает примерно ПОЛОВИНУ времени, затрачиваемого циклом с вызовом функции. Как это вообще возможно? Возможно, это связано с длинной подписью. Множество изменяемых и неизменяемых параметров. Все равно спасибо за ответ, сейчас поищу - person quan2m; 08.02.2021
comment
@quan2m Это интересно, не могли бы вы поделиться подписью? - person Roman Pavelka; 08.02.2021
comment
shareablecode.com/snippets/war-against-redundancy-ydRS-9GDp Это работает? В качестве альтернативы, пожалуйста, скажите мне лучший способ поделиться им. Если вам нужно что-то еще, просто спросите - person quan2m; 08.02.2021
comment
Я отредактировал этот пост и добавил его сюда с некоторыми исправлениями.. @Roman - person quan2m; 08.02.2021
comment
@quan2m Может быть, import dis, тогда dis.dis(slow)и dis.dis(fast) дают некоторое представление. - person ack; 10.02.2021