Explication de code : callback à la mise à jour d’un array Numpy


Toujours dans l’esprit de l’explication de code, voici un petit bout de numpyries envoyé par un lecteur.

Lisez l’article sur les callbacks si vous n’êtes pas familiers avec le principe, et en avant:

# on importe numpy, une bibliothèque spécialisée dans le calculs impliquant de grands
# jeux de nombres et très utilisée par les scientifiques
import numpy as np
 
# On créé une classe qui hérite du type np.ndarray qui est un type d'array
# de taille fixe, et dont tous les objets doivent être du même type,
# mais qui peut avoir plusieurs dimensions et qui est très rapide à manipuler
# Notez que par convention NumPy n'utilise pas de majuscule pour le nom de 
# ces types pour matcher str, int, unicode, etc. Pour rester congruent,
# la classe enfant ne le fait pas non plus. Ne prenez pas cette habitude, 
# c'est un cas particulier.
class cbarray(np.ndarray):
 
 
    # __new__ est la seule méthode de classe par défaut, sans déclaration de @classmethode, 
    # donc le premier argument sera la classe en court. Normalement la convention
    # est d'appeler cet argument cls, mais ici l'auteur fait à sa sauce...
    #
    # Le reste des arguments sont les mêmes que pour __init__: ce sont ceux
    # attendus à l'instanciation de la classe cbarray:
    #   * data sont les données qu'on veut mettre dans l'array
    #   * cb est un callback qu'on appelera à chaque mise à jour de l'array
    #   * dtype est le type des données à mettre dans l'array (si on veut bypasser l'autodétection)
    #   * copy pour présicer si on veut copier les données, ou juste liér les données existantes
    # 
    # __new__ est appelée avant __init__: elle prend les paramètres qu'attend __init__, 
    # fabrique une instance, la retournepuis __init__ est appelée avec l'instance. 
    # On a rarement besoin d'overrider __new__, en générale __init__ est un meilleur choix
    # car c'est plus simple. Mais certains types NumPy ne permettent pas de faire 
    # autrement.
    def __new__(subtype, data, cb=None, dtype=None, copy=False):
 
        # On attache le callback à la CLASSE, et non à l'instance.
        # Ca peut paraitre étrange, mais on verra plus bas pour
        subtype.__defaultcb = cb
 
        # Selon que l'on souhaite copier les données, ou juste les lier
        # on créé une instance d'un type différent.
        # Notez que l'absence d'espace atour de "," et "=" n'est pas recommandé
        # pas le PEP8
        if copy:
            data = np.array(data,dtype=dtype)
        else:
            data = np.asarray(data,dtype=dtype)
 
        # Cette astuce permet d'utiliser un des deux types du dessus
        # mais au travers de l'API du type "subtype", c'est à dire notre 
        # classe.
        data = data.view(subtype)
 
        # On retourne l'instance ainsi créé.
        # Quand l'utilisateur fera cbarray(....), c'est cette instance
        # qu'il recevra
        return data
 
    # Juste une méthode qui appelle le callback si il existe
    def _notify(self):
        if self.cb is not None:
            self.cb()
 
    # Une propriété qui retourne un attribut de l'objet
    # en s'assurant qu'on utilise celui du parent.
    # C'est une supposition mais je pense que shape est une propriété du
    # parent, qu'elle n'est pas dispo sur le type array ou asarray, et 
    # que l'astuce data.view ne suffit pas à faire un proxy de celle-ci.
    # Donc je pense que ça sert à donner accès à cette donnée.
    def _get_shape(self):
        return super(cbarray, self).shape
    shape = property(_get_shape)
 
    # __setitem__ est une méthode "magique", appelée automatiquement qu'on
    # fait array[item] = val
    # Ici on l'utilise pour appeler _notify() à chaque mise à jour de l'array
    # et donc d'appeler le callback à chaque mise à jour.
    # Il est dommage de ne pas passer de paramètre à la méthode, comme 
    # l'ancienne valeur, l'item et la nouvelle valeur. Le callback va être
    # du coup assez limité. Mais ça suffira si par exemple tout ce qu'on
    # veut faire c'est écrire dans un fichier à chaque modification.
    def __setitem__(self, item, val):
        np.ndarray.__setitem__(self, item, val)
        self._notify()
 
    # NumPy permet de créer des sous types à partir du type de base, c'est
    # d'ailleurs une manière très courrante de créer des nouveaux conteneurs
    # de données. Mais ce faisant, NumPy bypass le mécanisme d'instanciation,
    # et __new__ et __init__ ne sont donc pas appelées. Pour y pallier, NumPy
    # ajoute la méthode __array_finalize__ qui est toujours appelée quand
    # un array est prêt à être utilisé. Elle peut être utilisée pour effectuer
    # un traitement pour chaque array créé, quelque soit sa provenance.
    # Ici, on l'utilise pour attacher le callback à "l'instance".
    # Souvenez-vous, plus haut on avait attaché le callback à la CLASSE.
    # Cette classe peut derrière être la source de nombreux arrays même si
    # __init__ n'est pas appelé pour eux :-(
    # La solution de l'auteur est donc de passer le callback à __new__, de 
    # l'attacher à la classe, et à travers __array_finalize__, de l'attacher
    # à "l'instance". Il faut garder en tête que tout nouvel appel à __new__
    # écrasera le callback pour toutes les instances suivantes. Mis à part
    # cela, ceci garanti que tout array aura le callback, et donc que 
    # _notify aura accès au callback, et donc que __setitem__ déclenchera le 
    # callback, et donc que la fonction sera bien appelée à chaque mise à jour
    # de l'array
    def __array_finalize__(self,obj):
        if not hasattr(self, "cb"):
            # The object does not yet have a `.cb` attribute
            self.cb = getattr(obj,'cb',self.__defaultcb)
 
    # Encore une méthode "magique" ajoutée par NumPy. Elle est appelée
    # quand on sérialise l'array et retourne des informations sur l'état
    # de l'array
    def __reduce__(self):
        object_state = list(np.ndarray.__reduce__(self))
        subclass_state = (self.cb,)
        object_state[2] = (object_state[2],subclass_state)
        return tuple(object_state)
 
    # inverse de __reduce__
    def __setstate__(self,state):
        nd_state, own_state = state
        np.ndarray.__setstate__(self,nd_state)
 
        cb, = own_state
        self.cb = cb
 
# Le callback qui doit être appelé à chaque mise à jour de l'array
# Je ne pense pas que ça puisse marcher, car il attend un argument,
# ce que _notify() ne lui passe pas.
def callback(arg):
    print 'array changed to',arg
 
 
# une petit démo du code si on run le script au lieu de l'importer
if __name__ == '__main__':
    x = cbarray([1,2,3], cb=callback)
    x[[0,1]] = 1.0

2 thoughts on “Explication de code : callback à la mise à jour d’un array Numpy

  • Gael Varoquaux

    Attention, il faut bien avoir en tête qu’il est très facile de prendre une vue des données qui ne lancera pas le callback. Beaucoup de code fait cela un peu partout.

  • Sam Post author

    Hello Gael. Comme c’est une explication de code (donc pas notre code) et en particulier sur un code NumPy (qui n’est pas mon domaine d’expertise), une explication et un snippet de démonstration de ce dont tu veux parler pourrait avoir une vraie valeur ajoutée dans les commentaires.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.