
# GUIDE COMPLET *args ET **kwargs EN PYTHON



[OK] 1. BASES : QU'EST-CE QUE *args ET **kwargs ?


# *args (arguments) : capture les arguments POSITIONNELS en tuple
# **kwargs (keyword arguments) : capture les arguments NOMMÉS en dictionnaire

def demo_base(*args, **kwargs):
    """Démontre les bases de *args et **kwargs"""
    print(f"[PACKAGE] args (tuple) : {args}")
    print(f"[PACKAGE] Type de args : {type(args)}")
    print(f"[LABEL]  kwargs (dict) : {kwargs}")
    print(f"[LABEL]  Type de kwargs : {type(kwargs)}")

# Exemples d'utilisation
# demo_base(1, 2, 3, name="Alice", age=30)
# Output:
# [PACKAGE] args (tuple) : (1, 2, 3)
# [PACKAGE] Type de args : <class 'tuple'>
# [LABEL]  kwargs (dict) : {'name': 'Alice', 'age': 30}
# [LABEL]  Type de kwargs : <class 'dict'>


[OK] 2. *args : ARGUMENTS POSITIONNELS VARIABLES


def sum_all(*args):
    """Additionne un nombre variable d'arguments"""
    # args est un tuple, on peut itérer dessus
    total = 0
    for num in args:
        total += num
    return total

# Ou plus court avec sum()
def sum_all_v2(*args):
    return sum(args)

# print(sum_all(1, 2, 3))        # 6
# print(sum_all(1, 2, 3, 4, 5))  # 15
# print(sum_all())               # 0 (tuple vide)


# Accès par index (comme un tuple)
def first_and_last(*args):
    """Retourne le premier et dernier argument"""
    if not args:
        return None, None
    return args[0], args[-1]

# print(first_and_last(10, 20, 30, 40))  # (10, 40)


# Déballage avec slicing
def print_args(*args):
    """Démontre l'accès aux éléments de args"""
    print(f"Premier : {args[0] if args else 'Aucun'}")
    print(f"Dernier : {args[-1] if args else 'Aucun'}")
    print(f"Tous sauf premier : {args[1:]}")
    print(f"Tous sauf dernier : {args[:-1]}")

# print_args(1, 2, 3, 4, 5)



[OK] 3. **kwargs : ARGUMENTS NOMMÉS VARIABLES


def print_info(**kwargs):
    """Affiche des informations à partir de kwargs"""
    # kwargs est un dictionnaire
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# print_info(name="Alice", age=30, city="Paris")
# Output:
# name: Alice
# age: 30
# city: Paris


# Accès aux valeurs avec get() (sécurisé)
def create_user(**kwargs):
    """Crée un utilisateur avec valeurs par défaut"""
    name = kwargs.get('name', 'Anonymous')  # Valeur par défaut
    age = kwargs.get('age', 0)
    email = kwargs.get('email', 'no-email@example.com')
    
    return {
        'name': name,
        'age': age,
        'email': email
    }

# user1 = create_user(name="Bob", age=25)
# user2 = create_user(email="alice@example.com")
# print(user1)  # {'name': 'Bob', 'age': 25, 'email': 'no-email@example.com'}
# print(user2)  # {'name': 'Anonymous', 'age': 0, 'email': 'alice@example.com'}


# Vérifier la présence d'une clé
def process_data(**kwargs):
    """Traite les données selon les clés présentes"""
    if 'user_id' in kwargs:
        print(f"Traitement pour user_id: {kwargs['user_id']}")
    
    if 'debug' in kwargs and kwargs['debug']:
        print("Mode debug activé")
    
    # Récupération avec pop (retire du dict)
    priority = kwargs.pop('priority', 'normal')
    print(f"Priorité: {priority}")
    print(f"Kwargs restants: {kwargs}")

# process_data(user_id=123, debug=True, priority='high', extra='data')



[OK] 4. COMBINAISON *args ET **kwargs


def full_function(*args, **kwargs):
    """Fonction acceptant tous types d'arguments"""
    print("Arguments positionnels:")
    for i, arg in enumerate(args, 1):
        print(f"  Arg {i}: {arg}")
    
    print("\nArguments nommés:")
    for key, value in kwargs.items():
        print(f"  {key}: {value}")

# full_function(1, 2, 3, name="Alice", age=30)


# ORDRE IMPORTANT : paramètres standards -> *args -> **kwargs
def exemple_ordre(a, b, *args, **kwargs):
    """Ordre correct des paramètres"""
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

# exemple_ordre(1, 2, 3, 4, 5, x=10, y=20)
# Output:
# a=1, b=2
# args=(3, 4, 5)
# kwargs={'x': 10, 'y': 20}



[OK] 5. PARAMÈTRES PAR DÉFAUT AVEC *args ET **kwargs


def fonction_avec_defauts(a, b=10, *args, c=100, **kwargs):
    """Mélange de différents types de paramètres"""
    print(f"a (obligatoire): {a}")
    print(f"b (défaut): {b}")
    print(f"args (variables): {args}")
    print(f"c (keyword-only): {c}")
    print(f"kwargs (variables): {kwargs}")

# fonction_avec_defauts(1)                              # a=1, reste par défaut
# fonction_avec_defauts(1, 2, 3, 4, c=50, x=10)        # tous fournis



[OK] 6. DÉBALLAGE (UNPACKING) : * ET **


# * pour déballer un itérable en arguments positionnels
def add_three(a, b, c):
    """Additionne 3 nombres"""
    return a + b + c

numbers = [1, 2, 3]
# result = add_three(*numbers)  # Équivalent à add_three(1, 2, 3)
# print(result)  # 6


# ** pour déballer un dictionnaire en arguments nommés
def greet(name, age, city):
    """Salutation avec infos"""
    return f"Bonjour {name}, {age} ans, de {city}"

user_data = {'name': 'Alice', 'age': 30, 'city': 'Paris'}
# message = greet(**user_data)  # Équivalent à greet(name='Alice', age=30, city='Paris')
# print(message)


# Déballage partiel
def complex_function(a, b, c, d, e):
    return a + b + c + d + e

args1 = [1, 2]
args2 = [3, 4, 5]
# result = complex_function(*args1, *args2)  # Combine plusieurs débállages
# print(result)  # 15



[OK] 7. FUSION DE DICTIONNAIRES AVEC **


# Fusion de plusieurs dictionnaires
def merge_configs(**kwargs):
    """Fusionne des configurations"""
    return kwargs

default_config = {'host': 'localhost', 'port': 8000, 'debug': False}
user_config = {'port': 3000, 'debug': True}

# Configuration finale (user_config écrase default_config)
# final_config = merge_configs(**default_config, **user_config)
# print(final_config)
# # {'host': 'localhost', 'port': 3000, 'debug': True}

# Python 3.9+ : opérateur | pour fusion
# final_config = default_config | user_config



[OK] 8. PASSAGE D'ARGUMENTS À D'AUTRES FONCTIONS


def wrapper_function(*args, **kwargs):
    """Wrapper qui transmet tous les arguments"""
    print("Avant l'appel")
    result = target_function(*args, **kwargs)  # Transmet tout
    print("Après l'appel")
    return result

def target_function(a, b, name=None):
    """Fonction cible"""
    return f"a={a}, b={b}, name={name}"

# wrapper_function(1, 2, name="Test")


# Cas d'usage : décorateurs
from functools import wraps

def log_decorator(func):
    """Décorateur qui log les appels"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Appel de {func.__name__} avec:")
        print(f"  args={args}")
        print(f"  kwargs={kwargs}")
        return func(*args, **kwargs)  # Transmet tout
    return wrapper

@log_decorator
def calculate(x, y, operation='+'):
    """Calcul simple"""
    if operation == '+':
        return x + y
    elif operation == '*':
        return x * y
    return 0

# calculate(5, 3, operation='*')



[OK] 9. VALIDATION ET TRAITEMENT DE *args ET **kwargs


def validated_function(*args, **kwargs):
    """Valide les arguments avant traitement"""
    # Validation de args
    if not args:
        raise ValueError("Au moins un argument positionnel requis")
    
    if not all(isinstance(arg, (int, float)) for arg in args):
        raise TypeError("Tous les args doivent être des nombres")
    
    # Validation de kwargs
    allowed_keys = {'debug', 'verbose', 'timeout'}
    invalid_keys = set(kwargs.keys()) - allowed_keys
    if invalid_keys:
        raise ValueError(f"Clés non autorisées: {invalid_keys}")
    
    return sum(args), kwargs

# result, options = validated_function(1, 2, 3, debug=True)


# Extraction de valeurs spécifiques de kwargs
def process_user(**kwargs):
    """Extrait et valide des champs spécifiques"""
    # Champs requis
    required_fields = ['name', 'email']
    for field in required_fields:
        if field not in kwargs:
            raise ValueError(f"Champ requis manquant: {field}")
    
    # Extraction
    name = kwargs.pop('name')
    email = kwargs.pop('email')
    age = kwargs.pop('age', None)  # Optionnel
    
    # kwargs contient maintenant les champs non traités
    extra_data = kwargs
    
    return {
        'name': name,
        'email': email,
        'age': age,
        'extra': extra_data
    }

# user = process_user(name="Alice", email="alice@example.com", city="Paris")



[OK] 10. *args ET **kwargs DANS LES CLASSES


class FlexibleClass:
    """Classe avec constructeur flexible"""
    
    def __init__(self, *args, **kwargs):
        """Initialisation flexible"""
        # Stocke tous les arguments
        self.args = args
        self.kwargs = kwargs
        
        # Ou traite spécifiquement
        self.name = kwargs.get('name', 'Unknown')
        self.values = list(args)
    
    def __repr__(self):
        return f"FlexibleClass(args={self.args}, kwargs={self.kwargs})"


class ConfigurableService:
    """Service avec configuration flexible"""
    
    def __init__(self, **config):
        """Configuration via kwargs"""
        self.host = config.get('host', 'localhost')
        self.port = config.get('port', 8000)
        self.debug = config.get('debug', False)
        
        # Stocke toute config supplémentaire
        self.extra_config = {k: v for k, v in config.items() 
                            if k not in ['host', 'port', 'debug']}
    
    def connect(self, *args, **kwargs):
        """Méthode avec args/kwargs"""
        print(f"Connexion à {self.host}:{self.port}")
        print(f"Args: {args}, Kwargs: {kwargs}")

# service = ConfigurableService(host='example.com', port=3000, timeout=30)
# service.connect('param1', retry=3)


# Héritage et transmission d'arguments
class BaseClass:
    def __init__(self, name, **kwargs):
        self.name = name
        print(f"BaseClass init avec kwargs: {kwargs}")

class DerivedClass(BaseClass):
    def __init__(self, name, age, **kwargs):
        self.age = age
        # Transmet les kwargs restants à la classe parente
        super().__init__(name, **kwargs)

# obj = DerivedClass('Alice', 30, city='Paris', country='France')



[OK] 11. CAS D'USAGE PRATIQUES


# 1. Logger flexible
def log(message, *args, level='INFO', **kwargs):
    """Logger avec formatage flexible"""
    # args peut contenir des valeurs pour formatage
    if args:
        message = message.format(*args)
    
    # kwargs pour métadonnées
    metadata = ' '.join(f"{k}={v}" for k, v in kwargs.items())
    print(f"[{level}] {message} {metadata}")

# log("User {} logged in", "Alice", level='INFO', user_id=123, ip='192.168.1.1')


# 2. Builder pattern
class QueryBuilder:
    """Constructeur de requêtes SQL"""
    
    def __init__(self, table):
        self.table = table
        self.conditions = []
    
    def where(self, **conditions):
        """Ajoute des conditions WHERE"""
        for key, value in conditions.items():
            if isinstance(value, str):
                self.conditions.append(f"{key} = '{value}'")
            else:
                self.conditions.append(f"{key} = {value}")
        return self
    
    def build(self):
        """Construit la requête"""
        query = f"SELECT * FROM {self.table}"
        if self.conditions:
            query += " WHERE " + " AND ".join(self.conditions)
        return query

# query = QueryBuilder('users').where(age=30, city='Paris', active=True).build()
# print(query)


# 3. API wrapper
def api_call(endpoint, *path_params, method='GET', **query_params):
    """Appel API flexible"""
    # path_params pour construire l'URL
    url = f"https://api.example.com/{endpoint}"
    if path_params:
        url += "/" + "/".join(map(str, path_params))
    
    # query_params pour les paramètres de requête
    if query_params:
        params = "&".join(f"{k}={v}" for k, v in query_params.items())
        url += f"?{params}"
    
    return f"{method} {url}"

# result = api_call('users', 123, 'posts', method='GET', limit=10, page=2)
# print(result)  # GET https://api.example.com/users/123/posts?limit=10&page=2


# 4. Configuration merging
def create_config(*configs, **overrides):
    """Fusionne plusieurs configs avec priorités"""
    final_config = {}
    
    # Fusionne tous les dicts de configs
    for config in configs:
        final_config.update(config)
    
    # Les overrides ont la plus haute priorité
    final_config.update(overrides)
    
    return final_config

# default = {'host': 'localhost', 'port': 8000}
# prod = {'host': 'prod.example.com', 'debug': False}
# config = create_config(default, prod, port=3000, workers=4)



[OK] 12. FONCTIONS VARIADIQUES TYPÉES (Type Hints)


from typing import Any, Dict, Tuple

def typed_function(*args: int, **kwargs: str) -> Tuple[int, Dict[str, str]]:
    """Fonction avec types pour args et kwargs"""
    total = sum(args)
    return total, kwargs

# Python 3.10+ : types plus précis
from typing import TypedDict

class UserConfig(TypedDict):
    name: str
    age: int
    email: str

def create_typed_user(**kwargs: Any) -> UserConfig:
    """Crée un user avec type défini"""
    return UserConfig(
        name=kwargs['name'],
        age=kwargs['age'],
        email=kwargs['email']
    )



[OK] 13. ARGS ET KWARGS AVEC KEYWORD-ONLY PARAMETERS


# Paramètres keyword-only (après *args ou *)
def function_with_keyword_only(*args, required_kwarg, optional_kwarg=10):
    """
    required_kwarg DOIT être passé par nom
    optional_kwarg est optionnel
    """
    print(f"args: {args}")
    print(f"required_kwarg: {required_kwarg}")
    print(f"optional_kwarg: {optional_kwarg}")

# function_with_keyword_only(1, 2, 3, required_kwarg='must_be_named')

# Force tous les paramètres à être nommés (avec *)
def keyword_only_function(*, name, age, city='Paris'):
    """Tous les paramètres doivent être nommés"""
    return f"{name}, {age}, {city}"

# result = keyword_only_function(name='Alice', age=30)  # [OK]
# result = keyword_only_function('Alice', 30)  # [X] TypeError



[OK] 14. ARGS ET KWARGS AVEC POSITIONAL-ONLY PARAMETERS (Python 3.8+)


# Paramètres positional-only (avant /)
def positional_only_function(a, b, /, c, d, *args, **kwargs):
    """
    a et b DOIVENT être positionnels
    c et d peuvent être les deux
    """
    print(f"a={a}, b={b}, c={c}, d={d}")
    print(f"args={args}, kwargs={kwargs}")

# positional_only_function(1, 2, 3, 4, 5, x=10)  # [OK]
# positional_only_function(a=1, b=2, c=3, d=4)  # [X] TypeError pour a et b



[OK] 15. PIÈGES COURANTS ET SOLUTIONS


# [X] PIÈGE 1 : Mutation de args (tuple immuable)
def bad_function(*args):
    # args[0] = 100  # [X] TypeError: tuple doesn't support item assignment
    pass

# [OK] Solution : convertir en liste
def good_function(*args):
    args_list = list(args)
    args_list[0] = 100
    return args_list


# [X] PIÈGE 2 : Ordre incorrect des paramètres
# def wrong_order(**kwargs, *args):  # [X] SyntaxError
#     pass

# [OK] Solution : *args avant **kwargs
def correct_order(*args, **kwargs):
    pass


# [X] PIÈGE 3 : Mélanger * et paramètres normaux sans attention
def confusing(a, *args, b):  # b est keyword-only!
    pass

# confusing(1, 2, 3)  # [X] TypeError: missing required keyword-only argument: 'b'
# confusing(1, 2, 3, b=4)  # [OK]


# [X] PIÈGE 4 : Déballer avec mauvais nombre d'arguments
def needs_three(a, b, c):
    pass

# needs_three(*[1, 2])  # [X] TypeError: missing 1 required positional argument

# [OK] Solution : vérifier avant
def safe_unpack(*args):
    if len(args) != 3:
        raise ValueError("Exactement 3 arguments requis")
    return needs_three(*args)



[OK] 16. ASTUCES AVANCÉES


# 1. Capture tous les arguments sauf certains
def exclude_specific(**kwargs):
    """Retire des clés spécifiques"""
    excluded = ['password', 'secret']
    safe_kwargs = {k: v for k, v in kwargs.items() if k not in excluded}
    return safe_kwargs

# data = exclude_specific(name='Alice', password='123', email='alice@example.com')


# 2. Déballage conditionnel
def conditional_call(func, args_list, should_unpack=True):
    """Appelle avec ou sans déballage"""
    if should_unpack:
        return func(*args_list)
    else:
        return func(args_list)


# 3. Collecting avec *
# Capturer le "reste" dans une liste
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2, 3, 4], last=5

# Avec fonctions
def process_first_and_rest(first, *rest):
    print(f"Premier: {first}")
    print(f"Reste: {rest}")

# process_first_and_rest(1, 2, 3, 4, 5)


# 4. Dictionnaire en arguments nommés uniquement désirés
def selective_kwargs(func, **all_kwargs):
    """Passe seulement les kwargs que func accepte"""
    import inspect
    sig = inspect.signature(func)
    valid_kwargs = {k: v for k, v in all_kwargs.items() 
                    if k in sig.parameters}
    return func(**valid_kwargs)

def example_func(name, age):
    return f"{name}, {age}"

# result = selective_kwargs(example_func, name='Alice', age=30, extra='ignored')



# RÉSUMÉ DES CONCEPTS CLÉS

"""
[OK] *args : Capture arguments positionnels en TUPLE
[OK] **kwargs : Capture arguments nommés en DICTIONNAIRE
[OK] Ordre : paramètres normaux -> *args -> kwargs -> **kwargs
[OK] * déballe itérables en positionnels
[OK] ** déballe dicts en nommés
[OK] Utile pour : wrappers, décorateurs, APIs flexibles
[OK] / force positional-only (Python 3.8+)
[OK] * force keyword-only
[OK] Toujours documenter quels kwargs sont acceptés
[OK] Valider les arguments dans les fonctions flexibles

PATTERNS COMMUNS:
- Wrappers: wrapper(*args, **kwargs) -> func(*args, **kwargs)
- Config: merge(**default, **user, **overrides)
- Validation: extract puis validate args/kwargs
- Logging: log(msg, *values, **metadata)
"""



# EXEMPLES DE TESTS


if __name__ == "__main__":
    print("=" * 60)
    print("TESTS *args ET **kwargs")
    print("=" * 60)
    
    # Test 1: Fonction basique
    print("\n1. Fonction basique:")
    demo_base(1, 2, 3, name="Alice", age=30)
    
    # Test 2: Sum avec *args
    print("\n2. Somme variable:")
    print(f"sum_all(1, 2, 3, 4, 5) = {sum_all(1, 2, 3, 4, 5)}")
    
    # Test 3: Création user
    print("\n3. Création utilisateur:")
    user = create_user(name="Bob", age=25)
    print(user)
    
    # Test 4: Déballage
    print("\n4. Déballage:")
    numbers = [1, 2, 3]
    print(f"add_three(*{numbers}) = {add_three(*numbers)}")
    
    # Test 5: Fusion configs
    print("\n5. Fusion configs:")
    default = {'host': 'localhost', 'port': 8000}
    user_cfg = {'port': 3000, 'debug': True}
    final = create_config(default, user_cfg, workers=4)
    print(final)
    
    print("\n" + "=" * 60)