1. Python avancé🔗

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

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

1.1.2. Dépaquetage des arguments et des itérables🔗

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)

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

1def compute_and_print(fn, *args, **kwargs):
2
3    print("Function:  ", fn.__name__)
4    print("Arguments: ", args, kwargs)
5    result = fn(*args, **kwargs)
6    print("Result:    ", result)
7
8    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.

 1def verbose(fn):       # fonction → fonction décorée
 2
 3    def decorated(*args, **kwargs):
 4        print("Function:  ", fn.__name__)
 5        print("Arguments: ", args, kwargs)
 6        result = fn(*args, **kwargs)
 7        print("Result:    ", result)
 8
 9        return result
10
11    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.

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

 1def add_attrs(**kwargs):
 2    """
 3    Decorator adding attributes to a function, e.g.
 4    ::
 5
 6      @add_attrs(name="O'Donnell 94", reference="1994ApJ...422..158O")
 7      def ext_od94(...):
 8          ...
 9
10    will set attributes `ext_od94.name` and `ext_od94.reference`.
11    """
12
13    def decorate(f):
14        for key, val in kwargs.items():
15            setattr(f, key, val)
16        return f
17
18    return decorate

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

 1def make_method(obj):
 2    """
 3    Decorator to make the function a method of `obj` (*monkey patching*), e.g.
 4    ::
 5
 6      @make_method(MyClass)
 7      def func(myClassInstance, ...):
 8          ...
 9
10    makes `func` a method of `MyClass`, so that one can directly use::
11
12      myClassInstance.func()
13    """
14
15    def decorate(f):
16        setattr(obj, f.__name__, f)
17        return f
18
19    return decorate

Exemple 3: Exception-catching decorator (PEP 463)

 1def exception_catcher(exception, default=None):
 2    """
 3    Exception-catching decorator.
 4
 5    This is similar to :pep:`463` (Exception-catching expressions) so one
 6    can use exception-throwing functions in a comprehension list.  E.g., to
 7    find index of elements of list2 in list1:
 8
 9    >>> list1 = [0, 1, 2, 3, 4]; list2 = [1, 3, 5, 7]
10    >>> [ list1.index(_) for _ in list2 ]  # ValueError: some elements of list2 are not in list1
11    ValueError: 5 is not in list
12    >>> safe_index = exception_catcher(ValueError)(lambda alist, val: alist.index(val))
13    >>> [ safe_index(list1, _) for _ in list2 ]  # Unmatched elements return None
14    [1, 3, None, None]
15    """
16
17    def decorate(f):
18
19        def decorated_f(*args, **kwargs):
20            try:
21                return f(*args, **kwargs)  # No exception: return result
22            except exception:
23                return default             # Catch exception and return default
24
25        return decorated_f  # Decorated function: x → f(x)
26
27    return decorate         # Decorator: f → decorate(f)

Liens:

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

1.1.5. Itérateurs et générateurs🔗

Un objet itérable est un objet implémentant le protocole d'itération iter() (initialisation) et 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
<str_iterator at 0x7fc19cf74310>
>>> 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'

1.1.5.1. Itérateurs🔗

Il est possible de créer ses propres itérateurs en implémentant les méthodes spéciales __iter__() (pour l'initialisation) et __next__() (pour l'itération), p.ex.

 1class Fibonacci:
 2    """
 3    Itérateur retournant les nombres de Fibonacci.
 4    """
 5
 6    def __init__(self, maximum=100):
 7        """
 8        Itérateur jusqu'à la valeur maximale maximum.
 9        Fixer max à 0 ou None pour un itérateur infini.
10        """
11
12        self.maximum = maximum
13        self.i = 0
14        self.inext = 1
15
16    def __iter__(self):
17        """
18        Surcharge de la fonction iter(self), appelée
19        automatiquement par l'initialisation de l'itérateur
20        (`for ... in ...`).
21        """
22
23        return self
24
25    def __next__(self):
26        """
27        Surcharge de la fonction next(self), appelée
28        lors de l'itération.
29        """
30
31        if not self.maximum or self.i <= self.maximum:
32            i = self.i
33            self.i, self.inext = self.inext, self.i + self.inext
34            return i
35        else:
36            raise StopIteration  # Fin de l'itérateur
>>> [ 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!!!!!!!

1.1.5.2. Générateurs🔗

En pratique, il n'est souvent pas nécessaire ni pythonique de définir son propre itérateur. On utilise plutôt un générateur, i.e. une fonction retournant un itérateur:

  • le premier appel de la fonction instancie et initialise l'itérateur,

  • à chaque nouvel appel de l'itérateur, il retourne la valeur suivante (yield); la fonction est alors suspendue, l'état de la mémoire reste inchangé jusqu'à la prochaine itération.

Par exemple:

1def fibonacci():
2    """Générateur *infini* de nombres de Fibonacci."""
3
4    a, b = 0, 1          # Initialisation
5    while True:          # Boucle infinie (attention!)
6        yield a          # La valeur est retournée et la fonction suspendue
7        a, b = b, a + b  # Préparation de 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 return (et non plus une exception StopIteration, cf. PEP 479):

1def fibonacci_limite(max=100):
2    """Générateur *fini* de nombres de Fibonacci."""
3
4    a, b = 0, 1
5    while a <= max:  # 'return None' sinon → interruption
6        yield a
7        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 générateur en compréhension ... ):

>>> gen = ( i**2 for i in range(10) )
>>> gen
<generator object <genexpr> at 0x7fc19d042890>
>>> sum(gen)  # 0² + 1² + ... 9²
285

1.2. Programmation Orientée Objet avancée🔗

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

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

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

 1class Date:
 2    "Source: https://stackoverflow.com/questions/12179271"
 3
 4    def __init__(self, day=0, month=0, year=0):
 5        """Initialize from day, month and year values (no verification)."""
 6
 7        self.day = day
 8        self.month = month
 9        self.year = year
10
11    @classmethod
12    def from_string(cls, astring):
13        """Initialize from (verified) 'day-month-year' string."""
14
15        if not cls.is_valid_date(astring):
16            raise IOError(f"{astring!r} is not a valid date string.")
17
18        day, month, year = map(int, astring.split('-'))
19
20        return cls(day, month, year)
21
22    @staticmethod
23    def is_valid_date(astring):
24        """Check validity of 'day-month-year' string."""
25
26        try:
27            day, month, year = map(int, astring.split('-'))
28        except ValueError:
29            return False
30        else:
31            return (0 < day <= 31) and (0 < month <= 12) and (0 < year <= 2999)

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

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

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

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

 1class AnimalProperty:
 2
 3    def __init__(self, mass):
 4
 5        self.mass = mass        # Appelle le setter de la propriété
 6
 7    @property
 8    def mass(self):             # Propriété mass (= getter)
 9
10        return self._mass
11
12    @mass.setter
13    def mass(self, mass):       # Setter de la propriété mass
14
15        if float(mass) < 0:
16            raise ValueError("Mass should be a positive float.")
17
18        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.

 1class Interval:
 2
 3    def __init__(self, minmax):
 4        """Initialisation à partir d'un 2-tuple."""
 5
 6        self._range = _, _ = minmax  # Test à la volée
 7
 8    @property
 9    def min(self):
10        """La propriété min est simplement _range[0]. Elle n'a pas de setter."""
11
12        return self._range[0]
13
14    @property
15    def max(self):
16        """La propriété max est simplement _range[1]. Elle n'a pas de setter."""
17
18        return self._range[1]
19
20    @property
21    def middle(self):
22        """La propriété middle est calculée à la volée. Elle n'a pas de setter."""
23
24        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

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

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

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.

1.4. 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 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 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 [3].

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.

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

Liens (plus ou moins obsolètes…)

Notes de bas de page