Forzar / asegurar que los atributos de la clase Python sean de un tipo específico

¿Cómo restrinjo una variable miembro de clase para que sea un tipo específico en Python?


Versión más larga:

Tengo una clase que tiene varias variables miembro que se establecen externamente a la clase. Debido a la forma en que se utilizan, deben ser de tipos específicos, ya sea int o list. Si esto fuera C ++, simplemente los convertiría en privado y realizaría la comprobación de tipos en la función ‘set’. Dado que eso no es posible, ¿hay alguna forma de restringir el tipo de las variables para que se produzca un error / excepción en el tiempo de ejecución si se les asigna un valor de tipo incorrecto? ¿O necesito verificar su tipo dentro de cada función que los usa?

Gracias.

Puede usar una propiedad como las otras respuestas, por lo que, si desea restringir un solo atributo, diga “barra” y restringirlo a un entero, podría escribir código como este:

 class Foo(object): def _get_bar(self): return self.__bar def _set_bar(self, value): if not isinstance(value, int): raise TypeError("bar must be set to an integer") self.__bar = value bar = property(_get_bar, _set_bar) 

Y esto funciona:

 >>> f = Foo() >>> f.bar = 3 >>> f.bar 3 >>> f.bar = "three" Traceback (most recent call last): File "", line 1, in  File "", line 6, in _set_bar TypeError: bar must be set to an integer >>> 

(También hay una nueva forma de escribir propiedades, usando la “propiedad” incorporada como decorador al método getter, pero prefiero la forma antigua, como la mencioné anteriormente).

Por supuesto, si tienes muchos atributos en tus clases y quieres protegerlos a todos de esta manera, comienza a ser detallado. No hay nada de qué preocuparse: las capacidades de introspección de Python permiten crear un decorador de clase que podría automatizar esto con un mínimo de líneas.

 def getter_setter_gen(name, type_): def getter(self): return getattr(self, "__" + name) def setter(self, value): if not isinstance(value, type_): raise TypeError("%s attribute must be set to an instance of %s" % (name, type_)) setattr(self, "__" + name, value) return property(getter, setter) def auto_attr_check(cls): new_dct = {} for key, value in cls.__dict__.items(): if isinstance(value, type): value = getter_setter_gen(key, value) new_dct[key] = value # Creates a new class, using the modified dictionary as the class dict: return type(cls)(cls.__name__, cls.__bases__, new_dct) 

Y simplemente utiliza auto_attr_check como decorador de clase y declara que los atributos que desea en el cuerpo de la clase son iguales a los tipos que los atributos deben restringir también:

 ... ... @auto_attr_check ... class Foo(object): ... bar = int ... baz = str ... bam = float ... >>> f = Foo() >>> f.bar = 5; f.baz = "hello"; f.bam = 5.0 >>> f.bar = "hello" Traceback (most recent call last): File "", line 1, in  File "", line 6, in setter TypeError: bar attribute must be set to an instance of  >>> f.baz = 5 Traceback (most recent call last): File "", line 1, in  File "", line 6, in setter TypeError: baz attribute must be set to an instance of  >>> f.bam = 3 + 2j Traceback (most recent call last): File "", line 1, in  File "", line 6, in setter TypeError: bam attribute must be set to an instance of  >>> 

En general, esta no es una buena idea por las razones que @yak mencionó en su comentario. Básicamente, está impidiendo que el usuario proporcione argumentos válidos que tengan los atributos / comportamiento correctos pero que no estén en el árbol de herencia en el que está inscrito.

Descargo de responsabilidad a un lado, hay algunas opciones disponibles para lo que está tratando de hacer. El problema principal es que no hay atributos privados en Python. Por lo tanto, si solo tiene una referencia de objeto antiguo, diga self._a , no puede garantizar que el usuario no la configurará directamente a pesar de que haya proporcionado un setter que hace una comprobación de tipo. Las siguientes opciones muestran cómo aplicar realmente la comprobación de tipos.

Anular __setattr__

Este método solo será conveniente para una (muy) pequeña cantidad de atributos a los que haga esto. El método __setattr__ es lo que se llama cuando usa la notación de puntos para asignar un atributo regular. Por ejemplo,

 class A: def __init__(self, a0): self.a = a0 

Si ahora hacemos A().a = 32 , se llamaría A().__setattr__('a', 32) debajo del capó . De hecho, self.a = a0 en __init__ usa self.__setattr__ también. Puede usar esto para imponer la verificación de tipo:

  class A: def __init__(self, a0): self.a = a0 def __setattr__(self, name, value): if name == 'a' and not isinstance(value, int): raise TypeError('Aa must be an int') super().__setattr__(name, value) 

La desventaja de este método es que tiene que tener un if name == ... separado if name == ... para cada tipo que desea verificar (o if name in ... para verificar múltiples nombres para un tipo dado). La ventaja es que es la forma más sencilla de hacer que sea casi imposible para el usuario eludir la verificación de tipo.

Hacer una propiedad

Las propiedades son objetos que reemplazan su atributo normal con un objeto descriptor (generalmente usando un decorador). Los descriptores pueden tener los métodos __get__ y __set__ que personalizan cómo se accede al atributo subyacente. Esto es como tomar la twig if correspondiente en __setattr__ y ponerla en un método que se ejecute solo para ese atributo. Aquí hay un ejemplo:

 class A: def __init__(self, a0): self.a = a0 @property def a(self): return self._a @a.setter def a(self, value): if not isinstance(value, int): raise TypeError('Aa must be an int') self._a = value 

Una forma ligeramente diferente de hacer lo mismo se puede encontrar en la respuesta de @jsbueno.

Si bien el uso de una propiedad es ingenioso y, en su mayoría, resuelve el problema, presenta un par de problemas. La primera es que tiene un atributo _a privado “que el usuario puede modificar directamente, sin pasar por la verificación de tipo. Este es casi el mismo problema que usar un captador y definidor simple, excepto que ahora a es accesible como el atributo “correcto” que redirige al definidor entre bambalinas, lo que hace que sea menos probable que el usuario se meta con _a . El segundo problema es que usted tiene un captador superfluo para hacer que la propiedad funcione como lectura-escritura. Estas cuestiones son objeto de esta pregunta.

Crear un verdadero descriptor solo de Setter

Esta solución es probablemente la más robusta en general. Se sugiere en la respuesta aceptada a la pregunta mencionada anteriormente. Básicamente, en lugar de usar una propiedad, que tiene un montón de adornos y comodidades de las que no puedes deshacerte, crea tu propio descriptor (y decorador) y úsalo para cualquier atributo que requiera una verificación de tipo:

 class SetterProperty: def __init__(self, func, doc=None): self.func = func self.__doc__ = doc if doc is not None else func.__doc__ def __set__(self, obj, value): return self.func(obj, value) class A: def __init__(self, a0): self.a = a0 @SetterProperty def a(self, value): if not isinstance(value, int): raise TypeError('Aa must be an int') self.__dict__['a'] = value 

El colocador esconde el valor real directamente en el __dict__ de la instancia para evitar que se repita en sí mismo por tiempo indefinido. Esto hace posible obtener el valor del atributo sin proporcionar un captador explícito. Como el descriptor a no tiene el método __get__ , la búsqueda continuará hasta que encuentre el atributo en __dict__ . Esto asegura que todos los conjuntos pasen por el descriptor / definidor mientras que obtiene acceso directo al valor del atributo.

Si tiene una gran cantidad de atributos que requieren una verificación como esta, puede mover la línea self.__dict__['a'] = value al método __set__ del __set__ :

 class ValidatedSetterProperty: def __init__(self, func, name=None, doc=None): self.func = func self.__name__ = name if name is not None else func.__name__ self.__doc__ = doc if doc is not None else func.__doc__ def __set__(self, obj, value): ret = self.func(obj, value) obj.__dict__[self.__name__] = value class A: def __init__(self, a0): self.a = a0 @ValidatedSetterProperty def a(self, value): if not isinstance(value, int): raise TypeError('Aa must be an int') 

Actualizar

Python3.6 hace esto por ti casi de inmediato: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL; DR

Para una cantidad muy pequeña de atributos que necesitan verificación de tipo, __setattr__ directamente. Para un número mayor de atributos, use el descriptor de solo establecimiento como se muestra arriba. El uso de propiedades directamente para este tipo de aplicación presenta más problemas de los que resuelve.

Desde Python 3.5, puede usar sugerencias de tipo para indicar que un atributo de clase debe ser de un tipo particular. Luego, podría incluir algo como MyPy como parte de su proceso de integración continua para verificar que se respeten todos los contratos tipo.

Por ejemplo, para la siguiente secuencia de comandos de Python:

 class Foo: x: int y: int foo = Foo() foo.x = "hello" 

MyPy daría el siguiente error:

 6: error: Incompatible types in assignment (expression has type "str", variable has type "int") 

Si desea que los tipos se apliquen en tiempo de ejecución, puede usar el paquete de ejecución . Desde el README:

 >>> import enforce >>> >>> @enforce.runtime_validation ... def foo(text: str) -> None: ... print(text) >>> >>> foo('Hello World') Hello World >>> >>> foo(5) Traceback (most recent call last): File "", line 1, in  File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 106, in universal _args, _kwargs = enforcer.validate_inputs(parameters) File "/home/william/.local/lib/python3.5/site-packages/enforce/enforcers.py", line 69, in validate_inputs raise RuntimeTypeError(exception_text) enforce.exceptions.RuntimeTypeError: The following runtime type errors were encountered: Argument 'text' was not of type . Actual type was . 

Nota 1: @Blckknght gracias por su comentario justo. Perdí el problema de la recursión en mi conjunto de pruebas demasiado simple.

Nota 2: Escribí esta respuesta cuando estaba al principio de aprender Python. Ahora mismo prefiero usar los descriptores de Python, ver, por ejemplo, link1 , link2 .

Gracias a las publicaciones anteriores y algunas reflexiones, creo que he descubierto una forma mucho más fácil de usar de cómo restringir un atributo de clase para que sea de un tipo específico.

En primer lugar, creamos una función que comprueba universalmente el tipo:

 def ensure_type(value, types): if isinstance(value, types): return value else: raise TypeError('Value {value} is {value_type}, but should be {types}!'.format( value=value, value_type=type(value), types=types)) 

Luego simplemente lo usamos y aplicamos en nuestras clases a través del setter. Creo que esto es relativamente simple y siga DRY, especialmente una vez que lo exporte a un módulo separado para alimentar todo su proyecto. Vea el ejemplo a continuación:

 class Product: def __init__(self, name, quantity): self.name = name self.quantity = quantity @property def name(self): return self.__dict__['name'] @name.setter def name(self, value): self.__dict__['name'] = ensure_type(value, str) @property def quantity(self): return self.quantity @quantity.setter def quantity(self, value): self.__dict__['quantity'] = ensure_type(value, int) 

Las pruebas producen resultados razonables. Vea primero las pruebas:

 if __name__ == '__main__': from traceback import format_exc try: p1 = Product(667, 5) except TypeError as err: print(format_exc(1)) try: p2 = Product('Knight who say...', '5') except TypeError as err: print(format_exc(1)) p1 = Product('SPAM', 2) p2 = Product('...and Love', 7) print('Objects p1 and p2 created successfully!') try: p1.name = -191581 except TypeError as err: print(format_exc(1)) try: p2.quantity = 'EGGS' except TypeError as err: print(format_exc(1)) 

Y el resultado de las pruebas:

 Traceback (most recent call last): File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 35, in  p1 = Product(667, 5) TypeError: Value 667 is , but should be ! Traceback (most recent call last): File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 40, in  p2 = Product('Knights who say...', '5') TypeError: Value 5 is , but should be ! Objects p1 and p2 created successfully! Traceback (most recent call last): File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 49, in  p1.name = -191581 TypeError: Value -191581 is , but should be ! Traceback (most recent call last): File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 54, in  p2.quantity = 'EGGS' TypeError: Value EGGS is , but should be ! 

Puede usar el mismo tipo de property que menciona en C ++. Recibirá ayuda para la propiedad en http://adam.gomaa.us/blog/2008/aug/11/the-python-property-builtin/ .

Puede hacerlo exactamente como dice que dijo que lo haría en C ++; Haga que la asignación pase por un método de establecimiento y haga que el método de ajuste compruebe el tipo. Los conceptos de “estado privado” y “interfaces públicas” en Python se realizan con documentación y convenciones, y es prácticamente imposible obligar a cualquiera a usar su configurador en lugar de asignar directamente la variable. Pero si le da a los atributos nombres que comienzan con un guión bajo y documenta a los definidores como una forma de usar su clase, debería hacerlo (no use __names con dos guiones bajos; casi siempre es más problemático de lo que vale la pena, a menos que realmente esté en la situación para la que están diseñados, que es un conflicto de nombres de atributos en una jerarquía de herencia). Solo los desarrolladores particularmente obtusos evitarán la manera fácil de usar la clase de la forma en que está documentado para trabajar a favor de averiguar cuáles son los nombres internos y usarlos directamente; o los desarrolladores que se sienten frustrados por tu clase se comportan de manera inusual (para Python) y no les permiten usar una clase similar a una lista personalizada en lugar de una lista.

Puede usar las propiedades, como lo han descrito otras respuestas, para hacer esto mientras sigue pareciendo que está asignando atributos directamente.


Personalmente, encuentro que los bashs de hacer cumplir la seguridad de tipos en Python son bastante inútiles. No porque creo que la comprobación de tipos estática siempre sea inferior, sino porque incluso si pudiera agregar requisitos de tipo a sus variables de Python que funcionaron el 100% del tiempo, simplemente no serán eficaces para mantener la seguridad de que su progtwig no tiene tipos. Errores porque solo generarán excepciones en tiempo de ejecución .

Piénsalo; cuando su progtwig comstackdo estáticamente se comstack exitosamente sin errores, usted sabe que está completamente libre de todos los errores que el comstackdor puede detectar (en el caso de lenguajes como Haskell o Mercury, eso es una garantía bastante buena, aunque aún no está completa; caso de lenguajes como C ++ o Java … meh).

Pero en Python, el error de tipo solo se notará si se ejecuta alguna vez. Esto significa que, incluso si pudiera obtener una aplicación de tipo estático completa en cualquier parte de su progtwig, debe ejecutar conjuntos de pruebas con una cobertura de código del 100% para saber que su progtwig está libre de errores de tipo. Pero si hubiera ejecutado regularmente pruebas con cobertura completa, sabría si tuvo algún error de tipo, ¡incluso sin intentar imponer tipos! Así que el beneficio realmente no me parece que valga la pena. Está desperdiciando la fuerza (flexibilidad) de Python sin ganar más que un poco en una de sus debilidades (detección de errores estáticos).

Sé que esta discusión se ha resuelto, pero una solución mucho más sencilla es utilizar el módulo de estructura de Python que se muestra a continuación. Esto requerirá que haga un contenedor para sus datos antes de asignarle un valor, pero es muy efectivo para mantener el tipo de datos estático. https://pypi.python.org/pypi/structures