<jupyter_start><jupyter_text>`*args`, `**kwargs` et *unpacking* en PythonOn trouve parfois des fonctions avec des définitions étranges:```pythondef add_all(*args):     does stuff    passdef compute_thing(**kwargs):    ...```Quelles sont les significations de ces arguments ? Explication rapide- une fonction preut prendre en argument des *positional arguments* qui sont nécessaires suivis de *keyword arguments* qui ont une valeur par défaut (et sont donc optionnels)- on utilise `*args` quand on veut que la fonction puisse être appelée avec un nombre de *positional arguments* variable  - il est souvent plus simple de **ne pas définir les fonctions ainsi**: on peut souvent remplacer `*args` par un objet de type liste   - on utilise `**kwargs` quand on veut que la fonction puisse être appelée avec des *keyword arguments* arbitraire  - il est souvent plus simple de **ne pas définir les fonctions ainsi** mais de nommer chaque argument de manière explicite.    - pour utiliser une liste sur une fonction écrite avec plusieurs arguments, on peut utiliser le *list unpacking*:  ```pythondef show_position(x, y):    passmy_pos = [23, 66]show_position(*my_pos)```      - pour passer d'un coup de nombreux paramètres à une fonction on peut utiliser le *dict unpacking*:```pythondef complex_function(filename, output_folder, scale_ratio=0.2, preprocess=True):    passconfig = {    'filename': 'myfile.tif',    'output_folder': 'data/output',      'scale_ratio': 18,   si on ne fournit pas ce paramètre, sa valeur par défaut (0.2) va être utilisée    'preprocess': False}complex_function(**config)```  Arguments habituels d'une fonctionHabituellement on distingue deux types d'arguments pour une fonction:- les *positional arguments* qui sont obligatoires et doivent être fournis dans l'ordre- les *keyword arguments* qui ont une valeur par défaut (ils sont donc optionels) et peuvent être fournis dans un ordre quelconque.<jupyter_code># Cette fonction a un positional argument et un named argument
def power(number, exponent=2):
    return number ** exponent
# Le premier argument est nécessaire. Ca ne fonctionne pas si on ne le met pas
power()
# Si on met un seul argument le deuxième prend la valeur par défaut
power(3)
# Si on utilise un deuxième argument il occupe naturellement la place du deuxième argument de la fonction 
# (qui se trouve ici être un argument nommé): ça remplace la valeur par défaut
power(4, 3)
# On peut utiliser les noms des arguments de manière explicites
power(4, exponent=3)
# Par contre on ne peut pas mettre les arguments "nommés" avant des arguments "positionnels": 
power(exponent=3, 4)
# On peut utiliser le nom du premier argument également (si les arguments suivants sont aussi nommés)
power(number=4, exponent=3)
# Dans ce cas on peut aussi changer arbitrairement l'ordre des arguments:
power(exponent=3, number=4)
# par contre il ne faut pas se tromper sur le nom des arguments:
power(mauvais_argument=4, exponent=3)
# Si on ajoute une troisième argument la fonction ne sait pas quoi en faire
power(3, 3, 4)<jupyter_output><empty_output><jupyter_text>`*args`: pour un nombre d'arguments positionnels variablesOn utilise `*args` quand on ne sait pas encore combien de *positional arguments* seront reçus par la fonction. Par exemple si on veut faire une fonction qui fait la somme de plusieurs nombres on veut que ça fonctionne avec 0, 1, 2 ou N nombres.A l'intérieur de la fonction on peut considérer la variable `args` comme une liste (c'est en fait un `tuple` ce qui est presque pareil).<jupyter_code># Cette fonction n'a que des arguments optionels
# Elle accepte un nombre de positional argument variable
def sum_of_powers(*args, exponent=2):
    print('Adding numbers', args, 'with exponent', exponent)
    total = 0
    for number in args:
        total += number ** exponent
    return total
# Aucun argument n'est nécessaire ici
sum_of_powers()
# Si on met un seul argument: il va dans la 'liste' args
sum_of_powers(4)
# Si on ajoute d'autres arguments ils sont "rangés" dans la variable args
sum_of_powers(1, 2, 3, exponent=1)
# A nouveau on ne peut pas mettre des positional arguments après des keyword arguments
sum_of_powers(exponent=1, 2, 3)<jupyter_output><empty_output><jupyter_text>*Unpacking* d'une listePour une fonction qui accèpte plusieurs arguments, il est souvent pratique d'utiliser la syntaxe de *list unpacking* pour lui fournir les arguments.Pour cela on utilise `my_function(*list_of_values)`<jupyter_code>number_to_add = [1, 2, 3, 4]
sum_of_powers(*number_to_add, exponent=1)
# Dans l'exemple précédent on utilise le list unpacking pour une fonction qui accèpte un nombre variable d'arguments
# mais ce n'est pas toujours le cas.

def show_position(x, y):
    print('Your position: (x: {:.1f}, y: {:.1f})'.format(x, y))
    
pos = [12, 45]
show_position(*pos)<jupyter_output>Your position: (x: 12.0, y: 45.0)<jupyter_text>Conseil: utiliser une liste plutôt que `*args`Finalement il est souvent plus simple de fournir explicitement une liste à la fonction plutôt que des arguments les uns après les autres.<jupyter_code>def sum_of_powers(list_of_numbers, exponent=2):
    total = 0
    for number in list_of_numbers:
        total += number ** exponent
    return total
numbers_to_add = [1, 2, 3]
sum_of_powers(numbers_to_add, exponent=1)
# Par contre on ne peut plus utiliser la fonction sans argument
sum_of_powers()
# On est obligé de fournir une liste vide
sum_of_powers([])<jupyter_output><empty_output><jupyter_text>`**kwargs`: pour des arguments nommés arbitrairesDans la fonction, `kwargs` est un dictionnaire pour lequel:- clé = nom du *keyword argument*- valeur = valeur de l'argument<jupyter_code>def show_arguments(a, b, *args, exponent=2, **kwargs):
    print('Required arguments: a =', a, 'b =', b)
    print('Optional positional arguments ({:d}):'.format(len(args)), args)
    print('Optional "exponent" argument:', exponent)
    print('Other keyword arguments ({:d}):'.format(len(kwargs)), kwargs)
    
    for key, value in kwargs.items():
        print('    - ', key, '->', value)
show_arguments()
show_arguments(1, 2)
numbers = [1, 2, 3, 4, 5]
show_arguments(*numbers)
show_arguments(*numbers, exponent=4)
show_arguments(exponent=3, a=45, b=27)
show_arguments(*numbers, complicated=True, why='For Fun')<jupyter_output>Required arguments: a = 1 b = 2
Optional positional arguments (3): (3, 4, 5)
Optional "exponent" argument: 2
Other keyword arguments (2): {'complicated': True, 'why': 'For Fun'}
    -  complicated -> True
    -  why -> For Fun<jupyter_text>*Unpacking* d'un dictionnairePour une fonction qui prend plusieurs paramètres et que c'est compliqué à écrire il peut être pratique d'utiliser le *dictionnary unpacking*.<jupyter_code>configuration = {
    'a': 3,
    'b': 45,
    'exponent': 4,
    'complicated': True
}
show_arguments(**configuration)
# il ne faut pas oublier les argument nécessaire dans ce cas
configuration = {
    'a': 3,
    # 'b': 45,
    'exponent': 4,
    'complicated': True
}
show_arguments(**configuration)<jupyter_output><empty_output><jupyter_text>Conseil: nommer tous les arguments de manière explicite dans la définition de la fonction.Quand une fonction devient complexe on peut être tenté d'utiliser **kwargs pour simplifier. Même si il y a des cas où cet "argument fourre-tout" est utile, dans la plupart des cas il est préférable de nommer tous les arguments pour que l'utilisation de la fonction soit explicite.<jupyter_code>def complexe_computation_with_parameters(filename,
                                         load_method='regular',
                                         postprocessing=True,
                                         postprocessing_ratio=0.1,
                                         log_folder='logs/',
                                         save_images=False):
    # la fonction fait des choses compliquées...
    if postprocessing:
        do_postprocessing(postprocessing_ratio)
    if save_images:
        # ...
        pass
# On peut être tenté d'écrire:
def complexe_computation_with_parameters(filename, **kwargs):
    if kwargs['postprocessing']:
        do_postprocessing(kwargs['postprocessing_ratio'])
    if kwargs['save_images']:
        # ...
        pass
    
# Mais quand on lit la définition de la fonction on ne sait pas du tout quels arguments sont utiles.<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08867112810707457, 'fraction_numerical': 0.011352772466539197, 'mean_word_length': 4.323791348600509, 'pattern_counts': {'":': 1, '<': 27, '<?xml version=': 0, '>': 30, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '362438', 'license': 'MIT', 'n_tokens_mistral': 2574, 'n_tokens_neox': 2506, 'n_words': 985, 'path': 'notebooks/2018-07-03_tutoriel_args_kwargs.ipynb', 'repo_name': 'BenjaminHabert/fluotracker', 'size': '8368'}