Python: sous-classe `type` pour créer des types spécialisés (par exemple une « liste d'int »)

StackOverflow https://stackoverflow.com/questions/6332342

  •  27-10-2019
  •  | 
  •  

Question

Je suis en train de sous-classe type afin de créer une classe permettant de construire des types spécialisés. par exemple. un ListType:

>>> ListOfInt = ListType(list, value_type=int)
>>> issubclass(ListOfInt, list)
True
>>> issubclass(list, ListOfInt)
False
>>> # And so on ...

Cependant, cette ListOfInt ne sera jamais utilisé pour créer des instances! Je viens de l'utiliser comme une instance de type que je peux manipuler pour comparer avec d'autres types ... En particulier, dans mon cas, je dois consulter pour une opération appropriée, en fonction du type d'entrée, et j'ai besoin du type pour contenir plus de précisions (comme list of int ou XML string, etc ...).

Alors, voici ce que je suis venu avec:

class SpzType(type):

    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, C):
        return NotImplemented

    def __new__(cls, base, **features):
        name = 'SpzOf%s' % base.__name__
        bases = (base,)
        attrs = {}
        return super(SpzType, cls).__new__(cls, name, bases, attrs)

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

L'utilisation de abc n'est pas évident dans le code ci-dessus ... si je veux écrire un ListType de sous-classe comme dans l'exemple ci-dessus, il devient utile ...

La fonctionnalité de base fonctionne réellement:

>>> class SimpleType(SpzType): pass
>>> t = SimpleType(int)
>>> issubclass(t, int)
True
>>> issubclass(int, t)
False

Mais lorsque je tente de vérifier si t est une instance de SpzType, Freaks Python sur:

>>> isinstance(t, SpzType)
TypeError: __subclasscheck__() takes exactly one argument (0 given)

J'exploré avec pdb.pm() ce qui se passait, et je trouve que le code suivant génère l'erreur:

>>> SpzType.__subclasscheck__(SimpleType)
TypeError: __subclasscheck__() takes exactly one argument (0 given)

ÉTRANGE! De toute évidence, il y a un argument ... Alors qu'est-ce que cela veut dire? Une idée ? Ai-je mauvais usage abc?

Était-ce utile?

La solution 3

Merci à commentaires de Kindall , je refondus le code à ce qui suit:

class SpzType(abc.ABCMeta):

    def __subclasshook__(self, C):
        return NotImplemented

    def __new__(cls, base, **features):
        name = 'SpzOf%s' % base.__name__
        bases = (base,)
        attrs = {}
        new_spz = super(SpzType, cls).__new__(cls, name, bases, attrs)
        new_spz.__subclasshook__ = classmethod(cls.__subclasshook__)
        return new_spz

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

Donc, fondamentalement, SpzType est maintenant une sous-classe de abc.ABCMeta et subclasshook est mis en œuvre une méthode d'instance. Il fonctionne très bien et il est (OMI) élégante !!!

EDIT:. Il y avait une chose délicate ... parce que les besoins de __subclasshook__ être un classmethod, donc je dois appeler la fonction classmethod manuellement ... sinon il ne fonctionne pas si je veux mettre en œuvre __subclasshook__

Autres conseils

Voici une version de décorateur de mon autre réponse qui fonctionne avec toute la classe. Le décorateur retourne une fonction d'usine qui retourne une sous-classe de la classe d'origine avec les attributs souhaités. La bonne chose au sujet de cette approche est qu'elle ne rend pas obligatoire une métaclasse, vous pouvez utiliser une méta-classe (par exemple ABCMeta) si on le souhaite sans conflits.

Notez également que si la classe de base utilise une méta-classe, qui métaclasse sera utilisé pour instancier la sous-classe générée. Vous pouvez, si vous vouliez, coder en dur l'métaclasse désiré, ou, vous savez, écrire un décorateur qui fait une métaclasse dans un décorateur pour les classes de modèle ... c'est décorateurs tout en bas!

Si elle existe, une méthode de classe __classinit__() est passé avec succès les arguments passés à l'usine, de sorte que la classe elle-même peut avoir le code à des arguments validate et définir ses attributs. (Ce serait appelé après la __init__() du métaclasse.) Si __classinit__() retourne une classe, cette classe est retournée par l'usine à la place de celui généré, vous pouvez même prolonger la procédure de génération de cette façon (par exemple pour une classe de liste vérifiée type , vous pouvez retourner l'une des deux classes internes selon que les articles doivent être sous la contrainte au type d'élément ou non).

Si __classinit__() n'existe pas, les arguments transmis à l'usine sont simplement définie comme attributs de classe sur la nouvelle classe.

Pour plus de commodité dans la création de classes de conteneurs de type restreint, j'ai manipulé le type d'élément séparément de la fonction dict. Si ce n'est pas passé, il sera ignoré.

Comme précédemment, les classes générées par l'usine sont mises en cache de sorte que chaque fois que vous appelez pour une classe avec les mêmes caractéristiques, vous obtenez la même instance d'objet de classe.

def template_class(cls, classcache={}):

    def factory(element_type=None, **features):

        key = (cls, element_type) + tuple(features.items())
        if key in classcache:
            return classcache[key]

        newname  = cls.__name__
        if element_type or features:
            newname += "("
            if element_type:
                newname += element_type.__name__
                if features:
                    newname += ", "
            newname += ", ".join(key + "=" + repr(value)
                                 for key, value in features.items())
            newname += ")"

        newclass = type(cls)(newname, (cls,), {})
        if hasattr(newclass, "__classinit__"):
            classinit = getattr(cls.__classinit__, "im_func", cls.__classinit__)
            newclass = classinit(newclass, element_type, features) or newclass
        else:
            if element_type:
                newclass.element_type = element_type
            for key, value in features.items():
                setattr(newclass, key, value)

        classcache[key] = newclass
        return newclass

    factory.__name__ = cls.__name__
    return factory

Un exemple type restreint (type de conversion, en fait) classe de liste:

@template_class
class ListOf(list):

    def __classinit__(cls, element_type, features):
        if isinstance(element_type, type):
            cls.element_type = element_type
        else:
            raise TypeError("need element type")

    def __init__(self, iterable):
        for item in iterable:
            try:
                self.append(self.element_type(item))
            except ValueError:
                raise TypeError("value '%s' not convertible to %s"
                        % (item, self.element_type.__name__))

    # etc., to provide type conversion for items added to list 

Génération de nouvelles classes:

Floatlist = ListOf(float)
Intlist   = ListOf(int)

Alors instancier:

print FloatList((1, 2, 3))       # 1.0, 2.0, 3.0
print IntList((1.0, 2.5, 3.14))  # 1, 2, 3

Ou tout simplement créer la classe et instancier en une seule étape:

print ListOf(float)((1, 2, 3))
print ListOf(int)((1.0, 2.5, 3.14))
scroll top