2009-06-02 6 views
13

ma question est plutôt une question de conception. En Python, si le code de votre "constructeur" échoue, l'objet n'est pas défini. Ainsi:Mauvaise pratique pour exécuter du code dans le constructeur qui est susceptible d'échouer?

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

J'ai une situation bien, où beaucoup de copie de code se produirait si je supprime le code source d'erreurs de mon constructeur. Fondamentalement, mon constructeur remplit quelques attributs (via IO, où beaucoup de choses peuvent mal se passer) auxquels on peut accéder avec différents getters. Si je supprime le code du constructeur, j'aurais 10 getters avec le code de copier coller quelque chose comme:

  1. est vraiment attribut?
  2. faire quelques actions IO pour remplir l'attribut
  3. retour le contenu de la variable en question

Je n'aime, parce que tous mes getters contiendraient beaucoup de code. Au lieu de cela, j'effectue mes opérations d'E/S dans un emplacement central, le constructeur, et remplis tous mes attributs.

Quelle est la bonne façon de faire cela?

+0

ok, je me souviens que, je Je suis encore nouveau ici et je veux donner du crédit aux gens qui passent leur temps précieux, essayant de répondre à mes questions, je l'apprécie vraiment! – Tom

+0

Cette question vient loin de Python et même de l'explication de tout ce qui concerne RAII ou pourquoi pouvons-nous/ne pouvons pas faire des opérations dans un constructeur qui peut déclencher une exception? – lispmachine

+1

@Neil Butterworth: ce n'est pas vraiment important puisque vous pouvez accepter une autre meilleure réponse plus tard. –

Répondre

5

Je ne suis pas un développeur Python, mais en général, il est préférable d'éviter les opérations complexes/sujettes aux erreurs dans votre constructeur. Un moyen de contourner cela serait de mettre une méthode "LoadFromFile" ou "Init" dans votre classe pour peupler l'objet à partir d'une source externe. Cette méthode load/init doit ensuite être appelée séparément après la construction de l'objet.

+0

Très bien, je suppose que vous avez raison.J'espérais qu'il y aurait une meilleure approche, parce que je suppose que je devrais passer par tous ces contrôles. devinez mon code ressemblera alors que: 1. constructeur vide 2. une méthode de fileLoading 3. chaque getter faire un appel si le fichier a été chargé avant de faire son travail
laid, mais je suppose que c'est le seul – Tom

+3

L'idée d'usine proposée par laalto pourrait aussi être une bonne idée. L'usine masquera la construction en deux phases, de sorte que le code de votre application n'a pas besoin d'être informé. (Je l'avais upvote sa réponse, mais je suis hors de vote aujourd'hui :). Je maintiens toujours ma déclaration qu'il vaut mieux ne pas jeter des exceptions d'un constructeur. –

3

Un motif commun est la construction en deux phases, également suggéré par Andy White.

Première phase: constructeur régulier.

Deuxième phase: opérations pouvant échouer.

Intégration des deux: Ajouter une méthode usine pour faire les deux phases et rendre le constructeur protégé/privé pour empêcher l'instantané en dehors de la méthode d'usine.

Oh, et je ne suis ni un développeur Python.

20

En C++ au moins, il n'y a rien de mal à mettre du code sujet à la défaillance dans le constructeur - vous lancez simplement une exception si une erreur survient. Si le code est nécessaire pour construire correctement l'objet, il n'y a vraiment pas d'alternative (bien que vous puissiez abstraire le code dans des sous-fonctions, ou mieux dans les constructeurs de sous-objets). La pire pratique consiste à construire à moitié l'objet et à s'attendre à ce que l'utilisateur appelle d'autres fonctions pour terminer la construction d'une manière ou d'une autre.

+0

Objective-C utilise ce modèle de construction en 2 phases un peu partout pour créer des objets, il doit donc avoir un certain mérite. –

+5

Le lancement d'une exception dans le constructeur C++ n'appelle pas le destructeur (puisque l'objet n'est pas encore construit). Vous devrez vous assurer de nettoyer tout ce que vous avez réussi à construire avant de lancer. auto_ptrs sont pratiques. – laalto

+0

@Andy Je ne peux pas parler pour Objective C, mais cette approche signifie généralement stocker des drapeaux de quelque sorte pour indiquer l'état de la construction - c'est une erreur sujette et (en C++ au moins) inutile. –

0

Si le code pour initialiser les différentes valeurs est vraiment assez important pour que la copie soit indésirable (ce qui semble être le cas dans votre cas) J'opterais personnellement pour mettre l'initialisation requise dans une méthode privée, en ajoutant un drapeau indique si l'initialisation a eu lieu et si tous les accesseurs appellent la méthode d'initialisation si elle n'a pas encore été initialisée. Dans les scénarios filetés, vous devrez peut-être ajouter une protection supplémentaire au cas où l'initialisation ne peut avoir lieu qu'une fois pour une sémantique valide (ce qui peut être le cas ou non étant donné que vous traitez un fichier).

0

Encore une fois, j'ai peu d'expérience avec Python, mais en C#, il est préférable d'essayer d'éviter d'avoir un constructeur qui lève une exception.Un exemple de ce qui vient à l'esprit est si vous voulez placer votre constructeur à un point où il n'est pas possible de l'entourer d'un bloc try {} catch {}, par exemple l'initialisation d'un champ dans une classe:

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

Si le constructeur de MySecondClass génère une exception que vous souhaitez gérer dans MyClass, vous devez refactoriser ce qui précède - ce n'est certainement pas la fin du monde, mais un modèle sympa.

Dans ce cas, mon approche serait probablement de déplacer la logique d'initialisation sujette aux pannes dans une méthode d'initialisation et de demander aux getters d'appeler cette méthode d'initialisation avant de renvoyer des valeurs. En tant qu'optimisation, le getter (ou la méthode d'initialisation) doit mettre une valeur booléenne "IsInitialised" à true, pour indiquer que l'initialisation (potentiellement coûteuse) n'a pas besoin d'être répétée.

En pseudo-code (C# parce que je vais gâcher la syntaxe 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; 
     } 
    } 
} 

Ceci est bien sûr pas thread-safe, mais je ne pense pas que le multithreading est utilisé couramment en python donc c'est probablement un non-problème pour vous.

+0

Personnellement, je n'aime pas avoir chaque méthode unique dans une classe avoir une copie collée copie initialisée. C'est une erreur si quelqu'un d'autre travaille sur le code et l'oublie, et à mes yeux ce n'est pas joli non plus, mais peut-être que ce n'est qu'une question subjective de goût. J'espérais vraiment pouvoir éviter quelque chose comme ça:/ – Tom

4

Ce n'est pas une mauvaise pratique en soi.

Mais je pense que vous pouvez être après quelque chose de différent ici. Dans votre exemple, la méthode doSomething() ne sera pas appelée lorsque le constructeur MyClass échoue. Essayez le code suivant:

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" 

Il faut imprimer:

test123 
except 

Pour la conception de votre logiciel, vous pouvez poser les questions suivantes:

  • quelle est la portée de la variable someInstance devrait être ? Qui sont ses utilisateurs? Quelles sont leurs exigences?

  • Où et comment l'erreur doit-elle être gérée dans le cas où l'une de vos 10 valeurs n'est pas disponible?

  • Les 10 valeurs doivent-elles être mises en mémoire cache au moment de la construction ou mises en cache une par une lorsqu'elles sont nécessaires la première fois?

  • Le code d'E/S peut-il être refactorisé en une méthode auxiliaire, de sorte que faire quelque chose de semblable 10 fois ne provoque pas de répétition de code?

  • ...

30

Il y a une différence entre un constructeur en C++ et une méthode __init__ en Python. En C++, la tâche d'un constructeur est de construire un objet. En cas d'échec, aucun destructeur n'est appelé. Par conséquent, si des ressources ont été acquises avant la levée d'une exception , le nettoyage doit être effectué avant de quitter le constructeur. Ainsi, certains préfèrent la construction à deux phases avec la plupart de la construction faite en dehors du constructeur (pouah).

Python a une construction à deux phases beaucoup plus propre (construction, puis initialize). Cependant, beaucoup de personnes confondent une méthode __init__ (initialiseur) avec un constructeur. Le constructeur actuel en Python est appelé __new__. Contrairement à C++, il ne prend pas d'instance, mais renvoie une instance. La tâche de __init__ consiste à initialiser l'instance créée. Si une exception est levée dans __init__, le destructeur __del__ (le cas échéant) sera appelé comme prévu, car l'objet a déjà été créé (même s'il n'a pas été correctement initialisé) au moment où __init__ a été appelé.

répondre à votre question:

En Python, si le code dans votre "constructeur" échoue, l'objet se termine par ne pas être défini.

Ce n'est pas exactement vrai. Si __init__ déclenche une exception, l'objet est créé mais pas initialisé correctement (par exemple, certains attributs ne sont pas ). Mais au moment où il est levé, vous n'avez probablement aucune référence à cet objet, donc le fait que les attributs ne sont pas attribués n'a pas d'importance. Seul le destructeur (le cas échéant) doit vérifier si les attributs existent réellement.

Quelle est la meilleure façon de procéder?

En Python, initialisez les objets dans __init__ et ne vous inquiétez pas des exceptions. En C++, utilisez RAII.


Mise à jour [sur la gestion des ressources]:

Dans les langues les déchets collectés, si vous faites affaire avec des ressources, en particulier celles limitées telles que les connexions de base de données, il est préférable de ne pas les libérer dans le destructor. Ceci est parce que les objets sont détruits d'une manière non déterministe, et si vous arrive d'avoir une boucle de références (qui est pas toujours facile de dire), et au moins l'un des objets dans la boucle a un destructor défini, ils ne seront jamais détruits. Les langues collectées par les ordures ont d'autres moyens de gérer les ressources. En Python, c'est un with statement.

+1

Ahhh, enfin, une réponse de quelqu'un qui code réellement en Python. Thks, j'allais dire ça. De plus, il est tout à fait OK d'entourer votre objet de créer avec un try catch ... –

+0

'with statement' ... Vous avez mentionné ceci aaaaall le temps. Considérons un programme avec GUI. Vous créez un objet dans la boîte de dialogue __init__ et vous voulez le libérer à la fermeture de la boîte de dialogue. Cet objet contient une ressource non-mémoire. Et maintenant?... – cubuspl42

+0

@ cubuspl42 Dans le cas d'un programme basé sur un événement, j'opterais pour un lancement explicite. Par exemple dans GTK il y a un signal "destroy" pour écouter https://developer.gnome.org/pygtk/stable/class-gtkobject.html#signal-gtkobject--destroy – lispmachine

Questions connexes