Pitón:subclase `tipo` para crear tipos especializados (p. ej.una “lista de int”)
Pregunta
Estoy intentando subclasificar type
para crear una clase que permita construir tipos especializados.p.ej.a ListType
:
>>> ListOfInt = ListType(list, value_type=int)
>>> issubclass(ListOfInt, list)
True
>>> issubclass(list, ListOfInt)
False
>>> # And so on ...
Sin embargo, esto ListOfInt
¡Nunca se utilizará para crear instancias!Sólo lo uso como una instancia de type
que puedo manipular para comparar con otros tipos...En particular, en mi caso necesito buscar una operación adecuada, según el tipo de entrada, y necesito que el tipo contenga más precisiones (como list of int
o XML string
, etc ...).
Así que esto es lo que se me ocurrió:
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)
El uso de abc
no es obvio en el código anterior...sin embargo, si quiero escribir una subclase ListType
como en el ejemplo de arriba, entonces resulta útil...
La funcionalidad básica realmente funciona:
>>> class SimpleType(SpzType): pass
>>> t = SimpleType(int)
>>> issubclass(t, int)
True
>>> issubclass(int, t)
False
Pero cuando trato de comprobar si t
es una instancia de SpzType
, Python se asusta:
>>> isinstance(t, SpzType)
TypeError: __subclasscheck__() takes exactly one argument (0 given)
exploré con pdb.pm()
qué estaba pasando y descubrí que el siguiente código genera el error:
>>> SpzType.__subclasscheck__(SimpleType)
TypeError: __subclasscheck__() takes exactly one argument (0 given)
Extraño ?!Obviamente hay un argumento...Entonces que significa eso ?Alguna idea ?¿Usé mal? abc
?
Solución 3
Gracias por comentar de amable, He refactorizado el código a lo siguiente:
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)
Así que básicamente, SpzType
ahora es una subclase de abc.ABCMeta
, y gancho de subclase se implementa como un método de instancia.¡Funciona muy bien y es (en mi opinión) elegante!
EDITAR :Había algo complicado...porque __subclasshook__
necesita ser un método de clase, así que tengo que llamar a la función método de clase manualmente...de lo contrario no funciona si quiero implementar __subclasshook__
.
Otros consejos
No estoy muy seguro de lo que quieres lograr.Tal vez sea mejor usar collections
módulo en lugar de usar abc
¿directamente?
Hay más información sobre las clases de colección genéricas en PEP 3119
El tipo de cosas que desea hacer probablemente podría hacerse más fácilmente usando una función de fábrica de clases como la siguiente.Al menos para mí, hace que sea más sencillo mantener claros los distintos niveles en los que intento 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
Tenga en cuenta que estoy usando un dict
como caché aquí para que obtenga el mismo objeto de clase para una combinación determinada de tipo de elemento y características.Esto hace listOf(int) is listOf(int)
siempre True
.
Aquí hay una versión decorada de mi otra respuesta que funciona con cualquier clase.El decorador devuelve una función de fábrica que devuelve una subclase de la clase original con los atributos deseados.Lo bueno de este enfoque es que no exige una metaclase, por lo que puedes usar una metaclase (p. ej. ABCMeta
) si se desea sin conflictos.
También tenga en cuenta que si la clase base usa una metaclase, esa metaclase se usará para crear una instancia de la subclase generada.Podrías, si quisieras, codificar la metaclase deseada o, ya sabes, escribir un decorador que convierta una metaclase en un decorador para clases de plantilla...¡Son decoradores hasta el final!
Si existe, un método de clase. __classinit__()
Se pasan los argumentos pasados a la fábrica, por lo que la propia clase puede tener código para validar los argumentos y establecer sus atributos.(Esto se llamaría después de la metaclase __init__()
.) Si __classinit__()
devuelve una clase, esta clase es devuelta por la fábrica en lugar de la generada, por lo que incluso puedes extender el procedimiento de generación de esta manera (p. ej.para una clase de lista de tipo verificado, puede devolver una de dos clases internas dependiendo de si los elementos deben ser forzados al tipo de elemento o no).
Si __classinit__()
no existe, los argumentos pasados a la fábrica simplemente se establecen como atributos de clase en la nueva clase.
Para mayor comodidad al crear clases contenedoras de tipo restringido, he manejado el tipo de elemento por separado del dictado de características.Si no se aprueba, será ignorado.
Como antes, las clases generadas por la fábrica se almacenan en caché para que cada vez que llame a una clase con las mismas características, obtenga la misma instancia de objeto de clase.
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 ejemplo de clase de lista de tipo restringido (en realidad, de conversión de tipo):
@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
Generando nuevas clases:
Floatlist = ListOf(float)
Intlist = ListOf(int)
Luego crea una instancia:
print FloatList((1, 2, 3)) # 1.0, 2.0, 3.0
print IntList((1.0, 2.5, 3.14)) # 1, 2, 3
O simplemente cree la clase y cree una instancia en un solo paso:
print ListOf(float)((1, 2, 3))
print ListOf(int)((1.0, 2.5, 3.14))