web-dev-qa-db-fra.com

Composition de fonctions en python

J'ai un tableau de fonctions et j'essaie de produire une fonction qui consiste à composer les éléments de mon tableau. Mon approche est la suivante:

def compose(list):
    if len(list) == 1:
        return lambda x:list[0](x)
    list.reverse()
    final=lambda x:x
    for f in list:
        final=lambda x:f(final(x))
    return final

Cette méthode ne semble pas fonctionner, une aide sera appréciée.

(J'inverse la liste parce que c'est l'ordre de composition que je veux que les fonctions soient)

18
Starless

Cela ne fonctionne pas car toutes les fonctions anonymes que vous créez dans la boucle font référence à la même variable de boucle et partagent donc sa valeur finale.

Pour remédier rapidement à ce problème, vous pouvez remplacer l’affectation par:

final = lambda x, f=f, final=final: f(final(x))

Ou, vous pouvez retourner le lambda à partir d'une fonction:

def wrap(accum, f):
    return lambda x: f(accum(x))
...
final = wrap(final, f)

Pour comprendre ce qui se passe, essayez cette expérience:

>>> l = [lambda: n for n in xrange(10)]
>>> [f() for f in l]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]

Ce résultat surprend de nombreuses personnes, qui s’attendent à ce que le résultat soit [0, 1, 2, ...]. Cependant, tous les lambdas pointent vers la même variable n et font tous référence à sa valeur finale, qui est 9. Dans votre cas, toutes les versions de final qui sont supposées imbriquer finissent par se référer à la même f et, pire encore, à la même final.

Le sujet de lambdas et des boucles for en Python a été déjà couvert sur SO .

10
user4815162342

L’approche la plus simple serait d’abord d’écrire une composition de 2 fonctions:

def compose2(f, g):
    return lambda *a, **kw: f(g(*a, **kw))

Et utilisez ensuite reduce pour composer plus de fonctions:

def compose(*fs):
    return reduce(compose2, fs)

Ou vous pouvez utiliser une bibliothèque , qui contient déjà la fonction compose .

25
Suor
def compose (*functions):
    def inner(arg):
        for f in reversed(functions):
            arg = f(arg)
        return arg
    return inner

Exemple:

>>> def square (x):
        return x ** 2
>>> def increment (x):
        return x + 1
>>> def half (x):
        return x / 2

>>> composed = compose(square, increment, half) # square(increment(half(x)))
>>> composed(5) # square(increment(half(5))) = square(increment(2.5)) = square(3.5) = 12,25
12.25
15
poke

Implémentation récursive

Voici une implémentation récursive assez élégante, qui utilise les fonctionnalités de Python 3 pour plus de clarté:

def strict_compose(*funcs):
    *funcs, penultimate, last = funcs
    if funcs:
        penultimate = strict_compose(*funcs, penultimate)
    return lambda *args, **kwargs: penultimate(last(*args, **kwargs))

Version compatible Python 2:

def strict_compose2(*funcs):
    if len(funcs) > 2:
        penultimate = strict_compose2(*funcs[:-1])
    else:
        penultimate = funcs[-2]
    return lambda *args, **kwargs: penultimate(funcs[-1](*args, **kwargs))

Ceci est une version antérieure qui utilise une évaluation paresseuse de la récursivité:

def lazy_recursive_compose(*funcs):
    def inner(*args, _funcs=funcs, **kwargs):
        if len(_funcs) > 1:
            return inner(_funcs[-1](*args, **kwargs), _funcs=_funcs[:-1])
        else:
            return _funcs[0](*args, **kwargs)
    return inner

Les deux semblent faire un nouveau tuple et dict d'arguments chaque appel récursif.

Comparaison de toutes les suggestions:

Testons certaines de ces implémentations et déterminons laquelle est la plus performante, en premier lieu quelques fonctions à un seul argument (Merci poke):

def square(x):
    return x ** 2

def increment(x):
    return x + 1

def half(x):
    return x / 2

Voici nos implémentations, je soupçonne que ma version itérative est la deuxième plus efficace (la composition manuelle sera naturellement la plus rapide), mais cela peut être dû en partie au fait qu'elle évite la difficulté de passer un certain nombre d'arguments ou d'arguments de mots-clés entre les fonctions - dans la plupart des cas nous ne verrons que le seul argument trivial transmis. 

from functools import reduce

def strict_recursive_compose(*funcs):
    *funcs, penultimate, last = funcs
    if funcs:
        penultimate = strict_recursive_compose(*funcs, penultimate)
    return lambda *args, **kwargs: penultimate(last(*args, **kwargs))

def strict_recursive_compose2(*funcs):
    if len(funcs) > 2:
        penultimate = strict_recursive_compose2(*funcs[:-1])
    else:
        penultimate = funcs[-2]
    return lambda *args, **kwargs: penultimate(funcs[-1](*args, **kwargs))

def lazy_recursive_compose(*funcs):
    def inner(*args, _funcs=funcs, **kwargs):
        if len(_funcs) > 1:
            return inner(_funcs[-1](*args, **kwargs), _funcs=_funcs[:-1])
        else:
            return _funcs[0](*args, **kwargs)
    return inner

def iterative_compose(*functions):
    """my implementation, only accepts one argument."""
    def inner(arg):
        for f in reversed(functions):
            arg = f(arg)
        return arg
    return inner

def _compose2(f, g):
    return lambda *a, **kw: f(g(*a, **kw))

def reduce_compose1(*fs):
    return reduce(_compose2, fs)

def reduce_compose2(*funcs):
    """bug fixed - added reversed()"""
    return lambda x: reduce(lambda acc, f: f(acc), reversed(funcs), x)

Et pour tester ceux-ci:

import timeit

def manual_compose(n):
    return square(increment(half(n)))

composes = (strict_recursive_compose, strict_recursive_compose2, 
            lazy_recursive_compose, iterative_compose, 
            reduce_compose1, reduce_compose2)

print('manual compose', min(timeit.repeat(lambda: manual_compose(5))), manual_compose(5))
for compose in composes:
    fn = compose(square, increment, half)
    result = min(timeit.repeat(lambda: fn(5)))
    print(compose.__name__, result, fn(5))

Résultats

Et nous obtenons le résultat suivant (même magnitude et proportion en Python 2 et 3):

manual compose 0.4963762479601428 12.25
strict_recursive_compose 0.6564744340721518 12.25
strict_recursive_compose2 0.7216697579715401 12.25
lazy_recursive_compose 1.260614730999805 12.25
iterative_compose 0.614982972969301 12.25
reduce_compose1 0.6768529079854488 12.25
reduce_compose2 0.9890829260693863 12.25

Et mes attentes ont été confirmées: le plus rapide est bien sûr la composition de fonctions manuelle suivie de la mise en œuvre itérative. La version récursive paresseuse est beaucoup plus lente, ce qui est probablement dû au fait qu'un nouveau cadre de pile est créé par chaque appel de fonction et qu'un nouveau Tuple de fonctions est créé pour chaque fonction.

Pour une comparaison meilleure et peut-être plus réaliste, si vous supprimez **kwargs et remplacez *args par arg dans les fonctions, celles qui les utilisent seront plus performantes et nous pourrons mieux comparer des pommes avec des pommes. Ici, en dehors de la composition manuelle, reduction_compose1 gagne suivi du strict_recursive_compose:

manual compose 0.443808660027571 12.25
strict_recursive_compose 0.5409777010791004 12.25
strict_recursive_compose2 0.5698030130006373 12.25
lazy_recursive_compose 1.0381018499610946 12.25
iterative_compose 0.619289995986037 12.25
reduce_compose1 0.49532539502251893 12.25
reduce_compose2 0.9633988010464236 12.25

Fonctionne avec un seul argument:

def strict_recursive_compose(*funcs):
    *funcs, penultimate, last = funcs
    if funcs:
        penultimate = strict_recursive_compose(*funcs, penultimate)
    return lambda arg: penultimate(last(arg))

def strict_recursive_compose2(*funcs):
    if len(funcs) > 2:
        penultimate = strict_recursive_compose2(*funcs[:-1])
    else:
        penultimate = funcs[-2]
    return lambda arg: penultimate(funcs[-1](arg))

def lazy_recursive_compose(*funcs):
    def inner(arg, _funcs=funcs):
        if len(_funcs) > 1:
            return inner(_funcs[-1](arg), _funcs=_funcs[:-1])
        else:
            return _funcs[0](arg)
    return inner

def iterative_compose(*functions):
    """my implementation, only accepts one argument."""
    def inner(arg):
        for f in reversed(functions):
            arg = f(arg)
        return arg
    return inner

def _compose2(f, g):
    return lambda arg: f(g(arg))

def reduce_compose1(*fs):
    return reduce(_compose2, fs)

def reduce_compose2(*funcs):
    """bug fixed - added reversed()"""
    return lambda x: reduce(lambda acc, f: f(acc), reversed(funcs), x)
8
Aaron Hall

Bon mot:

compose = lambda *F: reduce(lambda f, g: lambda x: f(g(x)), F)

Exemple d'utilisation:

f1 = lambda x: x+3
f2 = lambda x: x*2
f3 = lambda x: x-1
g = compose(f1, f2, f3)
assert(g(7) == 15)
6
Brett

Vous pouvez également créer un tableau de fonctions et utiliser

def f1(x): return x+1
def f2(x): return x+2
def f3(x): return x+3

x = 5

# Will print f3(f2(f1(x)))
print reduce(lambda acc, x: x(acc), [f1, f2, f3], x)

# As a function:
def compose(*funcs):
    return lambda x: reduce(lambda acc, f: f(acc), funcs, x)

f = compose(f1, f2, f3)
3
Imanol Luengo

La réponse de Poke est bonne, mais vous pouvez également utiliser le package functional qui est fourni avec une méthode de composition. 

2
Marcin

pip install funcoperators est une autre bibliothèque à implémenter qui permet la notation infixe:

from funcoperators import compose

# display = lambda x: hex(ord(list(x)))
display = hex *compose* ord *compose* list

# also works as a function
display = compose(hex, ord, list)

pip install funcoperators https://pypi.org/project/funcoperators/

Disclaimer: je suis le créateur du module

1
Robert Vanden Eynde

L'implémentation la plus fiable que j'ai trouvée se trouve dans la bibliothèque tierce partie toolz . La fonction compose de cette bibliothèque traite également docstring pour la composition des fonctions.

Le code source est disponible gratuitement. Vous trouverez ci-dessous un exemple simple d'utilisation.

from toolz import compose

def f(x):
    return x+1

def g(x):
    return x*2

def h(x):
    return x+3

res = compose(f, g, h)(5)  # 17
1
jpp

Ceci est ma version

def compose(*fargs):
    def inner(arg):
        if not arg:
            raise ValueError("Invalid argument")
        if not all([callable(f) for f in fargs]):
            raise TypeError("Function is not callable")
        return reduce(lambda arg, func: func(arg), fargs, arg)
    return inner

Un exemple d'utilisation

def calcMean(iterable):
    return sum(iterable) / len(iterable)


def formatMean(mean):
    return round(float(mean), 2)


def adder(val, value):
    return val + value


def isEven(val):
    return val % 2 == 0

if __== '__main__':
    # Ex1

    Rand_range = [random.randint(0, 10000) for x in range(0, 10000)]

    isRandIntEven = compose(calcMean, formatMean,
                            partial(adder, value=0), math.floor.__call__, isEven)

    print(isRandIntEven(Rand_range))
0
CasualCoder3