Multiple constructors: the Pythonic way? [duplicar]


Esta pregunta ya tiene una respuesta aquí:

Tengo una clase de contenedor que contiene datos. Cuando se crea el contenedor, hay diferentes métodos para pasar datos.

  1. Pasar un archivo que contiene los datos
  2. Pase los datos directamente a través de argumentos
  3. No pase datos; simplemente cree 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 : Usando 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 : Usando argumentos por defecto:

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 proporciona constructor para crear contenedores vacíos. Proporcionar 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 el código se va a extender por un cuarto método para agregar datos.

La solución C parece ser la más agradable, pero el usuario tiene que saber qué método requiere. Por ejemplo: no puede hacer c = Container(args) si no sabe lo que args ser.

¿Cuál es el más Python solución?

Author: smci, 2017-06-26

7 answers

No puede tener varios métodos con el mismo nombre en Python. La sobrecarga de funciones-a diferencia de Java - no está soportada.

Utilice parámetros predeterminados o argumentos **kwargs y *args.

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

Never Nunca tener tipos mutables como predeterminados en python. ⚠ Véase aquí.

 72
Author: glegoux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-26 23:40:32

No puede tener varios constructores, pero puede tener varios métodos de fábrica correctamente nombrados.

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 desde uno de los métodos esperados.)

 25
Author: 9000,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-17 12:01:59

Lo más pitónico sería lo que ya hace la biblioteca estándar de Python. El desarrollador principal Raymond Hettinger (el tipo collections) dio una charla sobre esto, además de pautas generales para cómo escribir clases.

Use funciones separadas a nivel de clase para inicializar instancias, como que dict.fromkeys() no es el inicializador de clases pero aún devuelve una instancia de dict. Esto le permite ser flexible hacia los argumentos que necesita sin cambiar las firmas del método a medida que cambian los requisitos.

 16
Author: Arya McCarthy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-17 14:29:31

¿Cuáles son los objetivos del sistema para este código? Desde mi punto de vista, tu 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, vaya a mantenibilidad: ¿qué solución es más fácil de leer y mantener? De nuevo, 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 entender, aunque ambos se rompen fácilmente en pequeños bloques de código para el tratamiento.

 4
Author: Prune,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-26 17:46:39

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 manera:

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 {}
 4
Author: Gabriel Ecker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-26 18:21:02

Si estás en Python 3.4+ puedes usar el functools.singledispatch decorador para hacer esto (con un poco de ayuda extra del decorador methoddispatch 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
 3
Author: Sean Vieira,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-28 04:52:11

La forma más pitónica es asegurarse de que cualquier argumento opcional tenga valores predeterminados. Por lo tanto, incluya todos los argumentos que sepa que necesita y asígneles los valores predeterminados apropiados.

def __init__(self, timestamp=None, data=[], metadata={}):
    timestamp = time.now()

Una cosa importante a recordar es que cualquier argumento requerido debe no tener valores predeterminados, ya que desea que se genere un error si no se incluyen.

Puede aceptar aún 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
 0
Author: Soviut,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-26 17:53:42