Quelle est la manière la plus pythonique de gérer un module dans lequel les méthodes doivent être appelées dans un certain ordre?

Par exemple, j'ai une configuration XML qui doit être lue avant de faire quoi que ce soit d'autre car la configuration affecte le comportement. Le parse_config() doit être appelé en premier avec le fichier de configuration fourni. L'appel à d'autres méthodes de support comme query_data() ne fonctionnera pas tant que parse_config() n'aura pas été appelé.

J'ai d'abord implémenté cela en tant que singleton pour garantir qu'un nom de fichier pour la configuration soit passé au moment de l'initialisation, mais en remarquant que les modules sont en fait des singletons, ce n'est plus une classe, mais juste un module normal.

Quelle est la meilleure façon de faire en sorte que parse_config soit appelé en premier dans un module?

Modifier: il convient de noter que la fonction est en fait parse_config(configfile)

2
shadowland 26 oct. 2011 à 21:02

6 réponses

Meilleure réponse

Si l'objet n'est pas valide avant son appel, appelez cette méthode dans __init__ (ou utilisez une fonction d'usine). Vous n'avez pas besoin de singletons stupides, c'est sûr.

7
Cat Plus Plus 26 oct. 2011 à 17:05

Comme Cat Plus Plus l'a dit en d'autres termes, encapsulez le comportement / les fonctions dans une classe et mettez toute la configuration requise dans la méthode __init__. Vous pourriez vous plaindre du fait que les fonctions ne semblent pas naturellement appartenir ensemble dans un objet et, par conséquent, c'est une mauvaise conception OO. Si c'est le cas, pensez à votre classe / objet comme une forme d'espacement des noms. C'est beaucoup plus propre et plus flexible que d'essayer de faire respecter l'ordre d'appel de fonction ou d'utiliser des singletons.

1
Tavis Rudd 26 oct. 2011 à 17:12

Cela revient à dire à quel point voulez-vous que vos messages d'erreur soient conviviaux si une fonction est appelée avant d'être configurée.

Le moins convivial est de ne rien faire de plus et de laisser les fonctions échouer bruyamment avec les AttributeError, IndexError, etc.

Le plus convivial serait d'avoir des fonctions de stub qui déclenchent une exception informative, comme un ConfigError: configuration not initialized personnalisé. Lorsque la fonction ConfigParser() est appelée, elle peut alors remplacer les fonctions de stub par des fonctions réelles. Quelque chose comme ça:

config.py
----------
class ConfigError(Exception):
    "configuration errors"

def query_data():
    raise ConfigError("parse_config() has not been called")

def _query_data():
    do_actual_work()

def parse_config(config_file):
    load_file(config_file)
    if failure:
        raise ConfigError("bad file")
    all_objects = globals()
    for name in ('query_data', ):
        working_func = all_objects['_'+name]
        all_objects[name] = working_func

Si vous avez de nombreuses fonctions, vous pouvez ajouter des décorateurs pour garder une trace des noms de fonction, mais c'est une réponse à une question différente. ;)

D'accord, je n'ai pas pu résister - voici la version décoratrice, ce qui rend ma solution beaucoup plus facile à mettre en œuvre:

class ConfigError(Exception):
    "various configuration errors"

class NeedsConfig(object):
    def __init__(self, module_namespace):
        self._namespace = module_namespace
        self._functions = dict()
    def __call__(self, func):
        self._functions[func.__name__] = func
        return self._stub
    @staticmethod
    def _stub(*args, **kwargs):
        raise ConfigError("parseconfig() needs to be called first")
    def go_live(self):
        for name, func in self._functions.items():
            self._namespace[name] = func

Et un exemple d'exécution:

needs_parseconfig = NeedsConfig(globals())

@needs_parseconfig
def query_data():
    print "got some data!"

@needs_parseconfig
def set_data():
    print "set the data!"

def okay():
    print "Okay!"

def parse_config(somefile):
    needs_parseconfig.go_live()

try:
    query_data()
except ConfigError, e:
    print e

try:
    set_data()
except ConfigError, e:
    print e

try:
    okay()
except:
    print "this shouldn't happen!"
    raise

parse_config('config_file')
query_data()
set_data()
okay()

Et les résultats:

parseconfig() needs to be called first
parseconfig() needs to be called first
Okay!
got some data!
set the data!
Okay!

Comme vous pouvez le voir, le décorateur fonctionne en se souvenant des fonctions qu'il décore et au lieu de renvoyer une fonction décorée, il renvoie un simple stub qui soulève un ConfigError s'il est appelé. Lorsque la routine parse_config() est appelée, elle doit appeler la méthode go_live() qui remplacera alors tous les stubs de génération d'erreur par les fonctions réelles mémorisées.

0
Ethan Furman 19 nov. 2011 à 23:13

Un module ne fait rien de ce qui ne lui est pas demandé, alors placez vos appels de fonction au bas du module afin que lorsque vous l'importez, les choses soient exécutées dans l'ordre que vous spécifiez:

Test.py

import testmod

Testmod.py

def fun1():
    print('fun1')

def fun2():
    print('fun2')

fun1()
fun2()

Lorsque vous exécutez test.py, vous verrez que fun1 est exécuté avant fun2:

python test.py 
fun1
fun2
-1
Jeremy Whitlock 26 oct. 2011 à 17:10

La simple exigence selon laquelle un module doit être "configuré" avant d'être utilisé est mieux gérée par une classe qui effectue la "configuration" dans la méthode __init__, comme dans la réponse actuellement acceptée. Les autres fonctions du module deviennent des méthodes de la classe. Il n'y a aucun avantage à essayer de faire un singleton ... l'appelant peut très bien souhaiter que deux gadgets configurés différemment ou plus fonctionnent simultanément.

Passer de cela à une exigence plus compliquée, comme un ordre temporel des méthodes:

Cela peut être géré de manière assez générale en maintenant l'état dans les attributs de l'objet, comme cela se fait habituellement dans n'importe quel langage OOPable. Chaque méthode qui a des prérequis doit vérifier que ces prérequis sont satisfaits.

Piquer dans les méthodes de remplacement est une obscurcissement comparable au verbe COBOL ALTER, et aggravé en utilisant des décorateurs - il ne serait tout simplement pas / ne devrait pas être passé en revue le code.

0
John Machin 20 nov. 2011 à 03:23

Le modèle que j'ai utilisé est que les fonctions suivantes ne sont disponibles que comme méthodes sur la valeur de retour des fonctions précédentes, comme ceci:

class Second(object):
   def two(self):
     print "two"
     return Third()

class Third(object):
   def three(self):
     print "three"

def one():
   print "one"
   return Second()

one().two().three()

Correctement conçu, ce style (qui, je le reconnais, n'est pas terriblement pythonique, encore ) permet aux bibliothèques fluides de gérer des opérations de pipeline complexes où les étapes ultérieures de la bibliothèque nécessitent à la fois les résultats des premiers calculs et de nouvelles entrées de la fonction d'appel.

Un résultat intéressant est la gestion des erreurs. Ce que j'ai trouvé, c'est que la meilleure façon de gérer les erreurs bien comprises dans les étapes du pipeline est d'avoir une classe Error vide qui peut soi-disant gérer toutes les fonctions du pipeline (sauf la première), mais ces fonctions (sauf éventuellement les terminales) ne renvoient que { {X0}}:

class Error(object):
   def two(self, *args):
      print "two not done because of earlier errors"
      return self
   def three(self, *args):
      print "three not done because of earlier errors"

class Second(object):
   def two(self, arg):
     if arg == 2:
       print "two"
       return Third()
     else:
       print "two cannot be done"
       return Error()

class Third(object):
   def three(self):
     print "three"

def one(arg):
   if arg == 1:
      print "one"
      return Second()
   else:
      print "one cannot be done"
      return Error()

one(1).two(-1).three()

Dans votre exemple, vous auriez la classe Parser, qui n'aurait presque rien d'autre qu'une fonction configure qui renvoyait une instance d'une classe ConfiguredParser, ce qui ferait tout ce que seul un analyseur correctement configuré pourrait faire. Cela vous donne accès à des éléments tels que les configurations multiples et la gestion des tentatives de configuration ayant échoué.

2
Malvolio 26 oct. 2011 à 17:46