Il modo più pitonico per fornire variabili di configurazione globali in config.py? [chiuso]


98

Nella mia ricerca senza fine nel complicare troppo le cose semplici, sto cercando il modo più "pitonico" per fornire variabili di configurazione globali all'interno del tipico " config.py " che si trova nei pacchetti egg di Python.

Il modo tradizionale (aah, buon vecchio #define !) È il seguente:

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

Pertanto le variabili globali vengono importate in uno dei seguenti modi:

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

o:

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

Ha senso, ma a volte può essere un po 'complicato, specialmente quando stai cercando di ricordare i nomi di determinate variabili. Inoltre, fornire un oggetto di "configurazione" , con variabili come attributi , potrebbe essere più flessibile. Quindi, prendendo un esempio dal file bpython config.py, ho pensato :

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)])

e un 'config.py' che importa la classe e legge come segue:

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'

e viene utilizzato in questo modo:

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))

Il che sembra un modo più leggibile, espressivo e flessibile di memorizzare e recuperare variabili globali all'interno di un pacchetto.

L'idea più pessima mai? Qual è la migliore pratica per affrontare queste situazioni? Qual è il tuo modo di archiviare e recuperare i nomi e le variabili globali all'interno del tuo pacchetto?


3
Hai già preso una decisione qui che potrebbe o non potrebbe essere buona. La configurazione stessa può essere archiviata in diversi modi, come JSON, XML, diverse grammatiche per * nix e Windows e così via. A seconda di chi scrive il file di configurazione (uno strumento, un essere umano, quale background?) Potrebbero essere preferibili grammatiche diverse. Molto spesso potrebbe non essere una buona idea lasciare che il file di configurazione sia scritto nella stessa lingua che usi per il tuo programma, perché dà troppo potere all'utente (quello che potresti essere te stesso, ma tu stesso potresti non ricordare tutto ciò che può andare storto alcuni mesi prima).
erikbwork

4
Spesso finisco per scrivere un file di configurazione JSON. Può essere letto facilmente in strutture python e può anche essere creato da uno strumento. Sembra avere la massima flessibilità e l'unico costo sono alcune parentesi graffe che potrebbero essere fastidiose per l'utente. Non ho mai scritto un uovo, però. Forse questo è il modo standard. In tal caso ignora il mio commento sopra.
erikbwork

1
Puoi usare "vars (self)" invece di "self .__ dict __. Keys ()"
Karlisson

1
Possibile duplicato di Qual è la procedura migliore per utilizzare un file di impostazioni in Python? Rispondono "Sono possibili molti modi, ed esiste già un thread per la rimessa delle biciclette. Config.py va bene a meno che non ti interessi della sicurezza".
Nikana Reklawyks

Ho finito per usare python-box, vedi questa risposta
evoluto il

Risposte:


5

L'ho fatto una volta. Alla fine ho trovato il mio basicconfig.py semplificato adeguato alle mie esigenze. Puoi passare uno spazio dei nomi con altri oggetti affinché faccia riferimento, se necessario. Puoi anche trasferire valori predefiniti aggiuntivi dal tuo codice. Inoltre mappa l'attributo e la sintassi dello stile di mappatura allo stesso oggetto di configurazione.


6
Il basicconfig.pyfile a cui si fa riferimento sembra essere stato spostato su github.com/kdart/pycopia/blob/master/core/pycopia/…
Paul M Furley

So che ha qualche anno, ma sono un principiante e penso che questo file di configurazione sia essenzialmente quello che sto cercando (forse troppo avanzato), e vorrei capirlo meglio. Devo semplicemente passare l'inizializzazione ConfigHoldercon un dict di configurazioni che vorrei impostare e passare tra i moduli?
Jinx

@Jinx A questo punto userei (e attualmente sto usando) un file YAML e PyYAML per la configurazione. Uso anche un modulo di terze parti chiamato confite supporta l'unione di più sorgenti. Fa parte di un nuovo modulo devtest.config .
Keith

56

Che ne dici di usare solo i tipi integrati come questo:

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

Puoi accedere ai valori come segue:

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

Se sei disposto a sacrificare il potenziale per calcolare le espressioni all'interno del tuo albero di configurazione, potresti usare YAML e finire con un file di configurazione più leggibile come questo:

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

e usa una libreria come PyYAML per analizzare e accedere in modo conveniente al file di configurazione


Ma normalmente vuoi avere file di configurazione diversi e quindi non avere dati di configurazione all'interno del tuo codice. Quindi ´config´ sarebbe un file JSON / YAML esterno che devi caricare da disco ogni volta che vuoi accedervi, in ogni singola classe. Credo che la domanda sia "caricare una volta" e avere un accesso globale ai dati caricati. Come lo faresti con la soluzione che hai suggerito?
masi

3
se solo esistesse qualcosa per mantenere i dati in memoria ^^
cinatic

16

Mi piace questa soluzione per piccole applicazioni :

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")

E quindi l'utilizzo è:

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

.. ti dovrebbe piacere perché:

  • usa variabili di classe (nessun oggetto da passare / nessun singleton richiesto),
  • usa tipi incorporati incapsulati e sembra (è) una chiamata al metodo App,
  • ha il controllo sull'immutabilità della configurazione individuale , le variabili globali sono il peggior tipo di globali .
  • promuove l' accesso / leggibilità convenzionale e ben denominato nel codice sorgente
  • è una classe semplice ma impone l'accesso strutturato , un'alternativa è usare @property, ma ciò richiede un codice di gestione più variabile per elemento ed è basato su oggetti.
  • richiede modifiche minime per aggiungere nuovi elementi di configurazione e impostare la sua mutabilità.

--Edit-- : per applicazioni di grandi dimensioni, memorizzare i valori in un file YAML (cioè proprietà) e leggerlo come dati immutabili è un approccio migliore (cioè la risposta di blubb / ohaal ). Per piccole applicazioni, questa soluzione sopra è più semplice.


9

Che ne dici di usare le lezioni?

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

# main.py
from config import MYSQL

print(MYSQL.PORT) # 3306

8

Simile alla risposta di blubb. Suggerisco di costruirli con funzioni lambda per ridurre il codice. Come questo:

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

Questo però ha l'odore che potresti voler fare una lezione.

Oppure, come ha notato MarkM, potresti usare 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

3
passè un nome di variabile sfortunato, poiché è anche una parola chiave.
Thomas Schreiter

Oh sì ... ho appena messo insieme questo stupido esempio. Cambierò il nome
Cory-G

Per questo tipo di approccio, potresti prendere in considerazione una classe anziché mkDictlambda. Se chiamiamo la nostra classe User, le chiavi del dizionario "config" sarebbero inizializzate in modo simile {'st3v3': User('password','blonde','Steve Booker')}. Quando il tuo "utente" è in una uservariabile, puoi quindi accedere alle sue proprietà come user.hair, ecc.
Andrew Palmer,

Se ti piace questo stile puoi anche scegliere di utilizzare collections.namedtuple . User = namedtuple('User', 'passwd hair name'); config = {'st3v3': User('password', 'blonde', 'Steve Booker')}
MarkM

7

Una piccola variazione sull'idea di Husky che uso. Crea un file chiamato 'globals' (o quello che preferisci) e quindi definisci più classi in esso, come tale:

#globals.py

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

class runtime :
    debug = False
    output = 'stdio'

Quindi, se hai due file di codice c1.py e c2.py, entrambi possono avere in alto

import globals as gl

Ora tutto il codice può accedere e impostare valori, come tali:

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

Le persone dimenticano che esistono classi, anche se nessun oggetto è mai stato istanziato che sia un membro di quella classe. E variabili in una classe che non sono precedute da "self". sono condivisi tra tutte le istanze della classe, anche se non ce ne sono. Una volta che "debug" è stato modificato da qualsiasi codice, tutti gli altri codici vedono la modifica.

Importandolo come gl, è possibile avere più file e variabili di questo tipo che consentono di accedere e impostare valori tra file di codice, funzioni, ecc., Ma senza pericolo di collisione nello spazio dei nomi.

Ciò manca di alcuni dei controlli intelligenti degli altri approcci, ma è semplice e facile da seguire.


1
È sconsigliato nominare un modulo globals, poiché è una funzione incorporata che restituisce un dict con ogni simbolo nello scope globale corrente. Inoltre, PEP8 raccomanda CamelCase (con tutte le maiuscole negli acronimi) per le classi (cioè DBInfo) e maiuscolo con trattini bassi per le cosiddette costanti (cioè DEBUG).
Nuno André

1
Grazie @ NunoAndré per il commento, finché non l'ho letto pensavo che questa risposta globalsfacesse qualcosa di strano , l'autore dovrebbe cambiare il nome
oglop

Questo approccio è il mio obiettivo. Tuttavia vedo molti approcci che la gente dice sia "il migliore". Puoi indicare alcune carenze nell'implementazione di config.py come questo?
Yash Nag

5

Siamo onesti, probabilmente dovremmo considerare l'utilizzo di una libreria mantenuta da Python Software Foundation :

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

Esempio di configurazione: (formato ini, ma JSON disponibile)

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

[bitbucket.org]
User = hg

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

Esempio di codice:

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

Rendendolo accessibile a livello globale:

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)
 ....

Aspetti negativi:

  • Stato mutabile globale incontrollato .

Non è utile utilizzare il file .ini se è necessario applicare le istruzioni if ​​negli altri file per modificare la configurazione. Sarebbe meglio invece usare config.py, ma se i valori non cambiano, e lo chiami e lo usi, sono d'accordo con l'uso del file.ini.
Kourosh

3

controlla il sistema di configurazione IPython, implementato tramite traitlet per l'imposizione del tipo che stai eseguendo manualmente.

Taglia e incolla qui per rispettare le linee guida SO per non solo eliminare i link quando il contenuto dei link cambia nel tempo.

documentazione dei tratti

Ecco i principali requisiti che volevamo che il nostro sistema di configurazione avesse:

Supporto per le informazioni di configurazione gerarchica.

Piena integrazione con i parser delle opzioni della riga di comando. Spesso si desidera leggere un file di configurazione, ma poi sovrascrivere alcuni dei valori con le opzioni della riga di comando. Il nostro sistema di configurazione automatizza questo processo e consente a ciascuna opzione della riga di comando di essere collegata a un particolare attributo nella gerarchia di configurazione che sovrascriverà.

File di configurazione che sono essi stessi codice Python valido. Questo realizza molte cose. In primo luogo, diventa possibile inserire la logica nei file di configurazione che imposta gli attributi in base al sistema operativo, alla configurazione della rete, alla versione di Python, ecc. In secondo luogo, Python ha una sintassi semplicissima per accedere alle strutture di dati gerarchiche, vale a dire l'accesso regolare agli attributi (Foo. Bar.Bam.name). Terzo, l'uso di Python rende facile per gli utenti importare attributi di configurazione da un file di configurazione a un altro. Quarto, anche se Python è tipizzato dinamicamente, ha tipi che possono essere controllati in fase di esecuzione. Pertanto, un 1 in un file di configurazione è il numero intero "1", mentre un "1" è una stringa.

Un metodo completamente automatizzato per ottenere le informazioni di configurazione alle classi che ne hanno bisogno in fase di esecuzione. Scrivere codice che percorra una gerarchia di configurazione per estrarre un particolare attributo è doloroso. Quando hai informazioni di configurazione complesse con centinaia di attributi, questo ti fa venire voglia di piangere.

Controllo e convalida del tipo che non richiedono la specificazione statica dell'intera gerarchia di configurazione prima del runtime. Python è un linguaggio molto dinamico e non sempre sai tutto ciò che deve essere configurato all'avvio di un programma.

Per ottenere ciò, definiscono fondamentalmente 3 classi di oggetti e le loro relazioni tra loro:

1) Configurazione: fondamentalmente una ChainMap / basic dict con alcuni miglioramenti per la fusione.

2) Configurabile: classe base per sottoclassare tutte le cose che si desidera configurare.

3) Applicazione: oggetto di cui viene creata un'istanza per eseguire una funzione dell'applicazione specifica o l'applicazione principale per un software monofunzionale.

Nelle loro parole:

Applicazione: applicazione

Un'applicazione è un processo che svolge un lavoro specifico. L'applicazione più ovvia è il programma a riga di comando ipython. Ogni applicazione legge uno o più file di configurazione e un singolo set di opzioni della riga di comando, quindi produce un oggetto di configurazione principale per l'applicazione. Questo oggetto di configurazione viene quindi passato agli oggetti configurabili creati dall'applicazione. Questi oggetti configurabili implementano la logica effettiva dell'applicazione e sanno come configurarsi dato l'oggetto di configurazione.

Le applicazioni hanno sempre un attributo di registro che è un logger configurato. Ciò consente la configurazione della registrazione centralizzata per applicazione. Configurabile: configurabile

Un configurabile è una normale classe Python che funge da classe base per tutte le classi principali in un'applicazione. La classe base configurabile è leggera e fa solo una cosa.

Questo Configurable è una sottoclasse di HasTraits che sa come configurarsi. I tratti a livello di classe con i metadati config = True diventano valori che possono essere configurati dalla riga di comando e dai file di configurazione.

Gli sviluppatori creano sottoclassi configurabili che implementano tutta la logica nell'applicazione. Ciascuna di queste sottoclassi ha le proprie informazioni di configurazione che controllano la modalità di creazione delle istanze.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.