2012-02-16 9 views
20

¿Cómo restrinjo una variable miembro de clase a un tipo específico en Python?Fuerza/asegúrese de que los atributos de la clase python sean del tipo específico


versión más larga:

tengo una clase que tiene varias variables miembro que se configuran externamente a la clase. Debido a la forma en que se usan, deben ser de tipos específicos, ya sea int o list. Si esto fuera C++, simplemente los haría privados y verificaría el tipo en la función 'establecer'. Dado que eso no es posible, ¿hay alguna manera de restringir el tipo de variables para que ocurra 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.

+3

La forma Pythonic es documentar la clase de forma adecuada. Si se establece un tipo de objeto incorrecto, el código fallará de todos modos. Por otro lado, el usuario puede usar un tipo que no pasaría las comprobaciones 'isinstance', pero de lo contrario está bien (pato typing). – yak

Respuesta

30

Se puede utilizar una propiedad como las otras respuestas ponerlo - así, si quieres constraina solo atributo, diga "barra", y limitarla a un número entero, se 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 "<stdin>", line 1, in <module> 
    File "<stdin>", line 6, in _set_bar 
TypeError: bar must be set to an integer 
>>> 

(también hay una nueva forma de escribir propiedades, utilizando la "propiedad" incorporada como decorador al método getter - pero yo prefiero la manera antigua, como Lo pongo arriba).

Por supuesto, si tiene muchos atributos en sus clases y desea protegerlos a todos de esta manera, comienza a ser detallado. Nada de qué preocuparse: las habilidades de introspección de Python le permiten a uno crear un decorador de clases que pueda 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 sólo tiene que utilizar auto_attr_check como decorador de clase, y DECLAR los atributos que desee en el cuerpo de la clase que sea igual a los tipos de los atributos deben limitar demasiado:

...  
... @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 "<stdin>", line 1, in <module> 
    File "<stdin>", line 6, in setter 
TypeError: bar attribute must be set to an instance of <type 'int'> 
>>> f.baz = 5 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "<stdin>", line 6, in setter 
TypeError: baz attribute must be set to an instance of <type 'str'> 
>>> f.bam = 3 + 2j 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "<stdin>", line 6, in setter 
TypeError: bam attribute must be set to an instance of <type 'float'> 
>>> 
+1

¡Gracias! Funciona perfectamente. – thornate

+0

¿Existe una forma simple de tener múltiples comprobaciones de tipos, p. bar = int o float? – mysteryDate

+0

Isinstance acepta una tupla de instancias, ya que se llama en este código, simplemente puede usar una tupla de tipos allí; simplemente debería funcionar: 'bar = int, float' – jsbueno

1

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

+0

No ayudaría mucho. Para que una propiedad funcione de esa manera, aún necesitaría algo de almacenamiento dentro de la instancia, que el usuario podría usar para eludir la propiedad. La única ventaja es que puede hacer que el valor interno sea "privado" con la convención de subrayado inicial y no documentarlo. Pero también podrías hacer eso con un armador regular. –

+0

La respuesta del enlace solo es incorrecta, y de hecho el enlace está roto. – Antonio

1

Puede hazlo exactamente como dices que dijiste que lo harías en C++; haz que la asignación a ellos pase por un método setter, y haz que el método setter verifique el tipo. Los conceptos de "estado privado" e "interfaces públicas" en Python se hacen con documentación y convención, y es prácticamente imposible forzar que cualquiera use su setter en lugar de asignar directamente la variable. Pero si le das a los atributos nombres que comienzan con un guión bajo y documentan los setters como la forma de usar tu clase, eso debería hacerlo (no uses __names con dos guiones bajos, casi siempre es más problemático de lo que vale a menos que seas en realidad, en la situación para la que están diseñados, lo cual es un choque 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 desarrolladores que se sienten frustrados porque su clase se comporta de manera inusual (para Python) y no les permite usar una clase personalizada tipo lista en lugar de una lista.

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


Personalmente, considero que los intentos de aplicar seguridad de tipo en Python son bastante inútiles. No porque creo que la verificación estática es siempre inferior, sino porque incluso si pudieras agregar requisitos de tipo en tus variables Python que funcionaran el 100% del tiempo, simplemente no serían efectivas para mantener la seguridad de que tu programa está libre de tipo errores porque solo generarán excepciones en tiempo de ejecución.

Piénsalo; Cuando su programa compilado de forma estática compila con éxito sin errores, usted sabe que está completamente libre de todos los errores que el compilador puede detectar (en el caso de idiomas como Haskell o Mercury, es una buena garantía, aunque aún no completa; caso de lenguajes como C++ o Java ... meh).

Pero en Python, el error de tipo solo se notará si alguna vez se ejecuta. Esto significa que, incluso si pudiera obtener la aplicación de tipo estático completo en cualquier lugar en su programa, debe ejecutar regularmente suites de prueba con cobertura de código 100% para saber realmente que su programa no contiene errores de tipo. Pero si hubiera ejecutado regularmente pruebas con cobertura total, usted tendría si sabe si tiene algún tipo de error, ¡incluso sin intentar forzar tipos! Entonces el beneficio realmente no me parece que valga la pena. Estás desperdiciando la fortaleza (flexibilidad) de Python sin ganar más que una pequeñez en una de sus debilidades (detección de error estático).

+0

Normalmente, el tipo es más una guía para el desarrollador cuando ellos realmente usan un objeto. Ayuda a pasar el buen tipo e informar al desarrollador cómo usar el método del objeto correctamente. Además, es mucho más fácil depurar con un "TypeError, el valor incorrecto pasó a fonction x, necesita una 'lista'" en lugar de "type int no tiene la función 'append'". ¿Por qué aceptaría un número entero en su función cuando realmente necesita una lista? – Nico

+1

@Nico No se trata de aceptar un número entero cuando realmente necesita una lista. Se trata de aceptar cualquier cosa que tenga el método 'append' (adecuadamente adecuado). ¿Por qué rechazarías mi clase 'Rope' (lista especializada de cadenas) cuando el código que has escrito funciona perfectamente bien? En un proyecto del mundo real, en realidad tengo un módulo con 5 variaciones diferentes de clase similar a dic (sin superclase común a todas excepto 'objeto').Serían inútiles si todo el código de la biblioteca que estoy usando requiere explícitamente instancias de 'dict'. – Ben

+0

"y es prácticamente imposible forzar a nadie a utilizar su setter en lugar de asignar directamente la variable" en realidad, eso no es cierto en Python ". Si utiliza descriptores (que se materializan fácilmente con la ayuda de 'propiedad 'invocable), sería necesario algún tipo de pirateo feo para no utilizar la función setter. – jsbueno

5

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

Dejando de lado el descargo de responsabilidad, hay algunas opciones disponibles para lo que está intentando hacer. El problema principal es que no hay atributos privados en Python. Por lo tanto, si solo tiene una referencia de objeto antiguo, por ejemplo self._a, no puede garantizar que el usuario no lo configure directamente aunque haya proporcionado un sistema que sí lo hace. Las opciones a continuación demuestran cómo aplicar realmente la verificación de tipos.

Anulación __setattr__

Este método sólo será conveniente para un pequeño número (muy) de atributos que hacen esto para. El método __setattr__ es el que se llama cuando se utiliza 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, sería llamar A().__setattr__('a', 32)under the hood. De hecho, self.a = a0 en __init__ también usa self.__setattr__. Usted puede usar esto para hacer cumplir la comprobación de tipos:

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

La desventaja de este método es que usted tiene que tener una separada if name == ... para cada tipo que desea comprobar (o if name in ... para comprobar varios nombres para un tipo determinado) . La ventaja es que es la forma más sencilla de hacer que sea casi imposible para el usuario eludir el control de tipo.

hacer que una propiedad

Las propiedades son objetos que sustituyen su atributo normal con un objeto descriptor (por lo general mediante el uso de una anotación). Los descriptores pueden tener los métodos __get__ y __set__ que personalizan cómo se accede al atributo subyacente. Esto es como tomar la rama correspondiente if en __setattr__ y ponerla en un método que se ejecutará solo para ese atributo. Aquí está 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 ininstance(value, int): 
      raise TypeError('A.a must be an int') 
     self._a = value 

Una forma un poco diferente de hacer la misma cosa se puede encontrar en la respuesta de @ jsbueno.

Si bien usar una propiedad de esta manera es ingenioso y sobre todo resuelve el problema, presenta un par de problemas. La primera es que tiene un atributo "privado" _a que el usuario puede modificar directamente, evitando su verificación de tipo. Este es casi el mismo problema que usar un eliminador y un colocador, excepto que ahora se puede acceder al a como el atributo "correcto" que redirige al colocador detrás de las escenas, lo que hace menos probable que el usuario se meta con _a. El segundo problema es que tiene un captador superfluo para hacer que la propiedad funcione como de lectura y escritura. Estos problemas son el tema de la pregunta this.

crear una verdadera descriptor Setter-Sólo

Esta solución es probablemente el conjunto más robusto. Se sugiere en el accepted answer a la pregunta mencionada anteriormente. Básicamente, en lugar de utilizar una propiedad, que tiene un montón de lujos y comodidades que no puede quitarse de encima, crear su propia anotación descriptor y el uso que de cualquier atributo que requieren la comprobación de tipos:

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 ininstance(value, int): 
      raise TypeError('A.a must be an int') 
     self.__dict__['a'] = value 

Los alijos setter el valor real directamente en el __dict__ de la instancia para evitar recurrencias en sí mismo indefinidamente. Esto hace posible obtener el valor del atributo sin proporcionar un getter explícito. Dado que el descriptor a no tiene el método __get__, la búsqueda continuará hasta que encuentre el atributo en __dict__. Esto garantiza que todos los conjuntos pasen por el descriptor/setter mientras que permite el acceso directo al valor del atributo.

Si usted tiene un gran número de atributos que requieren un control de este tipo, se puede mover la línea self.__dict__['a'] = value en __set__ método del descriptor:

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 ininstance(value, int): 
      raise TypeError('A.a must be an int') 

actualización

Python3.6 hace esto para usted casi fuera de la caja: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL; DR

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

1

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

1

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 ser del tipo específico.

En primer lugar creamos una función, que universalmente pruebas de 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)) 

entonces simplemente utilizar y aplicar en nuestras clases a través de la moda. Creo que esto es relativamente simple y sigue SECO, especialmente una vez que lo exportas a un módulo separado para alimentar todo tu proyecto. Vea el siguiente ejemplo:

class Product: 
    def __init__(self, name, quantity): 
     self.name = name 
     self.quantity = quantity 

    @property 
    def name(self): 
     return self.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. Véase la primera de 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 <module> 
    p1 = Product(667, 5) 
TypeError: Value 667 is <class 'int'>, but should be <class 'str'>! 

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 <module> 
    p2 = Product('Knights who say...', '5') 
TypeError: Value 5 is <class 'str'>, but should be <class 'int'>! 

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 <module> 
    p1.name = -191581 
TypeError: Value -191581 is <class 'int'>, but should be <class 'str'>! 

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 <module> 
    p2.quantity = 'EGGS' 
TypeError: Value EGGS is <class 'str'>, but should be <class 'int'>! 
0

Como Python 3.5, puede utilizar type-hints 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 de 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 tipos que se aplican en tiempo de ejecución, se puede usar el paquete enforce . Del archivo 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 "<stdin>", line 1, in <module> 
    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 <class 'str'>. Actual type was <class 'int'>. 
Cuestiones relacionadas