Bienvenido, les saluda Miguel y hoy les traigo este nuevo artÃculo.
Se sabe que la fuerza de Python es su flexibilidad. Por ejemplo, Python es uno de los lenguajes de programación más sencillos para la programación orientada a objetos. Sin embargo, a veces también se critica porque es «demasiado flexible».
En este artÃculo, voy a presentar la forma más elegante que creo de programar la orientación de objetos usando Python.
La clave del camino es una biblioteca llamada marshmallow
.
Puede instalar fácilmente la biblioteca pip install marshmallow
.
Índice
Definición de clase
Comencemos declarando una clase de usuario y manténgalo simple para fines de demostración.
class User(object): def __init__(self, name, age): self.name = name self.age = age def __repr__(self): return f'I am {self.name} and my age is {self.age}'
Está bien. Nuestra clase solo tiene dos atributos: User
, name
y age
. Tenga en cuenta que también he implementado el método __repr__
, de modo que podamos generar fácilmente la instancia para verificarla.
Luego, necesitamos importar algunos módulos y métodos de la biblioteca.marshmallow
.
from marshmallow import Schema, fields, post_load from pprint import pprint
Aquà importé pprint
porque vamos a imprimir muchos diccionarios y listas. Solo quiero que se vea mejor.
Ahora, ¿cómo debemos usar marshmallow
? Simplemente defina un «esquema» para nuestra clase User
.
class UserSchema(Schema): name = fields.String() age = fields.Integer() @post_load def make(self, data, **kwargs): return User(**data)
Es bastante sencillo. Para cada atributo, debemos declarar que es fields
y luego seguido por el tipo.
La anotación @post_load
es opcional, que es necesario si queremos cargar el esquema como una instancia de cualquier clase. Por tanto, lo necesitamos en nuestro caso porque queremos generar instancias User
.
El método make
simplemente hará uso de todos los argumentos para crear una instancia de la instancia.
JSON a instancia
Si tenemos un diccionario (objeto JSON) y queremos una instancia, aquà está el código.
data = { 'name': 'Chris', 'age': 32 } schema = UserSchema() user = schema.load(data)
¡Qué fácil es! Solo llama al método load()
del esquema y deserializamos el objeto JSON en la instancia de la clase.
Matriz JSON a varias instancias
¿Qué pasa si tenemos una matriz JSON que contiene varios objetos para deserializar? No necesitamos escribir un bucle for, simplemente especifique many=True
como sigue.
data = [{ 'name': 'Alice', 'age': 20 }, { 'name': 'Bob', 'age': 25 }, { 'name': 'Chris', 'age': 32 }]schema = UserSchema() users = schema.load(data, many=True)
Serializar instancias en objetos JSON (diccionario)
Está bien. Sabemos que podemos usar el método load()
 para convertir un diccionario en instancias. ¿Qué tal si fuera de la otra manera? Nosotros podemos usar método dump()
 de la siguiente manera.
dict = schema.dump(users, many=True)
En este ejemplo, simplemente usé el users
que es una lista de instancias de usuario generadas a partir del ejemplo anterior. Se puede ver que la lista de instancias de usuario se convierte en una matriz JSON en una sola lÃnea de código.
Validación de campo
Tu crees marshmallow
¿Solo se pueden serializar / deserializar instancias? Si es asÃ, probablemente no compartiré esto como una historia aquÃ. La caracterÃstica más poderosa de esta biblioteca es la validación.
Comencemos con un ejemplo simple aquÃ. Para empezar, necesitamos importar el ValidationError
que es una excepción de la biblioteca.
from marshmallow import ValidationError
Recuerda que declaramos nuestro UserSchema
arriba con el campo age
como Integer
 ¿Qué pasa si pasamos un valor no válido?
data = [{ 'name': 'Alice', 'age': 20 }, { 'name': 'Bob', 'age': 25 }, { 'name': 'Chris', 'age': 'thirty two' }]
Tenga en cuenta que en la matriz JSON anterior, el tercer objeto «Chris» tiene un formato de edad no válido, que no se puede convertir en un número entero. Usemos ahora el método load()
 para deserializar la matriz.
try: schema = UserSchema() users = schema.load(data, many=True) except ValidationError as e: print(f'Error Msg: {e.messages}') print(f'Valid Data: {e.valid_data}')
Se detecta la excepción y nos dice «No es un entero válido». Imagina que estamos desarrollando una aplicación web, ¡ni siquiera es necesario que te molestes escribiendo los mensajes de error!
Además, en este ejemplo, solo el tercer objeto tiene un problema de validación. El mensaje de error en realidad nos dijo que sucedió en el Ãndice 2
. Además, los objetos que son válidos aún se pueden generar.
Validación avanzada
Por supuesto, no es suficiente validar solo contra los tipos de datos. La biblioteca admite muchos más métodos de validación.
Agreguemos un atributo más gender
a la clase User
.
class User(object): def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender def __repr__(self): return f'I am {self.name}, my age is {self.age} and my gender is {self.gender}'
Luego, definamos el esquema con validaciones. También necesitamos importar la caracterÃstica validate
 de la biblioteca.
from marshmallow import validateclass UserSchema(Schema): name = fields.String(validate=validate.Length(min=1)) age = fields.Integer(validate=validate.Range(min=18, max=None)) gender = fields.String(validate=validate.OneOf(['F', 'M', 'Other']))
AquÃ, agregamos validaciones a los tres campos.
- Para el campo
name
, la longitud debe ser de al menos1
. En otras palabras, no puede estar vacÃo. - Para el campo
age
, tiene que ser mayor o igual a18
. - Para el campo
gender
, tiene que ser uno de los tres valores.
Definamos un objeto JSON con todos los valores no válidos de la siguiente manera.
data = { 'name': '', 'age': 16, 'gender': 'X' }
Entonces, intentemos cargarlo.
try: UserSchema().load(data) except ValidationError as e: pprint(e.messages)
No es sorprendente que se detecten las excepciones, pero cuando probé esto por primera vez, me sorprendió mucho que los mensajes de error estuvieran listos para usar. Nos ahorra mucho tiempo escribir los mensajes de error de validación.
Funciones de validación personalizadas
Puede preguntar que todavÃa está un poco limitado para usar los métodos de validación incorporados, como rango, longitud y «uno de» del ejemplo anterior. ¿Qué pasa si queremos personalizar el método de validación? Por supuesto que puede.
def validate_age(age): if age < 18: raise ValidationError('You must be an adult to buy our products!')class UserSchema(Schema): name = fields.String(validate=validate.Length(min=1)) age = fields.Integer(validate=validate_age) gender = fields.String(validate=validate.OneOf(['F', 'M', 'Other']))
Aquà definimos nuestro método de validación validate_age
con lógica personalizada asà como el mensaje. Definamos un objeto JSON para probarlo. En el siguiente objeto, la edad es menor de 18
años.
data = { 'name': 'Chris', 'age': 17, 'gender': 'M' } try: user = UserSchema().load(data) except ValidationError as e: pprint(e.messages)
Ahora, está usando su lógica personalizada y su mensaje de error.
Hay otra forma de implementar esto, que creo que es más elegante.
class UserSchema(Schema): name = fields.String() age = fields.Integer() gender = fields.String() @validates('age') def validate_age(self, age): if age < 18: raise ValidationError('You must be an adult to buy our products!')
Entonces, en este ejemplo, usamos la anotación para definir el método de validación dentro de la clase.
Campos requeridos
También puede definir que algunos campos son obligatorios.
class UserSchema(Schema): name = fields.String(required=True, error_messages={'required': 'Please enter your name.'}) age = fields.Integer(required=True, error_messages={'required': 'Age is required.'}) email = fields.Email()
En este ejemplo, definimos name
y age
son campos obligatorios. Ahora, probémoslo usando un objeto sin correo electrónico.
data_no_email = { 'name': 'Chris', 'age': 32 } try: user = UserSchema().load(data_no_email) except ValidationError as e: pprint(e.messages)
Está bien. No hay problema. ¿Y si tenemos un objeto sin nombre y sin edad?
data_no_name_age = { 'email': 'abc@email.com' }try: user = UserSchema().load(data_no_name_age) except ValidationError as e: print(f'Error Msg: {e.messages}') print(f'Valid Data: {e.valid_data}')
Se queja y proporciona los mensajes de error que definimos para los campos obligatorios.
Valores predeterminados
A veces, es posible que queramos definir algunos campos con valores predeterminados. Por lo tanto, es posible que los usuarios no necesiten ingresarlo y se utilizarán los valores predeterminados.
class UserSchema(Schema): name = fields.String(missing='Unknown', default='Unknown') print(UserSchema().load({})) # Take the "missing" value print(UserSchema().dump({})) # Take the "default" value
En marshmallow
, hay dos formas de definir valores «predeterminados»:
-
missing
palabra clave define el valor predeterminado que se utilizará al deserializar una instancia utilizandoload()
-
default
palabra clave define el valor predeterminado que se utilizará al serializar una instancia utilizandodump()
En el ejemplo anterior, usamos las dos palabras clave y experimentamos ambos métodos load()
y dump()
 con un objeto vacÃo. Se puede ver que a ambos se les agregó el campo name
 con valores predeterminados.
Alias ​​de atributo
Sigue, aún no ha terminado 🙂
A veces, podemos tener alguna implementación de discrepancia entre nuestras clases y los datos JSON reales en términos de los nombres de claves / atributos.
Por ejemplo, en nuestra clase, definimos atributo name
. Sin embargo, en el objeto JSON, tenemos username
lo que significa el mismo campo pero con un nombre diferente.
En este caso, no tenemos que volver a implementar nuestras clases ni convertir las claves en el objeto JSON.
class User(object): def __init__(self, name, age): self.name = name self.age = age def __repr__(self): return f'I am {self.name} and my age is {self.age}' class UserSchema(Schema): username = fields.String(attribute='name') age = fields.Integer() @post_load def make(self, data, **kwargs): return User(**data)
Tenga en cuenta que tenemos name
en la clase User
, mientras que en UserSchema
tenemos username
, pero para el campo username
, definimos su attribute
deberÃa ser llamado name
.
Intentemos volcar una instancia de usuario.
user = User('Chris', 32) UserSchema().dump(user)
Se serializó correctamente la instancia con el nombre del campo username
.
Viceversa:
data = { 'username': 'Chris', 'age': 32 } UserSchema (). load (data)
Aunque pasamos el objeto JSON con la clave username
, todavÃa puede deserializarlo al User
instancia sin ningún problema.
Atributos anidados
Por último, si bien no menos importante, marshmallow
admite atributos anidados sin ningún problema.
class Dirección (objeto): def __init __ (self, street, suburb, postcode): self.street = street self.suburb = suburb self.postcode = postcode def __repr __ (self): return f '{self.street}, {self.suburb} {self.postcode}' class Usuario (objeto): def __init __ (self, name, address): self.name = name self.address = address def __repr __ (self): return f'Mi nombre es {self.name} y vivo en {self. habla a}'
Definimos dos clases Address
y User
. La clase User
tiene un atributo address
, que es de tipo Address
. Probemos las clases creando una instancia de un objeto de usuario.
address = Address('1, This St', 'That Suburb', '1234') user = User('Chris', address) print(user)
Ahora, vamos a definir el esquema de la siguiente manera.
class AddressSchema(Schema): street = fields.String() suburb = fields.String() postcode = fields.String() @post_load def make(self, data, **kwargs): return Address(**data) class UserSchema(Schema): name = fields.String() address = fields.Nested(AddressSchema()) @post_load def make(self, data, **kwargs): return User(**data)
El truco aquà es usar fields.Nested()
para definir un campo usando otro esquema. Ya tenemos una instancia de usuario arriba. Volquémoslo a un objeto JSON.
pprint(UserSchema().dump(user))
Como se muestra, la instancia de usuario se ha serializado en un objeto JSON anidado.
Por supuesto, al revés también funcionará.
data = { 'name': 'Chris', 'address': { 'postcode': '1234', 'street': '1, This St', 'suburb': 'That Suburb' } } pprint(UserSchema().load(data))
Resumen
En este artÃculo, he presentado cómo usar la biblioteca. marshmallow
para simplificar extremadamente la programación orientada a objetos en Python en la práctica.
Es la forma más elegante que creo de hacer uso de OO <-> JSON
en Python.
¡La vida es corta, usa Python!. Gracias por leer.
Cheers