Le moyen le plus pythonique de fournir des variables de configuration globales dans config.py? [fermé]

98

Dans ma quête sans fin de complication excessive de choses simples, je recherche le moyen le plus `` pythonique '' de fournir des variables de configuration globales à l'intérieur du typique `` config.py '' trouvé dans les packages d'oeufs Python.

La manière traditionnelle (aah, bon vieux #define !) Est la suivante:

MYSQL_PORT = 3306
MYSQL_DATABASE = 'mydb'
MYSQL_DATABASE_TABLES = ['tb_users', 'tb_groups']

Par conséquent, les variables globales sont importées de l'une des manières suivantes:

from config import *
dbname = MYSQL_DATABASE
for table in MYSQL_DATABASE_TABLES:
    print table

ou:

import config
dbname = config.MYSQL_DATABASE
assert(isinstance(config.MYSQL_PORT, int))

Cela a du sens, mais peut parfois être un peu compliqué, surtout lorsque vous essayez de vous souvenir des noms de certaines variables. En outre, fournir un objet de «configuration» , avec des variables comme attributs , pourrait être plus flexible. Donc, en prenant l'exemple du fichier bpython config.py, j'ai trouvé:

class Struct(object):

    def __init__(self, *args):
        self.__header__ = str(args[0]) if args else None

    def __repr__(self):
        if self.__header__ is None:
             return super(Struct, self).__repr__()
        return self.__header__

    def next(self):
        """ Fake iteration functionality.
        """
        raise StopIteration

    def __iter__(self):
        """ Fake iteration functionality.
        We skip magic attribues and Structs, and return the rest.
        """
        ks = self.__dict__.keys()
        for k in ks:
            if not k.startswith('__') and not isinstance(k, Struct):
                yield getattr(self, k)

    def __len__(self):
        """ Don't count magic attributes or Structs.
        """
        ks = self.__dict__.keys()
        return len([k for k in ks if not k.startswith('__')\
                    and not isinstance(k, Struct)])

et un 'config.py' qui importe la classe et se lit comme suit:

from _config import Struct as Section

mysql = Section("MySQL specific configuration")
mysql.user = 'root'
mysql.pass = 'secret'
mysql.host = 'localhost'
mysql.port = 3306
mysql.database = 'mydb'

mysql.tables = Section("Tables for 'mydb'")
mysql.tables.users = 'tb_users'
mysql.tables.groups =  'tb_groups'

et est utilisé de cette manière:

from sqlalchemy import MetaData, Table
import config as CONFIG

assert(isinstance(CONFIG.mysql.port, int))

mdata = MetaData(
    "mysql://%s:%s@%s:%d/%s" % (
         CONFIG.mysql.user,
         CONFIG.mysql.pass,
         CONFIG.mysql.host,
         CONFIG.mysql.port,
         CONFIG.mysql.database,
     )
)

tables = []
for name in CONFIG.mysql.tables:
    tables.append(Table(name, mdata, autoload=True))

Ce qui semble être une manière plus lisible, expressive et flexible de stocker et de récupérer des variables globales dans un package.

La moindre idée jamais? Quelle est la meilleure pratique pour faire face à ces situations? Quelle est votre façon de stocker et de récupérer les noms globaux et les variables dans votre package?

Rigel Di Scala
la source
3
Vous avez déjà pris une décision ici qui pourrait être bonne ou non. La configuration elle-même peut être stockée de différentes manières, comme JSON, XML, différentes grammaires pour * nixes et Windows, etc. Selon la personne qui écrit le fichier de configuration (un outil, un humain, quel contexte?), Différentes grammaires peuvent être préférables. Le plus souvent, ce n'est peut-être pas une bonne idée de laisser le fichier de configuration être écrit dans la même langue que vous utilisez pour votre programme, car cela donne trop de pouvoir à l'utilisateur (ce qui pourrait être vous-même, mais vous ne vous souvenez peut-être pas de tout ce qui peut mal dans quelques mois).
erikbwork
4
Souvent, je finis par écrire un fichier de configuration JSON. Il peut être lu facilement dans les structures python et également être créé par un outil. Il semble avoir le plus de flexibilité et le seul coût est quelques accolades qui peuvent être ennuyeuses pour l'utilisateur. Mais je n'ai jamais écrit d'oeuf. C'est peut-être la méthode standard. Dans ce cas, ignorez simplement mon commentaire ci-dessus.
erikbwork
1
Vous pouvez utiliser "vars (self)" au lieu de "self .__ dict __.
Keys
1
Copie possible de Quelle est la meilleure pratique d'utilisation d'un fichier de paramètres en Python? Ils répondent "De nombreuses façons sont possibles, et un fil de discussion en bikesh existe déjà. Config.py est bon sauf si vous vous souciez de la sécurité."
Nikana Reklawyks
J'ai fini par utiliser python-box, voir cette réponse
évolué le

Réponses:

5

Je l'ai fait une fois. En fin de compte, j'ai trouvé mon basicconfig.py simplifié adapté à mes besoins. Vous pouvez transmettre un espace de noms avec d'autres objets pour qu'il fasse référence si vous en avez besoin. Vous pouvez également transmettre des valeurs par défaut supplémentaires à partir de votre code. Il mappe également la syntaxe d'attribut et de style de mappage sur le même objet de configuration.

Keith
la source
6
Le basicconfig.pyfichier mentionné semble avoir été déplacé vers github.com/kdart/pycopia/blob/master/core/pycopia/…
Paul M Furley
Je sais que cela date de quelques années, mais je suis un débutant et je pense que ce fichier de configuration est essentiellement ce que je recherche (peut-être trop avancé), et j'aimerais mieux le comprendre. Dois-je juste passer initialize ConfigHolderavec un dict de configurations que je voudrais définir et passer entre les modules?
Jinx
@Jinx À ce stade, j'utiliserais (et j'utilise actuellement) un fichier YAML et PyYAML pour la configuration. J'utilise également un module tiers appelé confitet il prend en charge la fusion de plusieurs sources. Cela fait partie d'un nouveau module devtest.config .
Keith
56

Que diriez-vous d'utiliser simplement les types intégrés comme celui-ci:

config = {
    "mysql": {
        "user": "root",
        "pass": "secret",
        "tables": {
            "users": "tb_users"
        }
        # etc
    }
}

Vous accéderiez aux valeurs comme suit:

config["mysql"]["tables"]["users"]

Si vous êtes prêt à sacrifier le potentiel de calcul d'expressions dans votre arbre de configuration, vous pouvez utiliser YAML et vous retrouver avec un fichier de configuration plus lisible comme celui-ci:

mysql:
  - user: root
  - pass: secret
  - tables:
    - users: tb_users

et utilisez une bibliothèque comme PyYAML pour analyser et accéder de manière conventionnelle au fichier de configuration

blubb
la source
Mais normalement, vous voulez avoir des fichiers de configuration différents et donc ne pas avoir de données de configuration dans votre code. Ainsi, «config» serait un fichier JSON / YAML externe que vous devez charger à partir du disque à chaque fois que vous voulez y accéder, dans chaque classe. Je crois que la question est de "charger une fois" et d'avoir un accès global aux données chargées. Comment feriez-vous cela avec la solution que vous avez suggérée?
masi
3
si juste quelque chose existait pour garder les données en mémoire ^^
cinatic
16

J'aime cette solution pour les petites applications :

class App:
  __conf = {
    "username": "",
    "password": "",
    "MYSQL_PORT": 3306,
    "MYSQL_DATABASE": 'mydb',
    "MYSQL_DATABASE_TABLES": ['tb_users', 'tb_groups']
  }
  __setters = ["username", "password"]

  @staticmethod
  def config(name):
    return App.__conf[name]

  @staticmethod
  def set(name, value):
    if name in App.__setters:
      App.__conf[name] = value
    else:
      raise NameError("Name not accepted in set() method")

Et puis l'utilisation est:

if __name__ == "__main__":
   # from config import App
   App.config("MYSQL_PORT")     # return 3306
   App.set("username", "hi")    # set new username value
   App.config("username")       # return "hi"
   App.set("MYSQL_PORT", "abc") # this raises NameError

.. vous devriez l'aimer parce que:

  • utilise des variables de classe (aucun objet à passer / aucun singleton requis),
  • utilise des types intégrés encapsulés et ressemble à (est) un appel de méthode App,
  • a le contrôle sur l' immuabilité des configurations individuelles , les globaux mutables sont les pires types de globaux .
  • favorise l' accès / la lisibilité conventionnelle et bien nommée dans votre code source
  • est une classe simple mais impose un accès structuré , une alternative est d'utiliser @property, mais cela nécessite plus de code de gestion variable par élément et est basé sur les objets.
  • nécessite des modifications minimes pour ajouter de nouveaux éléments de configuration et définir sa mutabilité.

--Edit-- : Pour les applications volumineuses, stocker les valeurs dans un fichier YAML (c'est-à-dire les propriétés) et le lire comme des données immuables est une meilleure approche (c'est -à-dire la réponse de blubb / ohaal ). Pour les petites applications, cette solution ci-dessus est plus simple.

PDS
la source
9

Que diriez-vous d'utiliser des classes?

# config.py
class MYSQL:
    PORT = 3306
    DATABASE = 'mydb'
    DATABASE_TABLES = ['tb_users', 'tb_groups']

# main.py
from config import MYSQL

print(MYSQL.PORT) # 3306
Rauque
la source
8

Similaire à la réponse de blubb. Je suggère de les construire avec des fonctions lambda pour réduire le code. Comme ça:

User = lambda passwd, hair, name: {'password':passwd, 'hair':hair, 'name':name}

#Col      Username       Password      Hair Color  Real Name
config = {'st3v3' : User('password',   'blonde',   'Steve Booker'),
          'blubb' : User('12345678',   'black',    'Bubb Ohaal'),
          'suprM' : User('kryptonite', 'black',    'Clark Kent'),
          #...
         }
#...

config['st3v3']['password']  #> password
config['blubb']['hair']      #> black

Cela sent cependant que vous voudrez peut-être faire un cours.

Ou, comme MarkM l'a noté, vous pouvez utiliser namedtuple

from collections import namedtuple
#...

User = namedtuple('User', ['password', 'hair', 'name']}

#Col      Username       Password      Hair Color  Real Name
config = {'st3v3' : User('password',   'blonde',   'Steve Booker'),
          'blubb' : User('12345678',   'black',    'Bubb Ohaal'),
          'suprM' : User('kryptonite', 'black',    'Clark Kent'),
          #...
         }
#...

config['st3v3'].password   #> passwd
config['blubb'].hair       #> black
Cory-G
la source
3
passest un nom de variable malheureux, car c'est aussi un mot-clé.
Thomas Schreiter
Oh ouais ... Je viens de rassembler cet exemple stupide. Je vais changer le nom
Cory-G
Pour ce type d'approche, vous pouvez envisager une classe au lieu de la mkDictméthode lambda. Si nous appelons notre classe User, vos clés de dictionnaire "config" seraient initialisées quelque chose comme {'st3v3': User('password','blonde','Steve Booker')}. Lorsque votre "utilisateur" est dans une uservariable, vous pouvez alors accéder à ses propriétés comme user.hair, etc.
Andrew Palmer
Si vous aimez ce style, vous pouvez également choisir d'utiliser collections.namedtuple . User = namedtuple('User', 'passwd hair name'); config = {'st3v3': User('password', 'blonde', 'Steve Booker')}
MarkM
7

Une petite variation sur l'idée de Husky que j'utilise. Créez un fichier appelé 'globals' (ou ce que vous voulez), puis définissez-y plusieurs classes, en tant que telles:

#globals.py

class dbinfo :      # for database globals
    username = 'abcd'
    password = 'xyz'

class runtime :
    debug = False
    output = 'stdio'

Ensuite, si vous avez deux fichiers de code c1.py et c2.py, les deux peuvent avoir en haut

import globals as gl

Maintenant, tout le code peut accéder et définir des valeurs, en tant que telles:

gl.runtime.debug = False
print(gl.dbinfo.username)

Les gens oublient que les classes existent, même si aucun objet n'est jamais instancié qui soit membre de cette classe. Et les variables d'une classe qui ne sont pas précédées de «self». sont partagées entre toutes les instances de la classe, même s'il n'y en a pas. Une fois que 'debug' est modifié par n'importe quel code, tous les autres codes voient le changement.

En l'important en tant que gl, vous pouvez avoir plusieurs fichiers et variables de ce type qui vous permettent d'accéder et de définir des valeurs dans les fichiers de code, les fonctions, etc., mais sans risque de collision d'espace de noms.

Cela manque une partie de la vérification intelligente des erreurs des autres approches, mais est simple et facile à suivre.

eSurfsnake
la source
1
Il est déconseillé de nommer un module globals, car c'est une fonction intégrée qui renvoie un dict avec chaque symbole dans la portée globale actuelle. De plus, PEP8 recommande CamelCase (avec toutes les majuscules dans les acronymes) pour les classes (ie DBInfo) et les majuscules avec des traits de soulignement pour les soi-disant constantes (ie DEBUG).
Nuno André
1
Merci @ NunoAndré pour le commentaire, jusqu'à ce que je le lise, je pensais que cette réponse faisait quelque chose d'étrange globals, l'auteur devrait changer le nom
oglop
Cette approche est mon choix. Je vois cependant beaucoup d'approches que les gens considèrent comme «les meilleures». Pouvez-vous indiquer certaines lacunes dans l'implémentation de config.py comme ceci?
Yash Nag
5

Soyons honnêtes, nous devrions probablement envisager d'utiliser une bibliothèque maintenue par Python Software Foundation :

https://docs.python.org/3/library/configparser.html

Exemple de configuration: (format ini, mais JSON disponible)

[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes

[bitbucket.org]
User = hg

[topsecret.server.com]
Port = 50022
ForwardX11 = no

Exemple de code:

>>> import configparser
>>> config = configparser.ConfigParser()
>>> config.read('example.ini')
>>> config['DEFAULT']['Compression']
'yes'
>>> config['DEFAULT'].getboolean('MyCompression', fallback=True) # get_or_else

Rendre accessible à l'échelle mondiale:

import configpaser
class App:
 __conf = None

 @staticmethod
 def config():
  if App.__conf is None:  # Read only once, lazy.
   App.__conf = configparser.ConfigParser()
   App.__conf.read('example.ini')
  return App.__conf

if __name__ == '__main__':
 App.config()['DEFAULT']['MYSQL_PORT']
 # or, better:
 App.config().get(section='DEFAULT', option='MYSQL_PORT', fallback=3306)
 ....

Inconvénients:

  • État mutable global incontrôlé .
PDS
la source
Il n'est pas utile d'utiliser le fichier .ini si vous devez appliquer des instructions if dans vos autres fichiers pour modifier la configuration. Il serait préférable d'utiliser config.py à la place, mais si les valeurs ne changent pas et que vous appelez et utilisez simplement le fichier, je suis d'accord avec l'utilisation du fichier of.ini.
Kourosh
3

veuillez consulter le système de configuration IPython, implémenté via des traitlets pour l'application de type que vous effectuez manuellement.

Couper et coller ici pour se conformer aux directives SO pour non seulement supprimer des liens car le contenu des liens change au fil du temps.

documentation traitlets

Voici les principales exigences que nous voulions que notre système de configuration ait:

Prise en charge des informations de configuration hiérarchique.

Intégration complète avec les analyseurs d'options de ligne de commande. Souvent, vous souhaitez lire un fichier de configuration, puis remplacer certaines des valeurs par des options de ligne de commande. Notre système de configuration automatise ce processus et permet à chaque option de ligne de commande d'être liée à un attribut particulier dans la hiérarchie de configuration qu'elle remplacera.

Fichiers de configuration qui sont eux-mêmes du code Python valide. Cela accomplit beaucoup de choses. Premièrement, il devient possible de mettre une logique dans vos fichiers de configuration qui définit des attributs en fonction de votre système d'exploitation, de la configuration du réseau, de la version de Python, etc. Deuxièmement, Python a une syntaxe super simple pour accéder aux structures de données hiérarchiques, à savoir l'accès aux attributs réguliers (Foo. Bar.Bam.name). Troisièmement, l'utilisation de Python permet aux utilisateurs d'importer facilement des attributs de configuration d'un fichier de configuration à un autre. Quatrièmement, même si Python est typé dynamiquement, il a des types qui peuvent être vérifiés à l'exécution. Ainsi, un 1 dans un fichier de configuration est l'entier «1», tandis qu'un «1» est une chaîne.

Une méthode entièrement automatisée pour obtenir les informations de configuration vers les classes qui en ont besoin au moment de l'exécution. L'écriture de code qui parcourt une hiérarchie de configuration pour extraire un attribut particulier est douloureuse. Lorsque vous avez des informations de configuration complexes avec des centaines d'attributs, cela vous donne envie de pleurer.

Vérification et validation de type qui n'exige pas que la hiérarchie de configuration entière soit spécifiée de manière statique avant l'exécution. Python est un langage très dynamique et vous ne savez pas toujours tout ce qui doit être configuré au démarrage d'un programme.

Pour y parvenir, ils définissent essentiellement 3 classes d'objets et leurs relations entre elles:

1) Configuration - essentiellement un ChainMap / dict de base avec quelques améliorations pour la fusion.

2) Configurable - classe de base pour sous-classer toutes les choses que vous souhaitez configurer.

3) Application - objet qui est instancié pour exécuter une fonction d'application spécifique, ou votre application principale pour un logiciel à usage unique.

Dans leurs mots:

Application: Application

Une application est un processus qui effectue un travail spécifique. L'application la plus évidente est le programme de ligne de commande ipython. Chaque application lit un ou plusieurs fichiers de configuration et un seul ensemble d'options de ligne de commande, puis produit un objet de configuration principal pour l'application. Cet objet de configuration est ensuite transmis aux objets configurables créés par l'application. Ces objets configurables implémentent la logique réelle de l'application et savent se configurer en fonction de l'objet de configuration.

Les applications ont toujours un attribut de journal qui est un enregistreur configuré. Cela permet une configuration de journalisation centralisée par application. Configurable: Configurable

Un configurable est une classe Python standard qui sert de classe de base pour toutes les classes principales d'une application. La classe de base Configurable est légère et ne fait qu'une chose.

Ce Configurable est une sous-classe de HasTraits qui sait se configurer. Les traits de niveau de classe avec les métadonnées config = True deviennent des valeurs qui peuvent être configurées à partir de la ligne de commande et des fichiers de configuration.

Les développeurs créent des sous-classes configurables qui implémentent toute la logique de l'application. Chacune de ces sous-classes possède ses propres informations de configuration qui contrôlent la manière dont les instances sont créées.

jLi
la source