web-dev-qa-db-fra.com

Comprendre la métaclasse et l'héritage dans Python

J'ai une certaine confusion concernant les méta-classes.

Avec héritage

class AttributeInitType(object):

   def __init__(self,**kwargs):
       for name, value in kwargs.items():
          setattr(self, name, value)

class Car(AttributeInitType):

    def __init__(self,**kwargs):
        super(Car, self).__init__(**kwargs)
    @property
    def description(self):
       return "%s %s %s %s" % (self.color, self.year, self.make, self.model)

c = Car(make='Toyota', model='Prius', year=2005, color='green')
print c.description

Avec méta-classe

class AttributeInitType(type):
   def __call__(self, *args, **kwargs):
       obj = type.__call__(self, *args)
       for name, value in kwargs.items():
           setattr(obj, name, value)
       return obj

class Car(object):
   __metaclass__ = AttributeInitType

   @property
   def description(self):
       return "%s %s %s %s" % (self.color, self.year, self.make, self.model)


c = Car(make='Toyota', model='Prius', year=2005,color='blue')
print c.description

Comme l'exemple ci-dessus n'est pas utile dans la pratique, mais juste pour comprendre,

J'ai quelques questions comme,

  1. Qu'est-ce que la métaclasse et quand dois-je l'utiliser?

  2. Quelle est la différence/similitude entre une méta-classe et l'héritage?

  3. Où doit-on utiliser une méta-classe ou l'héritage?

55
Nikhil Rupanawar

1) À quoi sert la métaclasse et quand l'utiliser?

Les métaclasses sont aux classes comme les classes aux objets. Ce sont des classes pour les classes (d'où l'expression "méta").

Les métaclasses sont généralement utilisées lorsque vous souhaitez travailler en dehors des contraintes normales de la POO.

2) Quelle est la différence/similitude entre la métaclasse et l'héritage?

Une métaclasse ne fait pas partie de la hiérarchie des classes d'un objet contrairement aux classes de base. Ainsi, lorsqu'un objet fait obj.some_method(), il ne recherchera pas la métaclasse pour cette méthode, mais la métaclasse peut l'avoir créée lors de la création de la classe ou de l'objet.

Dans cet exemple ci-dessous, la métaclasse MetaCar donne aux objets un attribut defect basé sur un nombre aléatoire. L'attribut defect n'est défini dans aucune des classes de base des objets ou dans la classe elle-même. Cependant, cela aurait pu être réalisé en utilisant uniquement des classes.

Cependant (contrairement aux classes), cette métaclasse réoriente également la création d'objets; dans le some_cars liste, tous les Toyotas sont créés en utilisant la classe Car. La métaclasse détecte que Car.__init__ contient un argument make qui correspond à une classe préexistante de ce nom et renvoie donc un objet de cette classe à la place.

De plus, vous remarquerez également que dans le some_cars liste, Car.__init__ est appelé avec make="GM". Une classe GM n'a pas été définie à ce stade de l'évaluation du programme. La métaclasse détecte qu'une classe n'existe pas sous ce nom dans l'argument make, elle en crée donc une et met à jour l'espace de noms global (elle n'a donc pas besoin d'utiliser le mécanisme de retour). Il crée ensuite l'objet à l'aide de la classe nouvellement définie et le renvoie.

import random

class CarBase(object):
    pass

class MetaCar(type):
    car_brands = {}
    def __init__(cls, cls_name, cls_bases, cls_dict):
        super(MetaCar, cls).__init__(cls_name, cls_bases, cls_dict)
        if(not CarBase in cls_bases):
            MetaCar.car_brands[cls_name] = cls

    def __call__(self, *args, **kwargs):
        make = kwargs.get("make", "")
        if(MetaCar.car_brands.has_key(make) and not (self is MetaCar.car_brands[make])):
            obj = MetaCar.car_brands[make].__call__(*args, **kwargs)
            if(make == "Toyota"):
                if(random.randint(0, 100) < 2):
                    obj.defect = "sticky accelerator pedal"
            Elif(make == "GM"):
                if(random.randint(0, 100) < 20):
                    obj.defect = "shithouse"
            Elif(make == "Great Wall"):
                if(random.randint(0, 100) < 101):
                    obj.defect = "cancer"
        else:
            obj = None
            if(not MetaCar.car_brands.has_key(self.__name__)):
                new_class = MetaCar(make, (GenericCar,), {})
                globals()[make] = new_class
                obj = new_class(*args, **kwargs)
            else:
                obj = super(MetaCar, self).__call__(*args, **kwargs)
        return obj

class Car(CarBase):
    __metaclass__ = MetaCar

    def __init__(self, **kwargs):
        for name, value in kwargs.items():
            setattr(self, name, value)

    def __repr__(self):
        return "<%s>" % self.description

    @property
    def description(self):
        return "%s %s %s %s" % (self.color, self.year, self.make, self.model)

class GenericCar(Car):
    def __init__(self, **kwargs):
        kwargs["make"] = self.__class__.__name__
        super(GenericCar, self).__init__(**kwargs)

class Toyota(GenericCar):
    pass

colours = \
[
    "blue",
    "green",
    "red",
    "yellow",
    "orange",
    "purple",
    "silver",
    "black",
    "white"
]

def Rand_colour():
    return colours[random.randint(0, len(colours) - 1)]

some_cars = \
[
    Car(make="Toyota", model="Prius", year=2005, color=Rand_colour()),
    Car(make="Toyota", model="Camry", year=2007, color=Rand_colour()),
    Car(make="Toyota", model="Camry Hybrid", year=2013, color=Rand_colour()),
    Car(make="Toyota", model="Land Cruiser", year=2009, color=Rand_colour()),
    Car(make="Toyota", model="FJ Cruiser", year=2012, color=Rand_colour()),
    Car(make="Toyota", model="Corolla", year=2010, color=Rand_colour()),
    Car(make="Toyota", model="Hiace", year=2006, color=Rand_colour()),
    Car(make="Toyota", model="Townace", year=2003, color=Rand_colour()),
    Car(make="Toyota", model="Aurion", year=2008, color=Rand_colour()),
    Car(make="Toyota", model="Supra", year=2004, color=Rand_colour()),
    Car(make="Toyota", model="86", year=2013, color=Rand_colour()),
    Car(make="GM", model="Camaro", year=2008, color=Rand_colour())
]

dodgy_vehicles = filter(lambda x: hasattr(x, "defect"), some_cars)
print dodgy_vehicles

3) Où doit-on utiliser la métaclasse ou l'héritage?

Comme mentionné dans cette réponse et dans les commentaires, utilisez presque toujours l'héritage lorsque vous effectuez la POO. Les métaclasses sont destinées à travailler en dehors de ces contraintes (voir l'exemple) et ne sont presque toujours pas nécessaires, mais un flux de programme très avancé et extrêmement dynamique peut être obtenu avec elles. C'est à la fois leur force et leur danger.

37
dilbert