«Предварительная обработка» функции Python, чтобы избежать избыточной оценки условной логики

Я пишу код Python, который включает функции с двумя наборами параметров. Первый набор параметров будет меняться каждый раз при вызове функции. Второй набор параметров будет одинаковым для больших групп вызовов функций (например, они меняются каждые 10 000 вычислений функции). Я надеюсь написать эту функцию таким образом, чтобы условная логика выполнялась только один раз при изменении значений второго набора параметров. Я включил некоторый код, который, по моему мнению, может работать, но после некоторого тестирования с модулем времени улучшение кажется очень незначительным. Действительно ли этот код делает то, что я описываю? А если нет, есть ли способ сделать это на Python?

def preprocessor(parameter_array):
    # example parameters
    condition = parameter_array[0]
    index = parameter_array[1]

    def f(x_array):
        result = 0
        if (condition):
            result += x_array[index] ** 2
        else:
            result += x_array[index] - 5
        # ...
        return result

    return f

# define some test parameters
test_parameters = [True, 1]

# run the function 100000 times with these parameters
function = preprocessor(test_parameters)

sum = 0
for i in range(100000):
    sum += function([i, i])

print(sum)

Я надеюсь, что записанный таким образом оператор if будет оцениваться только всякий раз, когда я вызываю функцию preprocessor. Таким образом, это ускоряет вычисление цикла for, в котором изменяются только значения x. Для этого потребовалось около 0,024 секунды, а для запуска контрольного элемента потребовалось около 0,028 секунды. Я также включил код, который использовал для элемента управления ниже (для удобства чтения все без модуля времени).

def f(x_array, parameter_array):
    # example parameters
    condition = parameter_array[0]
    index = parameter_array[1]

    result = 0
    if (condition):
        result += x_array[index] ** 2
    else:
        result += x_array[index] - 5
    # ...
    return result

# define some test parameters
test_parameters = [True, 1]

# run the function 100000 times without using the preprocessed function
sum = 0
for i in range(100000):
    sum += f([i, i], test_parameters)

print(sum)

if (condition): находится внутри функции f, возвращаемой preprocessor(), поэтому он оценивается при каждом вызове функции — я не понимаю, как вы думали, что это что-то ускорит. Вы могли бы избавиться от условного условия, вернув preprocessor() одну из двух реализаций f, каждая из которых имеет разные вычисления result, но потенциальный выигрыш в скорости будет довольно небольшим.

jasonharper 25.06.2024 20:36

Вы читали о декораторах Python?

Chris 25.06.2024 20:40
stackoverflow.com/questions/815110/… может быть полезно
Gonzalo Odiard 25.06.2024 21:57

@jasonharper, понятно. Вот чего я боялся. Проблема возврата одной из двух реализаций заключается в том, что приведенный мной код — всего лишь игрушечный пример, объясняющий проблему. В моем реальном случае использования у меня есть сотни параметров, поэтому возможные перестановки параметров становятся слишком большими, чтобы это было практично. Спасибо за помощь.

Jack MacArthur 25.06.2024 22:01
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
4
70
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Отвечать:

Да, он делает то, что вы думаете. Самый простой способ узнать это — просто добавить несколько операторов печати.

def initialize(initialState):
    state = initialState
    print(f"I am executed on initialization: {state}")
    def f(increment): 
        nonlocal state
        print(f"I am incrementing {state} by {increment}")
        state = state + increment

    return f

fun = initialize(10)
fun(5)
fun(20)

выход

I am executed on initialization: 10
I am incrementing 10 by 5
I am incrementing 15 by 20

В стороне:

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

Здесь я сравнил ваш подход с двумя разными упрощениями, и оба обеспечивают ускорение. Python — действительно странный язык. Каждый известный мне компилируемый язык способен исключить инструкции ветвления при использовании тернарного кода. Я ожидаю, что троичный подход будет самым быстрым.

import timeit

def approach1(): 
    def preprocessor (parameter_array):
        condition = parameter_array[0]
        index = parameter_array[1]
        return lambda x_array: x_array[index] ** 2 if condition else x_array[index] - 5
    test_parameters = [True, 1]
    function = preprocessor(test_parameters)
    sum = 0
    for i in range(100000):
        sum += function([i, i])
    
def approach2():
    def preprocessor (parameter_array):
        condition = parameter_array[0]
        index = parameter_array[1]
        def f (x_array):
            result = 0
            if (condition):
                result += x_array[index] ** 2
            else:
                result += x_array[index] - 5
            return result
        return f
    
    test_parameters = [True, 1]
    function = preprocessor(test_parameters)
    sum = 0
    for i in range(100000):
        sum += function([i, i])

def approach3():
    def preprocessor (parameter_array):
        condition = parameter_array[0]
        index = parameter_array[1]
        def f (x_array):
            if (condition):
                return x_array[index] ** 2
            return x_array[index] - 5
        return f
    test_parameters = [True, 1]
    function = preprocessor(test_parameters)
    sum = 0
    for i in range(100000):
        sum += function([i, i])

n = 100
print(timeit.timeit(approach1, number=n))
print(timeit.timeit(approach2, number=n))
print(timeit.timeit(approach3, number=n))

выход

0.8117037120027817
0.9467067930017947
0.8014805819984758

P.S.

Хотя эти типы оптимизации могут быть интересными и помочь вам узнать о характеристиках производительности языка, гораздо лучше потратить время на оптимизацию таких вещей, как используемые вами алгоритмы. Если вам действительно нужна производительность, Python не для вас.

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

Jack MacArthur 25.06.2024 22:10

Проблемы с производительностью весьма и весьма специфичны. Трудно давать какие-либо рекомендации, когда я не знаю, что он делает. Обычно я следую двум подходам: 1) кеширование/запоминание - то, что вы пытаетесь сделать 2) сокращение №2 - это сокращение не только инструкций для ЦП, но и выделения памяти. Посмотрите, не выделяете ли вы память в циклах без необходимости.

zelarian 25.06.2024 22:17

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

zelarian 25.06.2024 22:18

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

Jack MacArthur 25.06.2024 22:23

Я не знаком. Это что-то итеративное? Это означает, что вычисление текущего состояния зависит от предыдущего состояния? Или это что-то сделано на матрице?

zelarian 25.06.2024 22:28

Он является итеративным в том смысле, что входной массив x_array будет зависеть от значений всех предыдущих вызовов функций.

Jack MacArthur 25.06.2024 22:39

Я собирался порекомендовать что-нибудь вроде Pandas/Numpy или Polars, но не думаю, что они в этом случае помогут. Вы можете попробовать numba.pydata.org и посмотреть, поможет ли это вообще, но им действительно нужны массивы Numpy. Единственное улучшение, в котором вы можете увидеть улучшение (если ваш список параметров однороден), — это замена списка массивом: docs.python.org/3/library/array.html. Списки необходимо просматривать, тогда как массивы можно напрямую индексировать.

zelarian 25.06.2024 22:47

Другие вопросы по теме