2
votes

Quelques informations sur les fonctions du décorateur

J'essaie de comprendre un exemple de séquence de Fibonacci utilisant un décorateur pour stocker des valeurs de nombres déjà calculés. Par exemple, fib(5) serait calculé, et quand nous sommes arrivés à fib(6) , il ne calculerait plus fib(5) ... Je comprends un peu les décorateurs, mais certaines choses me déroutent. J'ai quelques questions sur le code ci-dessous.

from functools import wraps
def dec(func):
    values = {}
    @wraps(func)
    def wrap(*args):
        if args not in values:
            values[args] = func(*args)
        return values[args]
    return wrap

@dec
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)
  1. Pourquoi les *args utilisés dans wrap() ? N'est-il pas censé prendre simplement un nombre n et vérifier si la valeur de celui-ci est dans le dictionnaire? Pourquoi les args sont-ils appelés à certains endroits avec, et dans certains sans * ?
  2. Que se passe-t-il lorsque la fonction fib est appelée récursivement (comment se comporte la fonction décoratrice). J'ai d'abord pensé qu'il entre dans cette fonction à chaque récursivité, mais cela ne peut pas être correct car le dictionnaire de valeurs se réinitialiserait? Alors, est-ce qu'il n'entre alors que la fonction wrap() ?
  3. Pourquoi retourne-t-il un emballage à la fin?


0 commentaires

3 Réponses :


0
votes

Qu'est-ce que *args

*args correspond à tous les arguments restants sous forme de tuple. Prenons cet exemple:

def f(*args):
    print(args)
f('a', 'b')
# output: ('a', 'b')

Dans ce cas, il est utilisé pour appeler la fonction interne avec exactement le même argument, quel qu'il soit. Vous pouvez également utiliser une étoile double pour faire correspondre les arguments de mot-clé, actuellement seuls les arguments de position fonctionnent.

Que se passe-t-il lorsque le fib est appelé récursivement

Lors de la décoration d'une fonction avec @ , la référence est immédiatement écrasée. Lorsque fib appelle fib() intérieur de lui-même, il recherche d'abord dans la portée locale une variable portant ce nom. Puisqu'il n'y en a pas, il cherchera dans la prochaine portée, qui est la portée globale dans ce cas. Là, il trouve une variable nommée fib , qui est en fait assignée à la fonction wrap de votre décorateur, avec un "contexte" du fib original étant func .

Recherchez les fermetures pour plus d'informations sur la façon dont cela fonctionne.

Pourquoi le décorateur retourne-t-il un wrap à la fin?

Un décorateur remplace essentiellement une fonction par une autre. Il appelle la variable après le @ comme une fonction, puis remplace la nouvelle fonction définie par def par le résultat de cet appel. Dans ce cas, vous souhaitez le remplacer par wrap , une nouvelle fonction qui peut ou non appeler l'ancienne fonction.

Si vous ne retournez rien, la variable fib sera simplement définie sur None (la valeur de retour par défaut) et vous ne pouvez pas appeler None .


0 commentaires

1
votes

1- Vous avez tout à fait raison. Il n'y a pas besoin de "*" car vous ne vérifiez que la valeur qui est passée à la fonction. Alors appelez-le simplement "n".

2- Voyons d'abord ce qu'est l'étiquette "fib" après avoir utilisé "@dec" dessus? En fait, c'est maintenant votre fonction intérieure à l'intérieur de votre décorateur (je veux dire la fonction «wrap»). Pourquoi ? parce que @dec fait effectivement ceci:

fib = dec(fib)

Alors on appelle le décorateur "déc", qu'est-ce que ça revient? Fonction "wrap". Qu'est-ce que la fonction "wrap"? C'est une fermeture qui a ce dictionnaire de «valeurs».

Chaque fois que vous appelez votre décorateur, le corps du décorateur ne s'exécute qu'une seule fois. Il n'y a donc qu'un seul dictionnaire de «valeurs». Que se passe-t-il d'autre lors de l'exécution du corps du décorateur "dec"? Rien d'autre que le retour de la référence à la fonction "wrap". c'est ça.

Maintenant, lorsque vous appelez votre fonction "fib" (à l'origine la fonction "wrap"), cette fermeture s'exécute normalement car c'est juste une fonction récursive, sauf qu'elle a cette fonctionnalité de cache supplémentaire.

3- Parce qu'il faut avoir un handle vers la fonction interne (ici la fonction "wrap"). Vous voulez l'appeler plus tard afin de calculer Fibonacci.


0 commentaires

0
votes

Vous pouvez avoir un bon aperçu de ce qui se passe ici en ajoutant simplement quelques instructions d'impression, par exemple:

Wrap <function fib at 0x7facac4b70d0>
args:  (5,)  *args: 5 True {}
args:  (4,)  *args: 4 True {}
args:  (3,)  *args: 3 True {}
args:  (2,)  *args: 2 True {}
args:  (1,)  *args: 1 True {(2,): 1}
args:  (2,)  *args: 2 False {(2,): 1, (1,): 1, (3,): 2}
args:  (3,)  *args: 3 False {(2,): 1, (1,): 1, (3,): 2, (4,): 3}
Answer 5

Donc, le résultat de ceci est:

from functools import wraps
def dec(func):
    values = {}
    @wraps(func)
    def wrap(*args):
        print("args: ", args, " *args:", *args, args not in values, values)
        if args not in values:
            values[args] = func(*args)
        return values[args]
    print("Wrap", wrap)
    return wrap

@dec
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

print("Answer", fib(5))

Pour commencer par la dernière partie de votre question, comme vous pouvez le voir dans la sortie, la fonction est encapsulée au début du programme lorsque Python exécute tout le code avant d'arriver à l'instruction d' print . Cela ne se produit qu'une seule fois et c'est ce qui permet à votre appel ultérieur à fib d'appeler la fonction déjà encapsulée.

Pour qu'une fonction wrapper fonctionne, elle a besoin de savoir quels arguments sont passés à la fonction wrapped, et elle le fait via *args pour voir le paramètre passé, dont elle a besoin pour la mémoriser et son résultat.

La différence entre args et *args se résume à la décompression de tuple . Vous pouvez voir dans la sortie ci-dessus que args contient, par exemple (1, ) , et *args décompresse ceci à seulement 1 .

Ainsi, en utilisant args dans la fonction encapsulée, il ne stocke pas réellement les nombres sous forme de clés dans le dictionnaire de values comme vous pouvez vous en douter, mais un tuple contenant le nombre. Dans ce cas, le déballage pourrait être fait, mais ce serait une étape supplémentaire inutile.

Cela vous donne également un bon aperçu de la façon dont la récursivité se produit car lors du premier appel à fib(n) , la première partie de l'instruction return fib(n - 1) est return fib(n - 1) , donc cela doit être évalué avant le fib(n-2) , donc cela entraîne immédiatement la mémorisation de chaque valeur de n à 1 , puis vous retournez la pile de récursivité, en évaluant fib(n-2) , mais tous ces résultats peuvent être satisfaits par des values sans autres appels récursifs à fib .


0 commentaires