python - Múltiples constructores: ¿la manera pitónica?
initialization initializer (7)
Esta pregunta ya tiene una respuesta aquí:
Tengo una clase contenedor que contiene datos. Cuando se crea el contenedor, existen diferentes métodos para pasar datos.
- Pase un archivo que contiene los datos
- Pase los datos directamente a través de argumentos
- No pase datos; solo crea un contenedor vacío
En Java, crearía tres constructores. Así es como se vería si fuera posible en Python:
class Container:
def __init__(self):
self.timestamp = 0
self.data = []
self.metadata = {}
def __init__(self, file):
f = file.open()
self.timestamp = f.get_timestamp()
self.data = f.get_data()
self.metadata = f.get_metadata()
def __init__(self, timestamp, data, metadata):
self.timestamp = timestamp
self.data = data
self.metadata = metadata
En Python, veo tres soluciones obvias, pero ninguna de ellas es bonita:
A : Uso de argumentos de palabras clave:
def __init__(self, **kwargs):
if ''file'' in kwargs:
...
elif ''timestamp'' in kwargs and ''data'' in kwargs and ''metadata'' in kwargs:
...
else:
... create empty container
B : Uso de argumentos predeterminados:
def __init__(self, file=None, timestamp=None, data=None, metadata=None):
if file:
...
elif timestamp and data and metadata:
...
else:
... create empty container
C : solo proporcione un constructor para crear contenedores vacíos. Proporcione métodos para llenar contenedores con datos de diferentes fuentes.
def __init__(self):
self.timestamp = 0
self.data = []
self.metadata = {}
def add_data_from_file(file):
...
def add_data(timestamp, data, metadata):
...
Las soluciones A y B son básicamente las mismas. No me gusta hacer el if / else, especialmente porque tengo que verificar si se proporcionaron todos los argumentos necesarios para este método. A es un poco más flexible que B si alguna vez se va a extender el código mediante un cuarto método para agregar datos.
La solución C parece ser la mejor, pero el usuario debe saber qué método necesita.
Por ejemplo: no puede hacer
c = Container(args)
si no sabe qué es
args
.
¿Cuál es la solución más pitónica?
¿Cuáles son los objetivos del sistema para este código?
Desde mi punto de vista, su frase crítica es
but the user has to know which method he requires.
¿Qué experiencia quieres que tengan tus usuarios con tu código?
Eso debería impulsar el diseño de la interfaz.
Ahora, pase a la capacidad de mantenimiento: ¿qué solución es más fácil de leer y mantener? Nuevamente, siento que la solución C es inferior. Para la mayoría de los equipos con los que he trabajado, la solución B es preferible a A: es un poco más fácil de leer y comprender, aunque ambos se dividen fácilmente en pequeños bloques de código para el tratamiento.
La forma más pitónica es asegurarse de que los argumentos opcionales tengan valores predeterminados. Por lo tanto, incluya todos los argumentos que sabe que necesita y asígneles los valores predeterminados apropiados.
def __init__(self, timestamp=None, data=[], metadata={}):
timestamp = time.now()
Una cosa importante para recordar es que cualquier argumento requerido no debe tener valores predeterminados, ya que desea que se genere un error si no están incluidos.
Puede aceptar incluso más argumentos opcionales usando
*args
y
**kwargs
al final de su lista de argumentos.
def __init__(self, timestamp=None, data=[], metadata={}, *args, **kwards):
if ''something'' in kwargs:
# do something
La mayoría de Pythonic sería lo que la biblioteca estándar de Python ya hace.
El desarrollador principal Raymond Hettinger (el chico de las
collections
)
dio una charla sobre esto
, además de pautas generales sobre cómo escribir clases.
Use funciones separadas de nivel de clase para inicializar instancias, como por ejemplo
dict.fromkeys()
no es el inicializador de clase pero aún devuelve una instancia de
dict
.
Esto le permite ser flexible con los argumentos que necesita sin cambiar las firmas del método a medida que cambian los requisitos.
No estoy seguro si entendí bien, pero ¿no funcionaría esto?
def __init__(self, file=None, timestamp=0, data=[], metadata={}):
if file:
...
else:
self.timestamp = timestamp
self.data = data
self.metadata = metadata
O incluso podrías hacer:
def __init__(self, file=None, timestamp=0, data=[], metadata={}):
if file:
# Implement get_data to return all the stuff as a tuple
timestamp, data, metadata = f.get_data()
self.timestamp = timestamp
self.data = data
self.metadata = metadata
Gracias al consejo de Jon Kiparsky, hay una mejor manera de evitar declaraciones globales sobre
data
y
metadata
así que esta es la nueva forma:
def __init__(self, file=None, timestamp=None, data=None, metadata=None):
if file:
# Implement get_data to return all the stuff as a tuple
with open(file) as f:
timestamp, data, metadata = f.get_data()
self.timestamp = timestamp or 0
self.data = data or []
self.metadata = metadata or {}
No puede tener múltiples constructores, pero puede tener varios métodos de fábrica con nombres correctos.
class Document(object):
def __init__(self, whatever args you need):
"""Do not invoke directly. Use from_NNN methods."""
# Implementation is likely a mix of A and B approaches.
@classmethod
def from_string(cls, string):
# Do any necessary preparations, use the `string`
return cls(...)
@classmethod
def from_json_file(cls, file_object):
# Read and interpret the file as you want
return cls(...)
@classmethod
def from_docx_file(cls, file_object):
# Read and interpret the file as you want, differently.
return cls(...)
# etc.
Sin embargo, no puede evitar fácilmente que el usuario use el constructor directamente. (Si es crítico, como precaución de seguridad durante el desarrollo, puede analizar la pila de llamadas en el constructor y verificar que la llamada se realice a partir de uno de los métodos esperados).
No puede tener varios métodos con el mismo nombre en
Python
.
La sobrecarga de funciones, a diferencia de
Java
, no es compatible.
Use los parámetros predeterminados o
**kwargs
y
*args
argumentos.
Puede crear métodos estáticos o métodos de clase con el decorador
@staticmethod
o
@classmethod
para devolver una instancia de su clase o para agregar otros constructores.
Te aconsejo que hagas:
class F:
def __init__(self, timestamp=0, data=None, metadata=None):
self.timestamp = timestamp
self.data = list() if data is None else data
self.metadata = dict() if metadata is None else metadata
@classmethod
def from_file(cls, path):
_file = cls.get_file(path)
timestamp = _file.get_timestamp()
data = _file.get_data()
metadata = _file.get_metadata()
return cls(timestamp, data, metadata)
@classmethod
def from_metadata(cls, timestamp, data, metadata):
return cls(timestamp, data, metadata)
@staticmethod
def get_file(path):
# ...
pass
⚠ Nunca tenga tipos mutables como predeterminados en python. ⚠ Mira here .
Si está en Python 3.4+, puede usar el decorador
functools.singledispatch
para hacer esto (con un poco de ayuda adicional del decorador de
@ZeroPiraeus
que
@ZeroPiraeus
escribió para
su respuesta
):
class Container:
@methoddispatch
def __init__(self):
self.timestamp = 0
self.data = []
self.metadata = {}
@__init__.register(File)
def __init__(self, file):
f = file.open()
self.timestamp = f.get_timestamp()
self.data = f.get_data()
self.metadata = f.get_metadata()
@__init__.register(Timestamp)
def __init__(self, timestamp, data, metadata):
self.timestamp = timestamp
self.data = data
self.metadata = metadata