.. _cours2: Python avancé ############# .. contents:: Table des matières :local: .. 1 Fonctionnalités avancées 1.1 Arguments anonymes 1.2 Dépaquetage des arguments et des itérables 1.3 Différents types de paramètre d'une fonction 1.4 Décorateurs 1.5 Fonction anonyme 1.6 Itérateurs et générateurs 1.6.1 Itérateurs 1.6.2 Générateurs 2 Programmation Orientée Objet avancée 2.1 Variables de classe 2.2 Méthodes statiques 2.3 Méthodes de classe 2.4 Attributs et méthodes privées 2.5 Propriétés 3 Éléments passés sous silence 4 Python 3.x 4.1 Transition Python 2 à Python 3 Fonctionnalités avancées ======================== La brève introduction à Python se limite à des fonctionnalités relativement simples du langage. De nombreuses fonctionnalités plus avancées n'ont pas encore été abordées [#global]_. Arguments anonymes ------------------ .. index:: *args **kwargs Il est possible de laisser libre *a priori* le nombre et le nom des arguments d'une fonction, traditionnellement nommés `args` (arguments nécessaires) et `kwargs` (arguments optionnels). P.ex.: >>> def f(*args, **kwargs): ... print("args:", args) ... print("kwargs:", kwargs) >>> f() args: () kwargs: {} >>> f(1, 2, 3, x=4, y=5) args: (1, 2, 3) kwargs: {'y': 5, 'x': 4} .. Attention:: Cela laisse une grande flexibilité dans l'appel de la fonction, mais au prix d'une très mauvaise lisibilité de sa signature (interface de programmation). *À utiliser avec parcimonie...* Dépaquetage des arguments et des itérables ------------------------------------------ .. index:: pair: dépaquetage; * pair: dépaquetage; ** Il est possible de dépaqueter les `[kw]args` d'une fonction à la volée à l'aide de l'opérateur `[*]*`. Ainsi, avec la même fonction `f` précédemment définie: >>> my_args = (1, 2, 3) >>> my_kwargs = dict(x=4, y=5) >>> f(my_args, my_kwargs) # 2 args (1 liste et 1 dict.) et 0 kwarg args: ((1, 2, 3), {'x': 4, 'y': 5}) kwargs: {} >>> f(*my_args, **my_kwargs) # 3 args (1, 2 et 3) et 2 kwargs (x et y) args: (1, 2, 3) kwargs: {'x': 4, 'y': 5} À partir de Python 3.5, il est encore plus facile d'utiliser un ou plusieurs de ces opérateurs conjointement aux `[kw]args` traditionnels (:pep:`448`), dans la limite où les `args` sont toujours situés *avant* les `kwargs`: >>> f(0, *my_args, 9, **my_kwargs, z=6) args: (0, 1, 2, 3, 9) kwargs: {'x': 4, 'z': 6, 'y': 5} Il est également possible d'utiliser l'opérateur `*` pour les affectations multiples (:pep:`3132`): >>> a, b, c = 1, 2, 3, 4 ValueError: too many values to unpack (expected 3) >>> a, *b, c = 1, 2, 3, 4 >>> a, b, c (1, [2, 3], 4) Différents types de paramètre d'une fonction -------------------------------------------- Il existe cinq types d':term:`paramètre ` pour une fonction: - *positional-or-keyword*: décrit un argument qui peut être passé soit comme `arg` (*positional argument*) soit comme `kwarg` (*keyword argument*), dans la limite où les `kwargs` suivent toujours les `args`. C'est le type d'argument par défaut: >>> def f(x, y=1): ... return (x, y) >>> f(3, y=2) # Passage mixte args et kwargs >>> f(3, 2) # Passage uniquement en args >>> f(x=3, y=2) # Passage uniquement en kwargs >>> f(x=3, 2) # SyntaxError: positional argument follows keyword argument - *positional-only*: décrit un argument qui peut uniquement être passé en `arg`. Ces arguments sont identifiés comme étant ceux placés *avant* le `/` dans la liste des arguments, p.ex.: >>> def func(posonly1, posonly2, /, positional_or_keyword): ... - *keyword-only*: décrit un argument qui peut uniquement être passé en `kwarg`. Ces arguments sont identifiés comme étant ceux placés *après* le `*` dans la liste des arguments, p.ex.: >>> def func(positional_or_keyword, *, kwonly1, kwonly2): ... - *var-positional* et *var-keyword* décrivent les arguments qui peuvent être passés via les séquences de longueur arbitraire `*args` et `**kwargs` (cf. ci-dessus): >>> def func(*args, **kwargs): ... Les différents types de paramètres peuvent être utilisés conjointement dans la définition d'une fonction, p.ex. >>> def func(a, *args, b=2, **kwargs): # a et b sont pos-only et kw-only resp. ... return (a, *args), dict(b=b, **kwargs) >>> func(1, 2, 3, c=3, d=4) ((1, 2, 3), {'b': 2, 'c': 3, 'd': 4}) Décorateurs ----------- .. index:: pair: décorateur; @ Les fonctions (et méthodes) sont en Python des objets comme les autres, et peuvent donc être utilisées comme arguments d'une fonction, ou retournées comme résultat d'une fonction. .. code-block:: python :linenos: def compute_and_print(fn, *args, **kwargs): print("Function: ", fn.__name__) print("Arguments: ", args, kwargs) result = fn(*args, **kwargs) print("Result: ", result) return result Les :term:`décorateurs ` sont des *fonctions* s'appliquant sur une fonction ou une méthode pour en modifier le comportement: elles retournent de façon transparente une version « *décorée* » (augmentée) de la fonction initiale. .. code-block:: python :linenos: def verbose(fn): # fonction → fonction décorée def decorated(*args, **kwargs): print("Function: ", fn.__name__) print("Arguments: ", args, kwargs) result = fn(*args, **kwargs) print("Result: ", result) return result return decorated # version décorée de la fonction initiale >>> verbose_sum = verbose(sum) # Décore la fonction standard 'sum' >>> verbose_sum([1, 2, 3]) Function: sum Arguments: ([1, 2, 3],) {} Result: 6 Il est possible de décorer une fonction à la volée lors de sa définition avec la notation `@`:: @verbose def null(*args, **kwargs): pass qui n'est qu'une façon concise d'écrire `null = verbose(null)`. >>> null(1, 2, x=3) Function: null Arguments: (1, 2) {'x': 3} Result: None Noter qu'il est possible d'imbriquer plusieurs décorateurs, et de passer des arguments supplémentaires aux décorateurs. .. rubric:: Exemple 1: ajouter un attribut à une fonction/méthode .. literalinclude:: avance.py :pyobject: add_attrs :linenos: .. index:: monkey patching .. rubric:: Exemple 2: *monkey patching* (modification à la volée des propriétés d'un objet) .. literalinclude:: avance.py :pyobject: make_method :linenos: .. rubric:: Exemple 3: *Exception-catching decorator* (:pep:`463`) .. literalinclude:: avance.py :pyobject: exception_catcher :linenos: .. rubric:: Liens: - `Python et les décorateurs `_ |fr|, - `Primer on Python Decorators `_, - `A guide to Python's function decorators `_, - `Python Decorator Library `_, - :func:`python:functools.wraps` de la librairie standard (`exemple d'utilisation `_), - librairie :pypi:`decorator`: « *signature-preserving function decorators and decorator factories* ». Fonction anonyme ---------------- .. index:: lambda Il est parfois nécéssaire d'utiliser une fonction intermédiaire *simple* que l'on ne souhaite pas définir explicitement et nommément à l'aide de `def`. Cela est possible avec l'opérateur fonctionnel :samp:`lambda {args}: {expression}`. P.ex.: >>> compute_and_print(sum, [1, 2]) # Fn nommée à 1 argument Function: sum Arguments: ([1, 2],), {} Result: 3 >>> compute_and_print(lambda x, y: x + y, 1, 2) # Fn anonyme à 2 arguments Function: Arguments: (1, 2) {} Result: 3 La définition d'une fonction :term:`lambda` ne peut inclure qu'**une seule** expression, et est donc contrainte *de facto* à être très simple, généralement pour être utilisée comme argument d'une autre fonction: >>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] >>> pairs.sort(key=lambda pair: pair[1]) # tri sur le 2e élément de la paire >>> pairs [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')] .. Note:: il est possible de « nommer » une fonction anonyme, p.ex.:: >>> adder = lambda x, y: x + y Cependant, cela est considéré comme une faute de style, puisque ce n'est justement pas l'objectif d'une fonction anonyme! Il n'y a p.ex. pas de *docstring* associée. **Voir également:** :doc:`Functional Programming `. Itérateurs et générateurs ------------------------- .. index:: iter next Un objet :term:`itérable ` est un objet implémentant le *protocole d'itération* :func:`iter` (initialisation) et :func:`next` (itération), p.ex. les objets itérables déjà vus:: >>> s = "abcdef" # Une chaîne est un objet itérable >>> iterator = iter(s) # Itérateur sur la chaîne >>> iterator >>> next(iterator) # 1e élément 'a' >>> [ next(iterator) for i in range(3) ] # les 3 élements suivants ['b', 'c', 'd'] >>> for elt in iterator: # tous les autres éléments ... print(elt) 'e' 'f' Itérateurs '''''''''' .. index:: itérateur Il est possible de créer ses propres itérateurs en implémentant les méthodes spéciales :meth:`~.object.__iter__` (pour l'initialisation de l'itérateur) et :meth:`~.iterator.__next__` (pour l'itération), p.ex. .. literalinclude:: fibonacci.py :pyobject: Fibonacci :linenos: :: >>> [ i for i in Fibonacci(100) ] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] .. Note:: Un itérateur permet de calculer les éléments successifs d'une suite sans avoir à tous les stocker simultanément en mémoire. .. Attention:: aux itérateurs infinis que l'on cherche à convertir en liste! :: >>> list(Fibonacci(10)) [0, 1, 1, 2, 3, 5, 8] >>> list(Fibonacci(None)) :# NOOOOOOOOONNNNNNNNNN!!!!!!! Générateurs ''''''''''' .. index:: générateur yield En pratique, il n'est souvent pas nécessaire ni pythonique de définir son propre itérateur. On utilise plutôt un :term:`générateur `, i.e. une *fonction* retournant un itérateur: - le premier appel de la fonction instancie et initialise l':term:`itérateur `, - à chaque nouvel appel de l'itérateur, il retourne la valeur suivante (:keyword:`yield`); la fonction est alors suspendue, l'état de la mémoire reste inchangé jusqu'à la prochaine itération. Par exemple: .. code-block:: python :linenos: def fibonacci(): """Générateur *infini* de nombres de Fibonacci.""" a, b = 0, 1 # Initialisation while True: # Boucle infinie (attention!) yield a # La valeur est retournée et la fonction suspendue... a, b = b, a + b # ... avant de reprendre pour l'itération suivante :: >>> fg = fibonacci() # Instanciation / initialisation >>> [ next(fg) for i in range(10) ] # Les 10 premiers éléments [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] À partir de Python 3.7, un générateur s'interrompt lorsqu'il émet (explicitement ou implicitement) un :keyword:`return` (et non plus une exception :exc:`StopIteration`, cf. :pep:`479`): .. code-block:: python :linenos: def fibonacci_limite(max=100): """Générateur *fini* de nombres de Fibonacci.""" a, b = 0, 1 while a <= max: # 'return None' sinon → interruption yield a a, b = b, a + b :: >>> fg = fibonacci_limite(100) >>> list(fg) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] Il est également possible de créer un :term:`générateur en compréhension ` :samp:`( {...} )`:: >>> gen = ( i**2 for i in range(10) ) >>> gen at 0x7fc19d042890> >>> sum(gen) # 0² + 1² + ... 9² 285 Programmation Orientée Objet avancée ==================================== .. https://aboucaud.github.io/slides/2016/python-classes/ Variables de classe ------------------- .. index:: pair: class; variable Il s'agit d'attributs fondamentaux communs à toutes les instances de la classe, contrairement aux attributs d'instance (définis à l'initialisation). .. code-block:: python class MyClass: version = 1.2 # Variable de classe (commun à toutes les instances) def __init__(self, x): self.x = x # Attribut d'instance (spécifique à chaque instance) Méthodes statiques ------------------ .. index:: staticmethod pair: class; méthode statique Ce sont des méthodes qui ne travaillent pas sur une instance (le `self` en premier argument). Elles sont définies à l'aide de la fonction :func:`staticmethod` généralement utilisée en décorateur. Elles sont souvent utilisées pour héberger dans le code d'une classe des méthodes génériques qui y sont liées, mais qui pourrait être utilisées indépendamment (p.ex. des outils de vérification ou de conversion). .. code-block:: python class MyClass: def __init__(self, speed): self.speed = speed # [m/s] @staticmethod def ms_to_kmh(speed): "Conversion m/s → km/h." return speed * 3.6 # [m/s] → [km/h] Une méthode statique peut être invoquée directement via la classe en dehors de toute instanciation (p.ex. `MyClass.ms_to_kmh()`), ou via une instance (p.ex. `self.ms_to_kmh()`). Méthodes de classe ------------------ .. index:: classmethod pair: class; méthode de classe Ce sont des méthodes qui ne travaillent pas sur une instance (`self` en premier argument) mais directement sur la classe elle-même (`cls` en premier argument). Elles sont définies à l'aide de la fonction :func:`classmethod` généralement utilisée en décorateur. Elles sont souvent utilisées pour fournir des méthodes d'instanciation alternatives. .. code-block:: python class MyClass: def __init__(self, x, y): "Initialisation classique." self.x, self.y = x, y @classmethod def init_from_file(cls, filename): "Initialisation à partir d'un fichier." x, y = ... # Lire x et y depuis le fichier. return cls(x, y) # Cette initialisation retourne bien une instance @classmethod def init_from_web(cls, url): "Initialisation à partir d'une URL." x, y = ... # Lire x et y depuis le Web. return cls(x, y) # Cette initialisation retourne bien une instance .. rubric:: Exemple .. literalinclude:: avance.py :pyobject: Date :linenos: Attributs et méthodes privées ----------------------------- Contrairement p.ex. au C++, Python n'offre *pas* de mécanisme de *privatisation* des attributs ou méthodes [#adults]_: * Les attributs/méthodes standards (qui ne commencent pas par `_`) sont publiques, librement accessibles et modifiables (ce qui n'est pas une raison pour faire n'importe quoi): >>> youki = Animal(10.); youki.masse 10.0 >>> youki.masse = -5; youki.masse # C'est vous qui voyez... -5.0 * Les attributs/méthodes qui commencent par un simple `_` sont *réputées* privées (mais sont en fait parfaitement publiques): une interface est généralement prévue (*setter* et *getter*), même si vous pouvez y accéder directement *à vos risques et périls*. .. literalinclude:: avance.py :pyobject: AnimalPrive :linenos: >>> youki = AnimalPrive(10); youki.get_mass() 10.0 >>> youki.set_mass(-5) ValueError: Mass should be a positive float. >>> youki._mass = -5; youki.get_mass() # C'est vous qui voyez... -5.0 * Les attributs/méthodes qui commencent par un double `__` (*dunder*) sont « cachées » (*obfuscated*) sous un nom complexe mais prévisible (cf. :pep:`8`). .. literalinclude:: avance.py :pyobject: AnimalTresPrive :linenos: >>> youki = AnimalTresPrive(10); youki.get_mass() 10.0 >>> youki.__mass = -5; youki.get_mass() # L'attribut __mass n'existe pas sous ce nom... 10.0 >>> youki._AnimalTresPrive__mass = -5; youki.get_mass() # ... mais sous un alias compliqué. -5.0 Propriétés ---------- .. index:: property Compte tenu de la nature foncièrement publique des attributs, le mécanisme des *getters* et *setters* n'est pas considéré comme très pythonique. Il est préférable d'utiliser la notion de :class:`property` (utilisée en décorateur). .. literalinclude:: avance.py :pyobject: AnimalProperty :linenos: >>> youki = AnimalProperty(10); youki.mass 10.0 >>> youki.mass = -5 ValueError: Mass should be a positive float. >>> youki._mass = -5; youki.mass -5.0 Les propriétés sont également utilisées pour accéder à des quantités calculées à la volée à partir d'attributs intrinsèques. .. literalinclude:: avance.py :pyobject: Interval :linenos: >>> i = Interval((0, 10)); i.min, i.middle, i.max (0, 5, 10) >>> i.max = 100 AttributeError: can't set attribute .. note:: les propriétés sont un type simple de :term:`descriptor`, orientées dans la gestion (lecture/écriture) des attributs d'une classe, mais il existe d'autres façons de définir un protocole de type :doc:`descripteur `. Éléments passés sous silence ============================ Il existe encore beaucoup d'éléments passés sous silence: - `for ... else ...` (:ref:`else clause on loops `); - gestion de contexte: :keyword:`with` (:pep:`343`); - :term:`annotations ` de fonctions (:pep:`484`) et de variables (:pep:`526`); - *Positional-only and keyword-only parameters* (v3.8, :pep:`570`); - *Assignment Expressions* (v3.8, :pep:`572`, `walrus operator `_); - *Structural Pattern Matching* (v3.10, :pep:`636`); - :meth:`~.object.__str__` vs. :meth:`~.object.__repr__` et *r-string*; - :meth:`~.object.__new__` (instanciation) vs. :meth:`~.object.__init__` (initialisation); - :ref:`python:metaclasses`; - héritages multiples et méthodes de résolution; - sous-générateur (`yield from ...`, :pep:`380`); - enchaînement d'exception (`raise ... from ...`, :pep:`3134`); - et beaucoup d'autres choses! Ces fonctionnalités peuvent évidemment être très utiles, mais ne sont généralement pas strictement indispensables pour une première utilisation de Python dans un contexte scientifique. Python 3.x ========== Python a connu une remise à plat *non rétrocompatible* du langage entre les versions 2 et 3; la branche 2.x n'est plus supportée depuis janvier 2020. Python 3 apporte des :doc:`changements fondamentaux `, notamment: - :func:`print` n'est plus un mot-clé mais une fonction: :samp:`print({...})`; - l'opérateur `/` ne réalise plus la division euclidienne entre les entiers, mais toujours la division *réelle*; - la plupart des fonctions qui retournaient des itérables en Python 2 — p.ex. :func:`range` — retournent maintenant des itérateurs, plus légers en mémoire; - un support complet des chaînes Unicode; - un nouveau système de formatage des chaînes de caractères (`f-string` du :pep:`498` à partir de Python 3.6); - la fonction de comparaison `cmp` (et la méthode spéciale associée `__cmp__`) n'existe plus [#total]_. .. Note:: La branche 3.x a pris un certain temps pour mûrir, et Python 3 n'est vraiment considéré fonctionnel (et maintenu) qu'à partir de la version 3.5. Inversement, la dernière version supportée de Python 2 a été 2.7. .. _python23: Transition Python 2 à Python 3 ------------------------------ .. Avertissement:: Python 2 n'étant plus supporté depuis le `31/12/2019 `_, il est **indispensable d'utiliser exclusivement Python 3**. Si votre code est encore sous Python 2.x, il existe de nombreux outils facilitant la transition vers 3.x: * L'interpréteur Python 2.7 dispose d'une option `-3` mettant en évidence dans un code les parties qui devront être modifiées pour un passage à Python 3. * Le script `2to3 `__ (2.x)/`2to3 `__ (3.11-) permet d'automatiser la conversion du code 2.x en 3.x. * Python 3.9 est la dernière version à inclure une couche de `rétro-compatibilité `_ avec la branche Python 2. Les constructions dépréciées devront être mises à jour pour les versions à venir. .. rubric:: Liens (plus ou moins obsolètes...) - `Py3 Readiness `_: liste (réduite) des bibliothèques encore non-compatibles avec Python 3 - :doc:`Porting Python 2 Code to Python 3 ` - :rtfd:`The Conservative Python 3 Porting Guide ` - `Python 2/3 compatibility `_ - :rtfd:`Python 3 Patterns, Recipes and Idioms ` - `10 awesome features of Python that you can't use because you refuse to upgrade to Python 3 `_ .. rubric:: Notes de bas de page .. [#global] Je ne parlerai pas ici des `variables globales `_... .. [#adults] *We're all consenting adults.* .. [#total] Voir :func:`functools.total_ordering` pour une alternative. .. |fr| image:: ../_static/france_flag_icon.png :alt: Fr .. |en| image:: ../_static/uk_flag_icon.png :alt: En