151 votes

Quelles sont les bonnes utilisations des valeurs par défaut des arguments de fonctions mutables ?

C'est une erreur courante en Python de définir un objet mutable comme valeur par défaut d'un argument dans une fonction. Voici un exemple tiré de cet excellent article de David Goodger :

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

L'explication de ce phénomène est la suivante ici .

J'en viens maintenant à ma question : Cette syntaxe peut-elle être utilisée à bon escient ?

Je veux dire que si tous ceux qui la rencontrent font la même erreur, la déboguent, comprennent le problème et, à partir de là, essaient de l'éviter, quelle est l'utilité d'une telle syntaxe ?

3voto

katrielalex Points 40655

EDIT (clarification) : Le problème des arguments par défaut mutables est un symptôme d'un choix de conception plus profond, à savoir que les valeurs des arguments par défaut sont stockées en tant qu'attributs de l'objet fonction. On peut se demander pourquoi ce choix a été fait ; comme toujours, il est difficile de répondre correctement à ce genre de questions. Mais il a certainement de bonnes raisons d'être :

Optimiser les performances :

def foo(sin=math.sin): ...

Saisir les valeurs d'un objet dans une fermeture au lieu de la variable.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)

1voto

Karl Knechtel Points 24349

Un argument par défaut mutable, qui n'est jamais utilisé par le code appelant, peut être utilisé pour créer une valeur sentinelle. La copie profonde intégrée à Python le fait .

Un argument mutable est utilisé pour garantir que la valeur est unique à cette fonction : puisqu'une nouvelle liste doit être créée lorsque la fonction deepcopy est compilé et qu'il est par ailleurs inaccessible, l'objet ne peut apparaître nulle part ailleurs. Les objets immuables ont tendance à être internés, et une liste vide est facile à créer. Normalement, les objets sentinelles de ce type devraient être explicitement créés séparément, mais cette méthode permet d'éviter la pollution de l'espace de noms (même avec des noms de type "leading-underscore"), je suppose.

-3voto

user10637953 Points 364

Pour répondre à la question des bonnes utilisations des valeurs mutables des arguments par défaut, je propose l'exemple suivant :

Une valeur par défaut mutable peut être utile pour programmer des commandes faciles à utiliser et importables de votre propre création. La méthode du défaut mutable revient à avoir des variables statiques privées dans une fonction que vous pouvez initialiser lors du premier appel (un peu comme une classe) mais sans avoir à recourir aux globales, sans avoir à utiliser un wrapper, et sans avoir à instancier un objet de classe qui a été importé. C'est à sa manière élégant, et j'espère que vous en conviendrez.

Prenons ces deux exemples :

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])

    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")

    print(" Calling dittle() normally..")
    dittle()

    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 

    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

Si vous exécutez ce code, vous verrez que la fonction dittle() internalise lors du tout premier appel mais pas lors des appels suivants, qu'elle utilise un cache statique privé (le défaut mutable) pour le stockage statique interne entre les appels, qu'elle rejette les tentatives de détournement du stockage statique, qu'elle est résistante aux entrées malveillantes et qu'elle peut agir sur la base de conditions dynamiques (ici sur le nombre de fois que la fonction a été appelée).

La clé de l'utilisation des valeurs par défaut mutables n'est pas de faire quoi que ce soit qui puisse réaffecter la variable en mémoire, mais de toujours changer la variable en place.

Pour vous rendre compte de la puissance et de l'utilité potentielles de cette technique, enregistrez ce premier programme dans votre répertoire courant sous le nom "DITTLE.py", puis exécutez le programme suivant. Il importe et utilise notre nouvelle commande dittle() sans qu'il soit nécessaire de se souvenir d'une quelconque étape ou d'un quelconque obstacle à la programmation.

Voici notre deuxième exemple. Compilez et exécutez ce programme en tant que nouveau programme.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

N'est-ce pas là une façon de faire aussi simple et propre que possible ? Ces valeurs par défaut modifiables peuvent s'avérer très utiles.

\========================

Après avoir réfléchi à ma réponse, je ne suis pas sûr d'avoir fait la différence entre l'utilisation de la méthode mutable par défaut et celle de la méthode normale par défaut. d'accomplir la même chose.

La méthode habituelle consiste à utiliser une fonction importable qui englobe un objet Class (et utilise un global). À titre de comparaison, voici une méthode basée sur une classe qui tente de faire les mêmes choses que la méthode mutable par défaut.

from time import sleep

class dittle_class():

    def __init__(self):

        self.b = 0
        self.a = " Hello World!"

        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")

    def report(self):
        self.b  = self.b + 1

        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")

        if self.b == 5:
            self.a = " It's Great to be alive!"

        print(" Internal String =",self.a,end="\n\n")

        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl

    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")

    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.

    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")

    print(" Calling dittle() normally..")
    dittle()

    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7

    dittle()
    dittle()

Enregistrez ce programme basé sur les classes dans votre répertoire courant sous le nom de DITTLE.py puis exécutez le code suivant (qui est le même que précédemment).

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

En comparant les deux méthodes, les avantages de l'utilisation d'un défaut mutable dans une fonction devraient être plus clairs. La méthode mutable par défaut n'a pas besoin de globaux, ses variables internes ne peuvent pas être définies directement. Et alors que la méthode mutable a accepté un argument transmis pour un cycle unique puis l'a ignoré, la méthode Class a été altérée de façon permanente parce que ses variables internes sont directement exposées à l'extérieur. Quant à savoir quelle méthode est la plus facile à programmer ? Je pense que cela dépend de votre niveau de confort avec les méthodes et de la complexité de vos objectifs.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X