2009-06-02 7 views
13

mi pregunta es más bien una pregunta de diseño. En Python, si el código en su "constructor" falla, el objeto termina sin definirse. Por lo tanto:¿Existe una mala práctica para ejecutar código en el constructor?

someInstance = MyClass("test123") #lets say that constructor throws an exception 
someInstance.doSomething() # will fail, name someInstance not defined. 

tengo una situación sin embargo, donde una gran cantidad de código copiado ocurriría si quito el código propenso a errores de mi constructor. Básicamente, mi constructor llena algunos atributos (a través de IO, donde muchas cosas pueden ir mal) a las que se puede acceder con varios captadores. Si eliminé el código del contructor, tendría 10 getters con código de copiar y pegar algo así como:

  1. ¿Es el atributo realmente establecido?
  2. hacer algunas acciones IO para llenar el atributo
  3. devolver el contenido de la variable en cuestión

No me gusta eso, porque todos mis captadores contendrían una gran cantidad de código. En lugar de eso, realizo mis operaciones de IO en una ubicación central, el constructor, y llené todos mis atributos.

¿Cuál es una forma adecuada de hacer esto?

+0

bien, voy a recordar que, yo' Siendo aún nuevo aquí y quiero dar crédito a las personas que pasan su valioso tiempo, tratando de responder mis preguntas, ¡realmente lo aprecio! – Tom

+0

Esta pregunta viene muy lejos de Python e incluso de explicar qué es todo acerca de RAII o por qué podemos/no podemos hacer operaciones en el constructor que pueden generar excepciones. – lispmachine

+1

@Neil Butterworth: esto no es realmente importante ya que puede aceptar otro artículo de mayor calidad más adelante. –

Respuesta

5

No soy un desarrollador de Python, pero en general, es mejor evitar las operaciones complejas/propensas a errores en su constructor. Una forma de evitar esto sería poner un método "LoadFromFile" o "Init" en su clase para rellenar el objeto desde una fuente externa. Este método load/init debe llamarse por separado después de construir el objeto.

+0

Bien, supongo que tienes razón.Esperaba que hubiera una aproximación más agradable, porque supongo que tendré que pasar por todos estos controles. supongo que mi código se verá así que a continuación: 1. constructor vacío 2. algún método fileLoading 3. cada captador de hacer una llamada si el archivo se ha cargado antes de hacer su trabajo
feo, pero supongo que eso es el único posible forma – Tom

+3

La idea de fábrica sugerida por laalto podría ser una buena idea también. La fábrica ocultará la construcción de dos fases, por lo que no es necesario que conozca el código de la aplicación. (He votado positivamente su respuesta, pero hoy no he votado :). Todavía apoyo mi declaración de que es mejor no lanzar excepciones de un constructor. –

3

Un patrón común es la construcción en dos fases, también sugerido por Andy White.

Primera fase: constructor regular.

Segunda fase: operaciones que pueden fallar.

Integración de los dos: agregue un método de fábrica para hacer ambas fases y hacer que el constructor esté protegido/privado para evitar la instalación fuera del método de fábrica.

Ah, y yo no soy un desarrollador de Python.

20

En C++ al menos, no hay nada de malo en poner código propenso a fallas en el constructor; simplemente se lanza una excepción si ocurre un error. Si el código es necesario para construir correctamente el objeto, realmente no hay otra alternativa (aunque puede abstraer el código en subfunciones, o mejor en los constructores de subobjetos). La peor práctica es construir a medias el objeto y luego esperar que el usuario llame a otras funciones para completar la construcción de alguna manera.

+0

Objective-C utiliza este patrón de construcción de dos fases prácticamente en todas partes para crear objetos, por lo que debe tener algún mérito. –

+5

Lanzar una excepción en C++ constructor no llama al destructor (ya que el objeto aún no está construido). Tendrás que asegurarte de limpiar todo lo que lograste construir antes de tirar. auto_ptrs son útiles. – laalto

+0

@Andy No puedo hablar en nombre del Objetivo C, pero este enfoque generalmente significa almacenar indicadores de algún tipo para indicar el estado de la construcción, esto es propenso a errores y (en C++ al menos) innecesario. –

0

Si el código para inicializar los diversos valores es realmente lo suficientemente extenso como para no copiarlo (lo cual suena como si fuera en su caso) yo personalmente optaría por poner la inicialización requerida en un método privado, agregando una bandera a indique si la inicialización se ha realizado y haciendo que todos los accesores invoquen el método de inicialización si aún no se ha inicializado.

En los escenarios con hebras, puede que tenga que agregar protección adicional en caso de que la inicialización solo se permita una vez para la semántica válida (que puede o no ser el caso dado que se trata de un archivo).

0

De nuevo, tengo poca experiencia con Python, sin embargo, en C# es mejor intentar y evitar tener un constructor que arroje una excepción.Un ejemplo de por qué viene a la mente es si desea colocar su constructor en un punto donde no es posible rodearlo con un bloque try {} catch {}, por ejemplo la inicialización de un campo en una clase:

class MyClass 
{ 
    MySecondClass = new MySecondClass(); 
    // Rest of class 
} 

Si el constructor de MySecondClass arroja una excepción que desea manejar dentro de MyClass, entonces necesita refactorizar lo anterior; ciertamente no es el fin del mundo, pero es agradable de tener.

En este caso, mi enfoque probablemente sería mover la lógica de inicialización propensa a fallas a un método de inicialización, y hacer que los getters llamen a ese método de inicialización antes de devolver cualquier valor.

Como optimización, debe tener el getter (o el método de inicialización) para establecer algún tipo de booleano "IsInitialised" en verdadero, para indicar que la inicialización (potencialmente costosa) no necesita volver a realizarse.

En pseudo-código (C#, porque sólo voy a estropear la sintaxis de Python):

class MyClass 
{ 
    private bool IsInitialised = false; 

    private string myString; 

    public void Init() 
    { 
     // Put initialisation code here 
     this.IsInitialised = true; 
    } 

    public string MyString 
    { 
     get 
     { 
      if (!this.IsInitialised) 
      { 
       this.Init(); 
      } 

      return myString; 
     } 
    } 
} 

Esto por supuesto no es seguro para subprocesos, pero no creo que se usa multithreading que comúnmente en Python así que probablemente esto no sea un problema para ti.

+0

Personalmente no me gusta tener todos y cada uno de los métodos en una clase con una copia de verificación inicializada pegada. Es propenso a errores si alguien más trabaja en el código y lo olvida, y en mi opinión tampoco es bonito, pero tal vez sea solo una cuestión subjetiva de gusto. Realmente esperaba poder evitar algo así:/ – Tom

4

No es una mala práctica per se.

Pero creo que puede que haya algo diferente aquí. En su ejemplo, el método doSomething() no se ejecutará cuando el constructor MyClass falle. Pruebe el siguiente código:

class MyClass: 
def __init__(self, s): 
    print s 
    raise Exception("Exception") 

def doSomething(self): 
    print "doSomething" 

try: 
    someInstance = MyClass("test123") 
    someInstance.doSomething() 
except: 
    print "except" 

Se debe imprimir:

test123 
except 

Para su diseño de software que podría hacer las siguientes preguntas:

  • ¿Cuál debe ser el alcance de la variable someInstance ? ¿Quiénes son sus usuarios? ¿Cuáles son sus requisitos?

  • ¿Dónde y cómo se debe manejar el error para el caso de que uno de sus 10 valores no esté disponible?

  • ¿Deberían almacenarse en caché los 10 valores en el momento de la construcción o almacenarse en caché uno por uno cuando se necesiten la primera vez?

  • ¿Se puede refactorizar el código de E/S en un método de ayuda, de modo que hacer algo similar 10 veces no da como resultado la repetición del código?

  • ...

30

hay una diferencia entre un constructor en C++ y un método __init__ en Python. En C++, la tarea de un constructor es construir un objeto. Si falla, no se llama al destructor. Por lo tanto, si se adquirieron recursos antes de lanzar una excepción , la limpieza debe realizarse antes de salir del constructor. Por lo tanto, algunos prefieren la construcción en dos fases con la mayoría de la construcción hecha fuera del constructor (ugh).

Python tiene una construcción de dos fases mucho más limpia (construir, luego inicializar). Sin embargo, muchas personas confunden un método __init__ (inicializador) con un constructor. El constructor real en Python se llama __new__. A diferencia de C++, no toma una instancia, pero devuelve uno. La tarea de __init__ es inicializar la instancia creada. Si se produce una excepción en __init__, se llamará al destructor __del__ (si existe) como se esperaba, porque el objeto ya se creó (aunque no se inicializó correctamente) cuando se llamó a __init__.

Respondiendo a su pregunta:

En Python, si el código en su "constructor" falla, el objeto termina hasta que no se está definiendo.

Eso no es exactamente cierto. Si __init__ genera una excepción, el objeto es creado pero no inicializado correctamente (p. Ej., Algunos atributos no están asignados a ). Pero en el momento en que se plantea, es probable que no tenga ninguna referencia al objeto , por lo que no importa el hecho de que los atributos no estén asignados. Solo el destructor (si lo hay) debe verificar si realmente existen los atributos.

¿Cuál es la forma correcta de hacerlo?

En Python, inicialice los objetos en __init__ y no se preocupe por las excepciones. En C++, use RAII.


actualización [gestión sobre recursos]:

En lenguajes de basura recogida, si se trata de recursos, especialmente los limitados, tales como conexiones de bases de datos, es mejor no ponerlos en libertad en el destructor. Esto se debe a que los objetos se destruyen de una manera no determinista, y si ocurre tener un bucle de referencias (que no siempre es fácil de decir), y al menos uno de los objetos en el bucle tiene un destructor definido, ellos nunca serán destruidos. Los lenguajes recolectados de basura tienen otros medios para manejar los recursos. En Python, es un with statement.

+1

Ahhh, por fin, un informe de alguien que realmente codifica en Python. Thks, iba a decir eso. Además, está perfectamente bien rodear la creación de objetos con una captura de prueba ... –

+0

'con declaración" ... Ustedes mencionan esto todo el tiempo. Considere un programa con GUI. Usted crea un objeto en '' __init__' del diálogo y desea liberarlo en el cuadro de diálogo cerrar. Este objeto contiene algunos recursos que no son de memoria. ¿Ahora que?... – cubuspl42

+0

@cubuspl42 En el caso de un programa basado en eventos, yo iría a la liberación explícita. Por ejemplo, en GTK hay una señal de "destrucción" para escuchar https://developer.gnome.org/pygtk/stable/class-gtkobject.html#signal-gtkobject--destroy – lispmachine

Cuestiones relacionadas