Python :subclasse `tipo` para criar tipos especializados (e.g.uma "lista de int")

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

  •  27-10-2019
  •  | 
  •  

Pergunta

Eu estou tentando subclasse type para criar uma classe, permitindo a construção de tipos especializados.exemplo:um ListType :

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

No entanto, este ListOfInt nunca vai ser usado para criar instâncias !Eu só usá-lo como uma instância de type que eu possa manipular comparar com outros tipos de ...Em particular, no meu caso eu preciso olhar-se para um adequado funcionamento, de acordo com o tipo de entrada, e eu preciso do tipo de conter mais precisão (como list of int ou XML string, etc ...).

Então aqui está o que eu encontrei :

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)

O uso de abc não é óbvio que o código acima ...no entanto, se eu quiser escrever uma subclasse ListType como no exemplo em cima, então torna-se útil ...

A funcionalidade básica realmente funciona :

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

Mas quando tento verificar se t é uma instância de SpzType, Python freaks out :

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

Eu explorado com pdb.pm() o que estava acontecendo e descobri que, o seguinte código gera o erro :

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

Estranho ?!Obviamente, há um argumento ...Então o que isso significa ?Alguma idéia ?Eu uso indevido abc ?

Foi útil?

Solução 3

Graças ao comentário de kindall , refatorei o código para o seguinte:

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)

Então, basicamente, SpzType agora é uma subclasse de abc.ABCMeta e subclasshook é implementado como um método de instância.Funciona muito bem e é (IMO) elegante !!!

EDIT: Houve uma coisa complicada ... porque __subclasshook__ precisa ser um método de classe, então eu tenho que chamar a função classmethod manualmente ... caso contrário, não funcionará se eu quiser implementar __subclasshook__.

Outras dicas

Não tenho certeza do que você deseja alcançar.Talvez seja melhor usar o módulo collections em vez de usar o abc diretamente?

Há mais informações sobre classes de coleção genéricas em PEP 3119

O tipo de coisa que você deseja fazer provavelmente poderia ser feito mais facilmente usando uma função de fábrica de classe como a seguinte.Pelo menos para mim, é mais simples manter os vários níveis nos quais estou tentando operar.

def listOf(base, types={}, **features):
    key = (base,) + tuple(features.items())
    if key in types:
        return types[key]
    else:

        if not isinstance(base, type):
            raise TypeError("require element type, got '%s'" % base)

        class C(list):

             def __init__(self, iterable=[]):
                 for item in iterable:
                     try:    # try to convert to desired type
                         self.append(self._base(item))
                     except ValueError:
                         raise TypeError("value '%s' not convertible to %s"
                            % (item, self._base.__name__))

              # similar methods to type-check other list mutations

        C.__name__ = "listOf(%s)" % base.__name__
        C._base = base
        C.__dict__.update(features)  
        types[key] = C
        return C

Observe que estou usando um dict como um cache aqui para que você obtenha o mesmo objeto de classe para uma determinada combinação de tipo de elemento e recursos.Isso faz com que listOf(int) is listOf(int) sempre True.

Aqui está um decorador versão da minha outra resposta que funciona com qualquer classe.O decorador retorna uma fábrica função que retorna uma subclasse da classe original com os atributos desejados.A coisa agradável sobre esta abordagem é que ela não o mandato de uma metaclasse, então você pode usar uma metaclasse (e.g. ABCMeta) se desejado, sem conflitos.

Observe também que se a classe base usa uma metaclasse, que metaclasse será usada para instanciar o gerado subclasse.Você poderia, se quisesse, rígido código desejado metaclasse, ou, você sabe, escrever um profissional que faz uma metaclasse em um decorador para o modelo de classes...é decoradores de todo o caminho para baixo!

Se ele existir, um método de classe __classinit__() é passado os argumentos passados para a fábrica, assim que a própria classe pode ter um código para validar argumentos e definir seus atributos.(Isso seria chamado depois que a metaclasse do __init__().) Se __classinit__() retorna uma classe, essa classe é retornado pela fábrica em lugar de a gerado um, então você pode até mesmo estender a geração de procedimento desta maneira (e.g.para um tipo-verificada a classe lista, você pode retornar uma das duas classes internas, dependendo se os itens devem ser forçado para o tipo de elemento ou não).

Se __classinit__() não existe, os argumentos passados para a fábrica de são simplesmente definidos como atributos de classe na nova classe.

Para facilitar a criação de tipo restrito de classes de contêiner, eu tenho tratado o tipo de elemento separadamente do recurso dict.Se não passou, vai ser ignorado.

Como antes, as classes geradas pela fábrica são armazenados em cache, de modo que cada vez que você chamar para uma classe com as mesmas características, você obtém a mesma classe de instância do objeto.

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

Um exemplo de tipo restrito (tipo de conversão, na verdade) classe de lista:

@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 

A geração de novas classes:

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

Em seguida, instanciar:

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

Ou apenas criar a classe e instanciar em uma única etapa:

print ListOf(float)((1, 2, 3))
print ListOf(int)((1.0, 2.5, 3.14))
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top