Journal python: ellipsis operator (...)

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
29
15
sept.
2022

J'ai découvert l'ellipsis operator de python (...). Dans le contexte où je l'utilise, c'est équivalent à pass, autrement dit ne rien faire. C'est utilisé principalement pour quand python attend qu'un bloc syntaxique soit rempli (corps d'une fonction, d'une boucle, …), mais qu'on a vraiment rien à y faire. Je trouve que ça permet de faire des interfaces plus élégantes.

from abc import ABC, abstractmethod

class CarElementVisitor(ABC):
    @abstractmethod
    def visitBody(self, element):
        ...

    @abstractmethod
    def visitEngine(self, element):
        ...

    @abstractmethod
    def visitWheel(self, element):
        ...

    @abstractmethod
    def visitCar(self, element):
        ...

plutôt que

from abc import ABC, abstractmethod

class CarElementVisitor(ABC):
    @abstractmethod
    def visitBody(self, element):
        raise NotImplementedError

    @abstractmethod
    def visitEngine(self, element):
        raise NotImplementedError

    @abstractmethod
    def visitWheel(self, element):
        raise NotImplementedError

    @abstractmethod
    def visitCar(self, element):
        raise NotImplementedError

La perte de l'exception n'est pas un problème car elle n'est de toute façon jamais lancé, abc (Abstract Base Classes, module python ajoutant les notions de classes abstraites et d'interfaces s'occupant d'en lancer une automatiquement:

$ ipython
Python 3.10.6 (main, Aug  3 2022, 17:39:45) [GCC 12.1.1 20220730]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from abc import ABC, abstractmethod
   ...: 
   ...: class CarElementVisitor(ABC):
   ...:     @abstractmethod
   ...:     def visitBody(self, element):
   ...:         ...
   ...: 
   ...:     @abstractmethod
   ...:     def visitEngine(self, element):
   ...:         ...
   ...: 
   ...:     @abstractmethod
   ...:     def visitWheel(self, element):
   ...:         ...
   ...: 
   ...:     @abstractmethod
   ...:     def visitCar(self, element):
   ...:         ...
   ...: 

In [2]: CarElementVisitor()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 CarElementVisitor()

TypeError: Can't instantiate abstract class CarElementVisitor with abstract methods visitBody, visitCar, visitEngine, visitWheel
  • # Docstring

    Posté par  . Évalué à 5.

    Merci, je connaissais pas.

    Pour ces cas de figure, j'utilise souvent une docstring, ça suffit à faire plaisir à Python, pas besoin d'écrire pass, raise ou … et ça documente la méthode (en disant par exemple qu'il faut la surcharger et comment).

    Un avantage de classes abstraites, c'est que l'exception pète à l'instanciation, pas à l'exécution de la méthode.

    • [^] # Re: Docstring

      Posté par  (site web personnel, Mastodon) . Évalué à 6.

      Plutôt que ... dans son exemple

          @abstractmethod
          def visitOther(self, element):
              ...

      Je préfère de loin le raise …ou

          @abstractmethod
          def visitOther(self, element):
              raise NotImplementedError

      …ou alors le pass …et

          @abstractmethod
          def visitBody(self, element):
              pass

      …et dans tous les cas la documentation aussi.
      C'est vrai que si ça peut se suffire, c'est encore mieux. Merci pour le tuyau.

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: Docstring

        Posté par  . Évalué à 4.

        Oui, dans ce cas je préfère encore

                @abstractmethod
                def visitBody(self, element):
                    """Visit body
        
                    Subclass this in child classes to define how to visit body
                    """

        Et il y a besoin de rien d'autre. (La coloration syntaxique a l'air de merder. Tant pis.)

  • # Autre usage de l'ellipsis

    Posté par  . Évalué à 4.

    Ellipsis est aussi un objet. Ça peut être utile quand tu veux mette une valeur par défaut à une fonction qui peu prendre None en entrée.

    def toto(arg1=...):
        if arg1 == ...:
            arg1= "pouet"
        pass # la suite de la fonction

    Cette façon de faire est intéressante parce qu’en pratique ellipsis n'est jamais utilisé donc on aura pas de faux positif.

    • [^] # Re: Autre usage de l'ellipsis

      Posté par  (site web personnel, Mastodon) . Évalué à 6.

      Intéressant… Mais je préfère quand même un None qui me semble plus lisible (là mon cerveau traduit anything ou juste etc. et c'est plus confusant.)

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: Autre usage de l'ellipsis

        Posté par  . Évalué à 2.

        Je préfère aussi None mais dans certains cas ce n'est pas une option car c'est une valeur acceptée en entrée qui est différente de la valeur par défaut. En pratique j’utilise très rarement ... comme valeur par défaut, c'est vraiment mon dernier recours.

        • [^] # Re: Autre usage de l'ellipsis

          Posté par  . Évalué à 5. Dernière modification le 16 septembre 2022 à 14:25.

          Parfois, quand None est une valeur attendue, la lib utilise un singleton maison pour représenter l'absence de valeur. C'est le cas dans pandas, il me semble.

  • # Une autre utilisation d'Ellipsis

    Posté par  (Mastodon) . Évalué à 4.

    Quand on souhaite annoter un tuple dont on ne connait pas la longueur a priori, on peut l'utiliser aussi…

    from typing import Tuple, Sequence
    
    def ma_func(foo: Tuple[str,...]) -> Sequence[int]:
        """Get the length of each items of a Tuple, as a Sequence."""
        output = [len(item) for item in foo]
        return output

    Et je l'utilise aussi pour les classes abstraites ! (et j'abuse des annotations…)

    • [^] # Re: Une autre utilisation d'Ellipsis

      Posté par  . Évalué à 3.

      Moi je connaissais dans le contexte numpy, où l'opérateur ellipsis est utilisé pour faire du "slicing". Par exemple, avec un tableau multidimensionnel A, A[0,...] signifie qu'on prend le premier élément de la première dimension, et tous les éléments des dimensions restantes. Ça équivaut par exemple à A[0,:,:,:] pour un tableau à 4 dimensions, mais reste valable quel que soit le nombre de dimensions.

  • # Surcharge

    Posté par  . Évalué à 4. Dernière modification le 18 septembre 2022 à 21:46.

    Cela permet aussi de déclarer plusieurs prototypes pour une fonction et ainsi mieux documenter les usages:

    import typing
    
    class range(object):
        @typing.overload
        def __init__(self, high: int) -> None:
            ...
    
        @typing.overload
        def __init__(self, low: int, high: int) -> None:
            ...
    
        def __init__(self, *args: int) -> None:
            # Implémentation ici.
            pass
    • [^] # Re: Surcharge

      Posté par  . Évalué à 3.

      Dans ce cas-là, je pense qu'on met ce qu'on veut dans le corps de la fonction. On pourrait aussi mettre pass ou bien une docstring. Mais les exemples de @typing.overload utilisent en effet ....

  • # numpy

    Posté par  . Évalué à 2.

    Pour compléter, je pense que ça été créé pour numpy à la base. Mais j'ai pas trouvé de source.

    C'est bien connu des data scientists, pour accéder à des tableaux de données multidimensionnelles

    # Un tableau de 4 dimensions vide
    a = numpy.zeros((10, 5, 3, 2))
    
    a[0]           # pour indexer le premier axe c'est facile
    a[0, :, :, :]  # c'est la même chose
    
    a[:, :, :, 0]  # le dernier axe c'est plus chiant
    a[..., 0]      # et du coup on peut faire ça plus simplement
    

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.