Autour du codeDevelopper toujours mieux
Posté le

Classes abstraites et duck typing

Python c'est du duck typing le type n'importe pas du moment que la méthode demandée existe mais alors, a-t-on besoin de classe abstraite ?

Classe abstraite

Pour faire large, une classe abstraite est une classe qui à des méthodes abstraites. Elles peuvent représenter des concepts comme par exemple un moyen de transport ou un repas, on a une idée de ce que l'on peut faire avec, mais on ne sait pas comment c'est concrètement.

Une façon simple de faire une classe abstraite en python, et de créer une classe et faire retourner une exception NotImplementedError aux méthodes que vous souhaitez abstraite.

class Repas:

    def inviter(personne):
        raise NotImplementedError("You must implement foo's %s method" % type(self).__name__)

    def manger(personne):
        raise NotImplementedError("You must implement foo's %s method" % type(self).__name__)

class Repas(object):

    def inviter(personne):
        raise NotImplementedError("You must implement foo's %s method" % type(self).__name__)

    def manger(personne):
        raise NotImplementedError("You must implement foo's %s method" % type(self).__name__)

Le module abc

Sinon vous pouvez utiliser le module abc. abc est l'abréviation de Abstract Base Class, il a était introduit par la PEP 3119. Pour l'utiliser, il faut définir abc.ABCMeta comme la métaclasse et décorer vos méthodes abstraites avec abc.abstractmethod.

Voici un exemple.

import abc


class Foo(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def foo(self):
        pass

import abc


class Foo(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        pass

Une classe qui a une méthode abstraite (Décoré avec abc.abstractmethod) ne peut pas être instanciée. Par contre,ses classes filles pourront être instanciées si elles ont redéfini les méthodes abstraites.

# Foo() # TypeError: Can't instantiate abstract class Foo with abstract methods foo

class Foo2(Foo):
    pass


# Foo2() # TypeError: Can't instantiate abstract class Foo2 with abstract methods foo

class Foo2(Foo):
    def foo(self):
        return 1

Foo2() # OK

# Foo() # TypeError: Can't instantiate abstract class Foo with abstract methods foo

class Foo2(Foo):
    pass


# Foo2() # TypeError: Can't instantiate abstract class Foo2 with abstract methods foo

class Foo2(Foo):
    def foo(self):
        return 1

Foo2() # OK

Le module abc va plus loin, ils vous permet de définir une classe mère abstraite à des classes déjà existantes grâce à la méthode de classe register.

class FooBar1:

    def foo(self):
        return "f1"

    def bar(self):
        return "b1"


class FooBar2:

    def foo(self):
        return "f2"

    def bar(self):
        return "b2"


class AbstractFooBar(metaclass=abc.ABCMeta):

    def foo(self):
        raise NotImplementedError()

    def bar(self):
        raise NotImplementedError()


AbstractFooBar.register(FooBar1)
AbstractFooBar.register(FooBar2)


print(isinstance(FooBar1(), AbstractFooBar)) # True
print(isinstance(FooBar1(), AbstractFooBar)) # True

class FooBar1(object):

    def foo(self):
        return "f1"

    def bar(self):
        return "b1"


class FooBar2(object):

    def foo(self):
        return "f2"

    def bar(self):
        return "b2"


class AbstractFooBar(object):

    __metaclass__ = abc.ABCMeta

    def foo(self):
        raise NotImplementedError()

    def bar(self):
        raise NotImplementedError()


AbstractFooBar.register(FooBar1)
AbstractFooBar.register(FooBar2)


print isinstance(FooBar1(), AbstractFooBar) # True
print isinstance(FooBar1(), AbstractFooBar) # True

Le module collections de python utilise abc, ils post-définis des classes abstraites pour les structures de donnés python.

from collections import MutableSet

isinstance(set(), MutableSet) # True

Duck typing ou classes abstraites

Mais revenons à nos canards. Si l'on utilise le duck typing, on n'a pas besoin de classes de base abstraites.

def call_foo(fooable):
    if not hasattr(fooable, "foo"):
        raise TypeError("first argument of call_foo must have foo method")
    return fooable.foo()[0] * int(fooable.foo()[1:])

class Foo5:
    def foo(self):
        return "f5"


print(call_foo(FooBar1())) # f
print(call_foo(Foo5())) # fffff
print(call_foo(42)) # TypeError: first argument of call_foo must have foo method

def call_foo(fooable):
    if not hasattr(fooable, "foo"):
        raise TypeError("first argument of call_foo must have foo method")
    return fooable.foo()[0] * int(fooable.foo()[1:])

class Foo5(object):
    def foo(self):
        return "f5"


print call_foo(FooBar1()) # f
print call_foo(Foo5()) # fffff
print call_foo(42) # TypeError: first argument of call_foo must have foo method

C'est vrai on peut très bien se passer de classe abstraite pour preuve, il n'y a pas de classe abstraite pour les callable ou les objets qui peuvent s'utiliser avec len. Alors pourquoi les utiliser undecided
? Je vois deux raisons à leur utilisation... La première, centraliser la doc et avoir une interface (ensemble des signatures des méthodes d'un type) explicite. smile
class AbstractFoo:
    """De la doc bien garnie sur Abstract class Foo"""

    def foo(self):
        """
        Cette méthode doit retourner une string formée d'une lettre et d'un nombre.
        """
        raise NotImplementedError()

def call_foo(fooable):
    if not isinstance(fooable, AbstractFoo):
        raise TypeError("First argument of call_foo must be AbstractFoo instance %s given" % type(fooable))
    return fooable.foo()[0] * int(fooable.foo()[1:])

call_foo(1) # TypeError: First argument of call_foo must be AbstractFoo instance <class int=""> given</class>

class AbstractFoo(object):
    """De la doc bien garnie sur Abstract class Foo"""

    def foo(self):
        """
        Cette méthode doit retourner une string formée d'une lettre et d'un nombre.
        """
        raise NotImplementedError()

def call_foo(fooable):
    if not isinstance(fooable, AbstractFoo):
        raise TypeError("First argument of call_foo must be AbstractFoo instance %s given" % type(fooable))
    return fooable.foo()[0] * int(fooable.foo()[1:])

call_foo(1) # TypeError: First argument of call_foo must be AbstractFoo instance <class int=""> given</class>

En cas d'erreur, vous irez voir AbstractFoo et par la même occasion la méthode foo.Ce sera alors clair pour vous de ce que doit faire la méthode foo. Je me vois mal expliquer cela dans call_foo surtout si je fait une librairie avec des dizaines de fonctions prenant des fooable.

La deuxième raison est que si un objet a beaucoup de méthodes à redéfinir, c'est plus facile de faire un seul isinstance à l'entrer d'une fonction que de un hasattr par méthode.

def call_foobar(foobar):
    if not hasattr(foobar, 'foo'):
        raise TypeError("where is foo method ?")
    if not hasattr(foobar, 'bar'):
        raise TypeError("where is barton killer ?")

def call_foobar(foobar):
    if not hasattr(foobar, 'foo'):
        raise TypeError("where is foo method ?")
    if not hasattr(foobar, 'bar'):
        raise TypeError("where is barton killer ?")

Bref, faire des classes abstraites en python permettent de cadrer un peu les choses car si le duck typing apporte de la souplesse, il peut aussi apporter de la confusion et il faut trouver le juste milieu. Voilou, j'espère que ça vous a plus alors à vos IDE et bon code. smile