4. Python avancé🔗

4.1. 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 1.

4.1.1. Arguments anonymes🔗

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…

4.1.2. Dépaquetage des arguments🔗

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}

4.1.3. Dépaquetage des itérables🔗

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)

4.1.4. Décorateurs🔗

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.

1
2
3
4
5
6
7
8
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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.

Exemple 1: ajouter un attribut à une fonction/méthode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def add_attrs(**kwargs):
    """
    Decorator adding attributes to a function, e.g.
    ::

      @attrs(source='NIST/IAPWS')
      def func(...):
          ...
    """

    def decorate(f):
        for key, val in kwargs.iteritems():
            setattr(f, key, val)
        return f

    return decorate

Exemple 2: monkey patching (modification à la volée des propriétés d’un objet)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def make_method(obj):
    """
    Decorator to make the function a method of `obj` (*monkey patching*), e.g.
    ::

      @make_method(MyClass)
      def func(myClassInstance, ...):
          ...

    makes `func` a method of `MyClass`, so that one can directly use::

      myClassInstance.func()
    """

    def decorate(f):
        setattr(obj, f.__name__, f)
        return f

    return decorate

Liens:

4.1.5. Fonction anonyme🔗

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 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:   <lambda>
Arguments:  (1, 2) {}
Result:     3

La définition d’une fonction 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: Functional Programming

4.2. Programmation Orientée Objet avancée🔗

4.2.1. Variables de classe🔗

Il s’agit d’attributs fondamentaux communs à toutes les instances de la classe, contrairement aux attributs d’instance (définis à l’initialisation).

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)

4.2.2. Méthodes statiques🔗

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 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).

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()).

4.2.3. Méthodes 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 classmethod() généralement utilisée en décorateur.

Elles sont souvent utilisées pour fournir des méthodes d’instanciation alternatives.

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

Exemple

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Date:
    "Source: https://stackoverflow.com/questions/12179271"

    def __init__(self, day=0, month=0, year=0):
        """Initialize from day, month and year values (no verification)."""

        self.day = day
        self.month = month
        self.year = year

    @classmethod
    def from_string(cls, astring):
        """Initialize from (verified) 'day-month-year' string."""

        if cls.is_valid_date(astring):
            day, month, year = map(int, astring.split('-'))

            return cls(day, month, year)
        else:
            raise IOError(f"{astring!r} is not a valid date string.")

    @staticmethod
    def is_valid_date(astring):
        """Check validity of 'day-month-year' string."""

        try:
            day, month, year = map(int, astring.split('-'))
        except ValueError:
            return False
        else:
            return (0 < day <= 31) and (0 < month <= 12) and (0 < year <= 2999)

4.2.4. 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 2:

  • 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.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class AnimalPrive:
    
        def __init__(self, mass):
    
            self.set_mass(mass)
    
        def set_mass(self, mass):
            """Setter de l'attribut privé `mass`."""
    
            if float(mass) < 0:
                raise ValueError("Mass should be a positive float.")
    
            self._mass = float(mass)
    
        def get_mass(self):
            """Getter de l'attribut privé `mass`."""
    
            return self._mass
    
    >>> 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).

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class AnimalTresPrive:
    
        def __init__(self, mass):
    
            self.set_mass(mass)
    
        def set_mass(self, mass):
            """Setter de l'attribut privé `mass`."""
    
            if float(mass) < 0:
                raise ValueError("Mass should be a positive float.")
    
            self.__mass = float(mass)
    
        def get_mass(self):
            """Getter de l'attribut privé `mass`."""
    
            return self.__mass
    
    >>> 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
    

4.2.5. Propriétés🔗

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 property (utilisée en décorateur).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class AnimalProperty:

    def __init__(self, mass):

        self.mass = mass        # Appelle le setter de la propriété

    @property
    def mass(self):             # Propriété mass (= getter)

        return self._mass

    @mass.setter
    def mass(self, mass):       # Setter de la propriété mass

        if float(mass) < 0:
            raise ValueError("Mass should be a positive float.")

        self._mass = float(mass)
>>> 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Interval:

    def __init__(self, minmax):
        """Initialisation à partir d'un 2-tuple."""

        self._range = _, _ = minmax  # Test à la volée

    @property
    def min(self):
        """La propriété min est simplement _range[0]. Elle n'a pas de setter."""

        return self._range[0]

    @property
    def max(self):
        """La propriété max est simplement _range[1]. Elle n'a pas de setter."""

        return self._range[1]

    @property
    def middle(self):
        """La propriété middle est calculée à la volée. Elle n'a pas de setter."""

        return (self.min + self.max) / 2
>>> i = Interval((0, 10)); i.min, i.middle, i.max
(0, 5, 10)
>>> i.max = 100
AttributeError: can't set attribute

4.3. Éléments passés sous silence🔗

Il existe encore beaucoup d’éléments passés sous silence:

  • iterator (next()) et generator (yield);

  • gestion de contexte: 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.

4.4. Python 3.x🔗

Pour des raisons historiques autant que pratiques 3, 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 quelques changements fondamentaux, notamment:

  • print() n’est plus un mot-clé mais une fonction: 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. 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 4.

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.

4.4.1. 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 __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 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.

Liens

Notes de bas de page

1

Je ne parlerai pas ici des variables globales

2

We’re all consenting adults.

3

De nombreuses distributions Linux utilisent encore des outils Python 2.7.

4

Voir functools.total_ordering() pour une alternative.