.. _cours2: Python avancé ############# .. contents:: Table des matières :local: .. 1 Fonctionnalités avancées 1.1 Arguments anonymes 1.2 Dépaquetage des arguments 1.3 Dépaquetage des itérables 1.4 Décorateurs 1.5 Fonction anonyme 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 ------------------------- .. 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} Dépaquetage des itérables ------------------------- .. index:: pair: dépaquetage; * 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) 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 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'ajouter 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: .. rubric:: Exemple 2: `monkey patching `_ (modification à la volée des propriétés d'un objet) .. literalinclude:: avance.py :pyobject: make_method :linenos: .. rubric:: Liens: - `Python et les décorateurs `_ |fr| - `Primer on Python Decorators `_ - `A guide to Python's function decorators `_ - `Python Decorator Library `_ 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 ` 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 » 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 >>> c._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 Éléments passés sous silence ============================ Il existe encore beaucoup d'éléments passés sous silence: - :term:`iterator` (:func:`next`) et :term:`generator` (:keyword:`yield`); - gestion de contexte: :keyword:`with` (:pep:`343`); - annotations de fonctions (:pep:`484`) et de variables (:pep:`526`); - `__str__` vs. `__repr__` et *r-string*, `__new__` (instanciation) vs. `__init__` (initialisation); - *class factory*; - héritages multiples et méthodes de résolution; - etc. 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 ========== Pour des raisons historiques autant que pratiques [#py3]_, ce cours présentait initialement le langage Python dans sa version 2. Cependant, puisque le développement actuel de Python (et de certaines de ses bibliothèques clés) se fait maintenant uniquement sur la branche 3.x, qui constitue une remise à plat *non rétrocompatible* du langage, et que la branche 2.x n'est plus supporté depuis janvier 2020 (:pep:`466`), le cours a été porté sur Python 3. Python 3 apporte :doc:`quelques 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 (mais encore complexe) 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é, il est dorénavant indispensable d'utiliser exclusivement Python 3. Si votre code est encore sous Python 2.x, il existe de nombreux outils permettant de **faciliter** la transition vers 3.x (mais pas de la repousser *ad eternam*): * 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 `_ permet d'automatiser la conversion du code 2.x en 3.x. * La bibliothèque standard :mod:`__future__` permet d'introduire des constructions 3.x dans un code 2.x, p.ex.:: from __future__ import print_function # Fonction print() from __future__ import division # Division non-euclidienne print(1/2) # Affichera '0.5' * La bibliothèque *non* standard :rtfd:`six` fournit une couche de compatibilité 2.x-3.x, permettant de produire de façon transparente un code compatible simultanément avec les deux versions. .. rubric:: Liens - `Py3 Readiness `_: liste (réduite) des bibliothèques encore non-compatibles avec Python 3 - `Porting Python 2 Code to Python 3 `_ - :rtfd:`The Conservative Python 3 Porting Guide ` - `Python 2/3 compatibility `_ .. rubric:: Notes de bas de page .. [#global] Je ne parlerai pas ici des `variables globales `_... .. [#adults] *We're all consenting adults.* .. [#py3] De nombreuses distributions Linux utilisent encore des outils Python 2.7. .. [#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