Crea un tipo de pareja con nombre personalizado con características adicionales

Me gustaría crear mi propio tipo de complemento con nombre que tenga algunas características adicionales. Digamos que creamos una clase:

from collections import namedtuple MyClass = namedtuple('MyClass', 'field1 field2') 

Es inmutable, legible y simple. Ahora puedo crear instancias de MyClass:

 myobj = MyClass(field1 = 1, field2 = 3.0) print(myobj.field1, myobj.field2) 

Mi requisito adicional es cuando se crea la instancia. Me gustaría comprobar si field1 es de tipo int y field2 es float . Por ejemplo, si el usuario intenta crear una instancia de MyClass:

 obj = MyClass(field1 = 1, field2 = 3.0) # instantiates ok obj1 = MyClass(field1 = 'sometext', field2 = 3.0) # raises TypeError 

Intenté hacer un tupo personalizado con nombre que pueda validar tipos de datos (MyClass debería ser inmutable) algo así como:

 MyClass = modifiednamedtuple('MyClass', 'field1 field2', (int, float) ) 

pero me quedé atascado :(. namedtuple es una función (no puede ser una clase de base para el nombre modificado de tuple), mis experimentos con metaclases fallaron.

¿Algún consejo o sugerencia?

ok, se me ocurrió una solución que podría no ser “limpia” o pythonic. Funciona excepto que mis objetos no son inmutables. ¿Cómo hacerlos inmutables? ¿Alguna sugerencia de cómo hacerlo más limpio y redimible?

Aquí está mi código:

 def typespecificnamedtuple(name, *attr_definitions): def init(self, *args, **kwargs): valid_types = dict(attr_definitions) # tuples2dict for attr_name, value in kwargs.items(): valid_type = valid_types[attr_name] if not isinstance(value, valid_type): raise TypeError('Cannot instantiate class '+ self.__name__+ '. Inproper datatype for '+ attr_name + '=' + str(value)+ ', expected '+str(valid_type) ) setattr(self, attr_name, value) class_dict = {'__init__' : init, '__name__' : name} for attr_def in attr_definitions: class_dict[attr_def[0]] = attr_def[1] # attr_def is ('name', ) customType = type(name, (object, ), class_dict ) return customType if __name__ == '__main__': MyClass = typespecificnamedtuple('MyClass', ('value', int), ('value2', float) ) mc = MyClass(value = 1, value2 = 3.0) mc.something = 1 # this assigment is possible :( how to make immutable? print(mc.__name__, mc.value, mc.value2, mc.something) mc1 = MyClass(value = 1, value2 = 'sometext') # TypeError exception is raised 

y salida de consola .:

 MyClass 1 3.0 1 Traceback (most recent call last): File "/home/pawel/workspace/prices/prices.py", line 89, in  mc1 = MyClass(value = 1, value2 = 'sometext') # TypeError exception is raised File "/home/pawel/workspace/prices/prices.py", line 70, in init ', expected '+str(valid_type) ) TypeError: Cannot instantiate class MyClass. Inproper datatype for value2=sometext, expected  

namedtuple no es una clase, como se nota; es una funcion Pero es una función que devuelve una clase. Por lo tanto, puede utilizar el resultado de la llamada a la llamada con nombre como clase principal.

Dado que es inmutable, un grupo con namedtuple se inicializa en __new__ en lugar de en __init__ .

Así que algo como esto, tal vez:

 MyTuple = namedtuple('MyTuple', 'field1 field2') class MyClass(MyTuple): def __new__(cls, field1, field2): if not isinstance(field1, int): raise TypeError("field1 must be integer") # accept int or float for field2 and convert int to float if not isinstance(field1, (int, float)): raise TypeError("field2 must be float") return MyTuple.__new__(cls, field1, float(field2)) 

namedtuple() usa una plantilla de cadena para generar un objeto de clase.

Podrías usar esa misma técnica para tu versión modificada; pero usa el código ya generado para ti como una clase base:

 import sys from collections import OrderedDict _typechecking_class_template = """\ from collections import namedtuple as _namedtuple class {typename}(_namedtuple({typename!r}, {field_names!r})): '{typename}({arg_list})' __slots__ = () def __new__(_cls, {arg_list}): 'Create new instance of {typename}({arg_list})' for name, type_ in _cls._field_types.items(): value = locals()[name] if not isinstance(value, type_): raise TypeError("Incorrect type {{!r}} for {{}}, expected {{!r}}".format( type(value).__name__, name, type_.__name__)) return tuple.__new__(_cls, ({arg_list})) """ def typechecking_namedtuple(typename, field_names, field_types): if isinstance(field_names, str): field_names = field_names.replace(',', ' ').split() field_names = list(map(str, field_names)) typename = str(typename) class_definition = _typechecking_class_template.format( typename = typename, field_names = tuple(field_names), arg_list = repr(tuple(field_names)).replace("'", "")[1:-1], ) namespace = dict(__name__='typechecking_namedtuple_%s' % typename) exec(class_definition, namespace) result = namespace[typename] result._field_types = OrderedDict(zip(field_names, field_types)) try: module = sys._getframe(1).f_globals.get('__name__', '__main__') result.__module__ = module except (AttributeError, ValueError): pass return result 

Esto le permite producir nuevas clases de duplicación de tipos de comprobación de tipos:

 >>> MyClass = typechecking_namedtuple('MyClass', 'field1 field2', (int, float)) >>> MyClass(42, 81.2) MyClass(field1=42, field2=81.2) >>> MyClass('fourtytwo', 81.2) Traceback (most recent call last): File "", line 1, in  File "", line 16, in __new__ TypeError: Incorrect type 'str' for field1, expected 'int' >>> MyClass(42, None) Traceback (most recent call last): File "", line 1, in  File "", line 16, in __new__ TypeError: Incorrect type 'NoneType' for field2, expected 'float'