Эффективный способ частичного применения в Python?

Я ищу способ частичного применения функций в python, которые просты для понимания, удобочитаемы, повторно используются и как можно меньше ошибок подвержены ошибкам кодировщика. Больше всего я хочу, чтобы стиль был максимально производительным — меньше кадров в стеке — это хорошо, а также желательно меньше памяти для частично применяемых функций. Я рассмотрел 4 стиля и написал примеры ниже:

import functools

def multiplier(m):
    def inner(x):
        return m * x

    return inner

def divide(n,d):
    return n/d

def divider(d):
    return functools.partial(divide,d=d)

times2 = multiplier(2)
print(times2(3))  # 6

by2 = divider(2)
print(by2(6)) # 3.0

by3 = functools.partial(divide,d=3)
print(by3(9)) # 3.0

by4 = lambda n: divide(n,4)
print(by4(12)) # 3.0

Мой анализ их:

times2 — это вложенная вещь. Я предполагаю, что python делает закрытие с привязкой m, и все хорошо. Код читабелен (я думаю) и прост для понимания. Никаких внешних библиотек. Это стиль, который я использую сегодня.

by2 имеет явную именованную функцию, которая упрощает работу пользователя. Он использует functools, поэтому он дает вам дополнительный импорт. Мне в какой-то степени нравится этот стиль, поскольку он прозрачен, и я могу использовать divide другими способами, если захочу. Сравните это с inner, который недоступен.

by3 похож на by2, но вынуждает читателя кода чувствовать себя комфортно с functools.partial, так как он сразу бросается в глаза. что мне меньше всего нравится, так это то, что PyCharm не может дать мне всплывающие подсказки о том, какими должны быть аргументы для functools.partial, поскольку они фактически являются аргументами для by3. Я сам должен знать подпись divide каждый раз, когда определяю какое-то новое частичное приложение.

by4 набирать просто, так как я могу использовать автозаполнение. Ему не нужен импорт functools. Я думаю, что это выглядит не питоническим. Кроме того, я всегда чувствую себя некомфортно из-за области видимости переменных/замыканий с лямбда-выражениями, работающими в python. Никогда не уверен, как это себя ведет....

В чем логическая разница между стилями и как это влияет на память и ЦП?


person LudvigH    schedule 06.09.2019    source источник
comment
Это довольно широко. Я не думаю, что есть какой-либо консенсус по этому поводу, поэтому используйте то, что кажется правильным. С точки зрения функциональности они в основном эквивалентны. С точки зрения скорости, вы можете проверить это самостоятельно с помощью timeit (не думаю, что будет существенная разница)   -  person Yevhen Kuzmovych    schedule 06.09.2019
comment
@JohnColeman Я понимаю это. Я перепишу, чтобы сузить его!   -  person LudvigH    schedule 06.09.2019
comment
Сравнения могут иметь более прямое значение, если у вас есть одна и та же функция (скажем, та, которая переводит x в 2x), определенная четырьмя разными способами, а не четыре разные функции, определенные четырьмя разными способами.   -  person John Coleman    schedule 06.09.2019
comment
Также может быть интересно: декоратор каррирования в Python   -  person tobias_k    schedule 06.09.2019
comment
@JohnColeman спасибо, что указали на важность выполнения одних и тех же математических расчетов в каждом случае, чтобы сделать их сопоставимыми. Когда я начал переписывать вопрос, я понял, что ваш ответ ниже действительно хорошо отвечает на мой вопрос. Кроме того, это дало мне инструменты, которые мне были нужны, чтобы решить вопрос самостоятельно на моей машине и с функциями в контексте. Благодарю вас!   -  person LudvigH    schedule 06.09.2019


Ответы (2)


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

import functools,timeit

def multiplier(m):
    def inner(x):
        return m * x

    return inner

def mult(x,m):
    return m*x

def multer(m):
    return functools.partial(mult,m=m)

f1 = multiplier(2)
f2 = multer(2)
f3 = functools.partial(mult,m=2)
f4 = lambda x: mult(x,2)

print(timeit.timeit('f1(10)',setup = 'from __main__ import f1'))
print(timeit.timeit('f2(10)',setup = 'from __main__ import f2'))
print(timeit.timeit('f3(10)',setup = 'from __main__ import f3'))
print(timeit.timeit('f4(10)',setup = 'from __main__ import f4'))

Типичный вывод (на моей машине):

0.08207898699999999
0.19439769299999998
0.20093803199999993
0.1442435820000001

Два подхода functools.partial идентичны (поскольку один из них является просто оберткой для другого), первый в два раза быстрее, а последний находится где-то посередине (но ближе к первому). Использование functools вместо простого закрытия сопряжено с явными накладными расходами. Поскольку подход с закрытием, возможно, также более читаем (и более гибок, чем лямбда, которая плохо распространяется на более сложные функции), я бы просто пошел с ним.

person John Coleman    schedule 06.09.2019

Технически вам не хватает еще одного параметра, так как operator.mul делает то же самое, что вы хотите сделать, и вы можете просто использовать functools.partial, чтобы получить первый аргумент по умолчанию, не изобретая велосипед.

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

from timeit import timeit
from functools import partial
from sys import getsizeof
from operator import mul

def multiplier(m):
    def inner(x):
        return m * x
    return inner

def mult(x,m):
    return m*x

def multer(m):
    return partial(mult,m=m)

f1 = multiplier(2)
f2 = multer(2)
f3 = partial(mult,m=2)
f4 = lambda n: mult(n,2)
f5 = partial(mul, 2)
from_main = 'from __main__ import {}'.format

print(timeit('f1(10)', from_main('f1')), getsizeof(f1))
print(timeit('f2(10)', from_main('f2')), getsizeof(f2))
print(timeit('f3(10)', from_main('f3')), getsizeof(f3))
print(timeit('f4(10)', from_main('f4')), getsizeof(f4))
print(timeit('f5(10)', from_main('f5')), getsizeof(f5))

Выход

0.5278953390006791 144
1.0804575479996856 96
1.0762036349988193 96
0.9348237040030654 144
0.3904160970050725 96

Это должно ответить на ваш вопрос об использовании памяти и скорости.

person Jab    schedule 06.09.2019
comment
Не уверен, что понял ваш ответ. Вы говорите, что можете использовать operator.mul, что несколько не по делу, поскольку функция умножения, вероятно, является просто простым примером для тестирования, а не тем, что на самом деле будет использовать OP. Тогда вы говорите, что для operator.mul лучше всего подходит partial, и, следовательно, partial является лучшей (во всех случаях?), даже если она медленнее, чем функция def? - person tobias_k; 06.09.2019
comment
Где я сказал для всех случаев? В этом случае использование partial и operator.mul является самым быстрым, поскольку все, что пытается сделать OP, - это добавить аргумент по умолчанию к другому, но не самый надежный, поскольку частичное не может принимать позиционные аргументы. Хотя это вариант, который можно рассмотреть. Я только заявляю, что operator.mul ЯВЛЯЕТСЯ другим вариантом и быстрее, чем делать свой собственный. - person Jab; 06.09.2019