python - referencia - ¿Debería una clase convertir tipos de parámetros en el momento inicial? ¿Si es así, cómo?
paso de parametros python (8)
Definí una clase con 5 variables de instancia
class PassPredictData:
def __init__(self, rating, name, lat, long, elev):
self.rating = rating
# rest of init code
Quiero asegurarme de:
-
rating
es una int -
name
es una str -
lat
,long
,elev
son flotadores
Al leer mi archivo de entrada, todo funciona creando una lista de objetos basada en mi clase. Cuando comienzo a comparar valores, obtuve resultados raros, ya que las variables de instancia seguían siendo cadenas.
¿Es la "forma más pitonica" de emitir los valores ya que el objeto se está creando usando int(string)
y float(string)
cuando se llama al constructor o se debe hacer este casting con la lógica dentro de la clase?
Definir tipos de campos personalizados
Una forma es definir sus propios tipos de campo y hacer la conversión y el manejo de errores en ellos. Los campos se basarán en descriptors . Esto es algo que vas a encontrar en los modelos Django , Flask-SQLAlchemy , DRF-Fields etc.
Tener dichos campos personalizados te permitirá __init__
, validarlos y esto funcionará no solo en __init__
, sino en cualquier lugar en el que intentemos asignarle un valor.
class Field:
type = None
def __init__(self, default=None):
self.value = default
def __get__(self, instance, cls):
if instance is None:
return self
return self.value
def __set__(self, instance, value):
# Here we could either try to cast the value to
# desired type or validate it and throw an error
# depending on the requirement.
try:
self.value = self.type(value)
except Exception:
raise ValueError(''Failed to cast {value!r} to {type}''.format(
value=value, type=self.type
))
class IntField(Field):
type = int
class FloatField(Field):
type = float
class StrField(Field):
type = str
class PassPredictData:
rating = IntField()
name = StrField()
lat = FloatField()
long = FloatField()
elev = FloatField()
def __init__(self, rating, name, lat, long, elev):
self.rating = rating
self.name = name
self.lat = lat
self.long = long
self.elev = elev
Manifestación:
>>> p = PassPredictData(1.2, ''foo'', 1.1, 1.2, 1.3)
>>> p.lat = ''123''
>>> p.lat
123.0
>>> p.lat = ''foo''
...
ValueError: Failed to cast ''foo'' to <class ''float''>
>>> p.name = 123
>>> p.name
''123''
Use un analizador estático
Otra opción es usar analizadores estáticos como Mypy y detectar los errores antes de que se ejecute el programa. El siguiente código utiliza la sintaxis de Python 3.6 , pero también puede hacer que funcione con otras versiones realizando algunos cambios.
class PassPredictData:
rating: int
name: str
lat: float
long: float
elev: float
def __init__(self, rating: int, name: str, lat: float, long: float, elev: float) -> None:
self.rating = rating
self.name = name
self.lat = lat
self.long = long
self.elev = elev
PassPredictData(1, 2, 3, 4, 5)
PassPredictData(1, ''spam'', 3.1, 4.2, 5.3)
PassPredictData(1.2, ''spam'', 3.1, 4.2, 5)
Cuando ejecutamos Mypy en esto obtenemos:
/so.py:15: error: Argument 2 to "PassPredictData" has incompatible type "int"; expected "str"
/so.py:17: error: Argument 1 to "PassPredictData" has incompatible type "float"; expected "int"
Dijiste que no puedes usar bibliotecas de terceros, pero otros pueden encontrar esta pregunta. El typeguard
vale la pena mencionar aquí.
from typeguard import typechecked
class PassPredictData:
@typechecked
def __init__(self, rating: int, name: str, lat: float, long: float, elev: float):
...
Por cierto. por defecto, el decorador se desactiva cuando Python se ejecuta en modo optimizado ( -O
). Y es fácil desactivar los controles cuando está seguro de que no son necesarios.
Por cierto. Quizás el lat
, long
, elev
debería ser numbers.Real
con el molde dentro del constructor para float
;)
EDITAR: (editar porque el tema de la pregunta ha cambiado) No recomendaría el tipo de conversión de parámetros en la hora de inicio . Por ejemplo:
class PassPredictData:
def __init__(self, rating, name, lat, long, elev):
self.rating = int(rating)
self.name = str(name)
...
En mi opinión, este tipo de conversión implícita es peligroso por algunas razones.
- Implícitamente convierte el tipo de parámetro a otro sin dar advertencia es muy engañoso
- No generará ninguna excepción si los usuarios pasan de tipo no deseado. Esto va de la mano con el casting implícito. Esto podría evitarse mediante la verificación explícita de tipos.
- El tipo de conversión silenciosa viola el tipado de pato
En lugar de convertir el tipo de parámetros, es mejor verificar el tipo de parámetro en la hora de inicio. Este enfoque evitaría los tres problemas anteriores. Para lograr esto, puede usar la verificación de tipo fuerte de typedecorator Me gusta porque es simple y muy legible
Para Python2 [edit: dejar esto como referencia cuando se solicite OP]
from typedecorator import params, returns, setup_typecheck, void, typed
class PassPredictData:
@void
@params(self=object, rating = int, name = str, lat = float, long = float, elev = float)
def __init__(self, rating, name, lat, long, elev):
self.rating = rating
self.name = name
self.lat = lat
self.long = long
self.elev = elev
setup_typecheck()
x = PassPredictData(1, "derp" , 6.8 , 9.8, 7.6) #works fine
x1 = PassPredictData(1.8, "derp" , 6.8 , 9.8, 7.6) #TypeError: argument rating = 1.8 doesn''t match signature int
x2 = PassPredictData(1, "derp" , "gagaga" , 9.8, 7.6) #TypeError: argument lat = ''gagaga'' doesn''t match signature float
x3 = PassPredictData(1, 5 , 6.8 , 9.8, 7.6) #TypeError: argument name = 5 doesn''t match signature str
Para Python3 puede usar la sintaxis de anotación :
class PassPredictData1:
@typed
def __init__(self : object, rating : int, name : str, lat : float, long : float, elev : float):
self.rating = rating
setup_typecheck()
x = PassPredictData1(1, 5, 4, 9.8, 7.6)
arroja un error:
TypeError: argumento nombre = 5 no coincide con la signatura str
En Python 3.5+ puede usar sugerencias de tipo y el módulo de escritura .
class PassPredictData:
def __init__(self, rating: int, name: str, lat: float, long: float, elev: float):
self.rating = rating
#rest of init code
Tenga en cuenta que estos son solo consejos. Python en realidad no hace nada con ellos, como mostrar un error si se usa el tipo incorrecto.
Incluso sin depender de bibliotecas externas, puede definir su propio decorador de verificación de tipos simple en solo unas pocas líneas. Esto usa el módulo de inspect
de Core-Python para obtener los nombres de los parámetros, pero incluso sin él, podrías zip
los args
con una lista de tipos, aunque esto dificultará el uso de kwargs
.
import inspect
def typecheck(**types):
def __f(f):
def _f(*args, **kwargs):
all_args = {n: a for a, n in zip(args, inspect.getargspec(f).args)}
all_args.update(kwargs)
for n, a in all_args.items():
t = types.get(n)
if t is not None and not isinstance(a, t):
print("WARNING: Expected {} for {}, got {}".format(t, n, a))
return f(*args, **kwargs)
return _f
return __f
class PassPredictData:
@typecheck(rating=int, name=str, elev=float)
def __init__(self, rating, name, lat=0.0, long=0.0, elev=0.0):
self.rating = rating
p = PassPredictData(5.1, "foo", elev=4)
# WARNING: Expected <class ''int''> for rating, got 5.1
# WARNING: Expected <class ''float''> for elev, got 4
En lugar de imprimir una advertencia, por supuesto también podría generar una excepción. O, utilizando el mismo enfoque, también podría simplemente (intentar) convertir los parámetros al tipo esperado:
def typecast(**types):
def __f(f):
def _f(*args, **kwargs):
all_args = {n: a for a, n in zip(args, inspect.getargspec(f).args)}
all_args.update(kwargs)
for n, a in all_args.items():
t = types.get(n)
if t is not None:
all_args[n] = t(a) # instead of checking, just cast
return f(**all_args) # pass the map with the typecast params
return _f
return __f
class PassPredictData:
@typecast(rating=int, name=str, elev=float)
def __init__(self, rating, name, lat=0.0, long=0.0, elev=0.0):
print([rating, name, lat, long, elev])
p = PassPredictData("5", "foo", elev="3.14")
# Output of print: [5, ''foo'', 0.0, 0.0, 3.14]
O una versión más simple, sin inspect
, pero que no funciona para kwargs
y que requiere proporcionar el tipo para cada parámetro, incluido self
(o None
para ningún tipo de conversión):
def typecast(*types):
def __f(f):
def _f(*args):
return f(*[t(a) if t is not None else a
for a, t in zip(args, types)])
return _f
return __f
class PassPredictData:
@typecast(None, int, str, float, float, float)
def __init__(self, rating, name, lat=0.0, long=0.0, elev=0.0):
print([rating, name, lat, long, elev])
Parece que hay un millón de formas de hacerlo, pero esta es la fórmula que uso:
class PassPredictData(object):
types = {''lat'' : float,
''long'' : float,
''elev'' : float,
''rating'': int,
''name'' : str,
}
def __init__(self, rating, name, lat, long, elev):
self.rating = rating
[rest of init code]
@classmethod
def from_string(cls, string):
[code to parse your string into a dict]
typed = {k: cls.types[k](v) for k, v in parsed.items()}
return cls(**typed)
Algo que es bueno acerca de esto: puedes usar directamente un re.groupdict()
para producir tu dict (como un ejemplo):
parsed = re.search(''(?P<name>/w): Latitude: (?P<lat>/d+), Longitude: (?P<long>/d+), Elevation: (?P<elev>/d+) meters. (?P<rating>/d)'', some_string).groupdict()
Personalmente, haría cualquier análisis de cadenas antes de pasar los valores al constructor, a menos que el análisis sea una (o la ) responsabilidad explícita de la clase. Prefiero que mi programa falle porque no emití un valor explícito que ser demasiado flexible y terminar en una situación similar a Javascript 0 == "0"
. Dicho esto, si quieres aceptar cadenas como parámetros, puedes llamar a int(my_parameter)
o float(my_parameter)
según sea necesario en el constructor y eso asegurará que sean números sin importar si pasas un número, una cadena o incluso un booleano .
En caso de que quiera saber más sobre la seguridad de tipo en Python, puede echar un vistazo a las anotaciones de tipo , que son compatibles con mypy tipo como mypy , y el paquete de rasgos para la seguridad de tipo en los atributos de clase.
Si escribe import this
en el intérprete de Python, obtendrá "The Zen of Python, por Tim Peters". Las primeras tres líneas parecen aplicarse a su situación:
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Recomiendo implementar tu clase de esta manera:
class PassPredictData:
def __init__(self, rating, name, lat, long, elev):
self.rating = int(rating)
self.name = str(name)
self.lat = float(lat)
self.long = float(long)
self.elev = float(elev)
Esta es la implementación que mencionas en tu pregunta. Es simple y explícito . La belleza está en el ojo del espectador.
Respuestas a los Comentarios
La implementación es explícita para el escritor de la clase versus alguna otra solución que oculta la conversión de tipo detrás de algún mecanismo opaco.
Existe un argumento válido de que no es obvio a partir de la firma de la función cuáles son los tipos de parámetros esperados. Sin embargo, la pregunta implica que todos los parámetros se pasan como cadenas. En ese caso, el tipo esperado es str
para todos los parámetros del constructor. Tal vez el título de la pregunta no describa claramente el problema. Tal vez un título mejor sería " Aplicar tipos de variables de instancia al pasar cadenas como parámetros al constructor ".