Самый Pythonic способ предоставить глобальные переменные конфигурации в config.py?

В своих бесконечных поисках чрезмерного усложнения простых вещей я исследую наиболее «питоновский» способ предоставления глобальных переменных конфигурации внутри типичного файла «config.py», который можно найти в пакетах яйца Python.

Традиционный способ (ааа, старый добрый #define!) выглядит следующим образом:

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

Поэтому глобальные переменные импортируются одним из следующих способов:

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

or:

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

Это имеет смысл, но иногда может быть немного запутанно, особенно когда вы пытаетесь запомнить имена определенных переменных. Кроме того, предоставление объекта конфигурации с переменными в качестве атрибутов может быть более гибким. Итак, основываясь на файле bpython config.py, я придумал:

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

и «config.py», который импортирует класс и выглядит следующим образом:

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'

и используется таким образом:

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

Что кажется более читабельным, выразительным и гибким способом хранения и выборки глобальных переменных внутри пакета.

Самая дурацкая идея? Как лучше всего справляться с такими ситуациями? Каков ваш способ хранения и извлечения глобальных имен и переменных внутри вашего пакета?


person Rigel Di Scala    schedule 01.06.2011    source источник
comment
Вы уже приняли здесь решение, которое может быть хорошим, а может и нет. Сам конфиг можно хранить по-разному, вроде JSON, XML, разные грамматики для *nix и Windows и так далее. В зависимости от того, кто пишет файл конфигурации (инструмент, человек, какой фон?), могут быть предпочтительны разные грамматики. Чаще всего может быть не очень хорошей идеей, чтобы файл конфигурации был написан на том же языке, который вы используете для своей программы, потому что это дает слишком много возможностей пользователю (что может быть вам, но вы сами можете не помнить всего, что может пойти не так на несколько месяцев вперед).   -  person erikbwork    schedule 01.06.2011
comment
Часто я заканчиваю тем, что пишу файл конфигурации JSON. Его можно легко прочитать в структурах Python, а также создать с помощью инструмента. Кажется, что он обладает наибольшей гибкостью, и единственной ценой являются некоторые фигурные скобки, которые могут раздражать пользователя. Однако я никогда не писал яйцо. Возможно, это стандартный способ. В таком случае просто игнорируйте мой комментарий выше.   -  person erikbwork    schedule 01.06.2011
comment
Вы можете использовать vars(self) вместо self.__dict__.keys()   -  person Karlisson    schedule 18.12.2013
comment
Возможный дубликат Как лучше всего использовать файл настроек в Python? Они отвечают. Возможны многие способы, и уже существует тема для велосипедного сарая. config.py хорош, если вы не заботитесь о безопасности.   -  person Nikana Reklawyks    schedule 21.11.2017
comment
В итоге я использовал python-box, см. этот ответ   -  person evolved    schedule 24.07.2020
comment
Я расхохотался, когда читал В своих бесконечных поисках чрезмерного усложнения простых вещей...   -  person D_Serg    schedule 05.03.2021


Ответы (8)


Я сделал это однажды. В конце концов я нашел мой упрощенный basicconfig.py адекватным для моего потребности. Вы можете передать пространство имен с другими объектами для ссылки на него, если вам нужно. Вы также можете передать дополнительные значения по умолчанию из своего кода. Он также сопоставляет атрибут и синтаксис стиля сопоставления с одним и тем же объектом конфигурации.

person Keith    schedule 01.06.2011
comment
Я знаю, что этому несколько лет, но я новичок, и я думаю, что этот файл конфигурации, по сути, то, что я ищу (возможно, слишком продвинутый), и я хотел бы понять его лучше. Мне просто передать initialize ConfigHolder с набором конфигураций, которые я хотел бы установить и передать между модулями? - person Jinx; 04.03.2020
comment
@Jinx На этом этапе я бы использовал (и сейчас использую) файл YAML и PyYAML для настройки. Я также использую сторонний модуль под названием confit, который поддерживает объединение нескольких источников. Это часть нового модуля devtest.config. - person Keith; 05.03.2020

Как насчет того, чтобы просто использовать встроенные типы, подобные этому:

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

Вы бы получили доступ к значениям следующим образом:

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

Если вы готовы пожертвовать возможностью вычисления выражений внутри вашего дерева конфигурации, вы можете использовать YAML и получить более читаемый файл конфигурации, например:

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

и используйте библиотеку, например PyYAML, для удобного анализа и доступа к файлу конфигурации.

person blubb    schedule 01.06.2011
comment
Но обычно вы хотите иметь разные файлы конфигурации и, следовательно, не иметь никаких данных конфигурации внутри вашего кода. Таким образом, «config» будет внешним файлом JSON / YAML, который вам нужно загружать с диска каждый раз, когда вы хотите получить к нему доступ, в каждом отдельном классе. Я считаю, что вопрос заключается в том, чтобы загрузить один раз и иметь глобальный доступ к загруженным данным. Как бы вы сделали это с предложенным вами решением? - person omni; 28.06.2018
comment
если бы что-то существовало для хранения данных в памяти ^^ - person cinatic; 31.07.2018
comment
Не забудьте включить файл конфигурации с чем-то from config import * - person Timo; 22.12.2020

Мне нравится это решение для небольших приложений:

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

И тогда использование:

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

.. вам должно понравиться, потому что:

  • использует переменные класса (нет объекта для передачи/не требуется синглтон),
  • использует инкапсулированные встроенные типы и выглядит как вызов метода для App,
  • имеет контроль над индивидуальной конфигурацией неизменяемость, изменяемые глобальные переменные — худший вид глобальных переменных.
  • продвигает обычный и хорошо названный доступ/удобочитаемость в исходном коде
  • является простым классом, но обеспечивает структурированный доступ, альтернативой является использование @property, но это требует больше кода обработки переменных для каждого элемента и основано на объектах.
  • требует минимальных изменений для добавления новых элементов конфигурации и настройки его изменчивости.

--Edit--: для больших приложений лучше хранить значения в файле YAML (т. е. свойств) и читать их как неизменяемые данные (например, blubb/ohaal's answer). Для небольших приложений это решение проще.

person pds    schedule 12.05.2017

Как насчет использования классов?

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

# main.py
from config import MYSQL

print(MYSQL.PORT) # 3306
person Husky    schedule 01.07.2017
comment
Я определенно предпочитаю этот подход, поскольку он хорошо работает с завершением кода в вашей среде IDE, и вам не нужно беспокоиться об опечатках в идентификаторах строк. Хотя я не уверен в производительности. Также неплохо работает при использовании внутренних классов для дальнейшего разделения записей. - person Boketto; 03.02.2021

Небольшая вариация идеи Хаски, которую я использую. Создайте файл с именем «globals» (или как вам угодно), а затем определите в нем несколько классов:

#globals.py

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

class runtime :
    debug = False
    output = 'stdio'

Затем, если у вас есть два файла кода c1.py и c2.py, оба могут иметь вверху

import globals as gl

Теперь весь код может получать доступ и устанавливать значения как таковые:

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

Люди забывают о существовании классов, даже если никогда не создается экземпляр объекта, являющегося членом этого класса. И переменные в классе, которым не предшествует «я». являются общими для всех экземпляров класса, даже если их нет. Как только «отладка» изменена каким-либо кодом, весь остальной код увидит это изменение.

Импортируя его как gl, вы можете иметь несколько таких файлов и переменных, которые позволяют вам получать доступ и устанавливать значения в файлах кода, функциях и т. д., но без опасности конфликта пространств имен.

В нем отсутствуют некоторые умные проверки ошибок других подходов, но он прост и легок в использовании.

person eSurfsnake    schedule 13.10.2017
comment
Не рекомендуется называть модуль globals, так как это встроенная функция, которая возвращает словарь с каждым символом в текущей глобальной области видимости. Кроме того, PEP8 рекомендует CamelCase (со всеми заглавными буквами в аббревиатурах) для классов (например, DBInfo) и прописными буквами с подчеркиванием для так называемых констант (например, DEBUG). - person Nuno André; 29.06.2018
comment
Спасибо @NunoAndré за комментарий, пока я его не прочитал, я думал, что этот ответ делает что-то странное с globals, автор должен изменить имя - person oglop; 25.03.2020
comment
Этот подход мне нравится. Однако я вижу много подходов, которые люди называют лучшими. Можете ли вы назвать некоторые недостатки реализации config.py в таком виде? - person Yash Nag; 08.04.2020
comment
Это шаблон глобального объекта, см. также шаблон singleton [поскольку в статье упоминается, что последний менее Pythonic] - person Louis Maddox; 22.04.2021

Давайте будем честными, нам, вероятно, следует рассмотреть возможность использования поддерживаемой библиотеки Python Software Foundation:

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

Пример конфигурации: (формат ini, но доступен JSON)

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

[bitbucket.org]
User = hg

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

Пример кода:

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

Сделайте его доступным по всему миру:

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

Недостатки:

  • Неконтролируемое глобальное изменяемое состояние.
person pds    schedule 18.11.2019
comment
Бесполезно использовать файл .ini, если вам нужно применить операторы if в других ваших файлах для изменения конфигурации. Вместо него было бы лучше использовать config.py, но если значения не меняются, а вы просто вызываете и используете его, я согласен с использованием файла .ini. - person Lulu; 05.12.2019
comment
Хм, configparser имеет функции read/write и раздела set/get. - person pds; 22.04.2021
comment
Но да, обработка вашей конфигурации как dict, а затем json.dump(d) и write является (меньше шагов, более распространенным и) более современным (текущим) способом управления конфигурацией.... - person pds; 22.04.2021

Похоже на ответ blubb. Я предлагаю создавать их с помощью лямбда-функций, чтобы сократить код. Так:

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

Это пахнет так, как будто вы, возможно, захотите создать класс.

Или, как заметил MarkM, вы можете использовать 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
person Cory-G    schedule 16.09.2014
comment
pass — неудачное имя переменной, поскольку оно также является ключевым словом. - person Thomas Schreiter; 17.12.2014
comment
Ах да... Я только что собрал этот тупой пример. я изменю имя - person Cory-G; 18.12.2014
comment
Для такого подхода вы можете рассмотреть класс вместо mkDict lambda. Если мы назовем наш класс User, ваши ключи словаря конфигурации будут инициализированы примерно как {'st3v3': User('password','blonde','Steve Booker')}. Когда ваш пользователь находится в переменной user, вы можете получить доступ к его свойствам как user.hair и т. д. - person Andrew Palmer; 29.03.2017
comment
Если вам нравится этот стиль, вы также можете использовать collections.namedtuple. User = namedtuple('User', 'passwd hair name'); config = {'st3v3': User('password', 'blonde', 'Steve Booker')} - person MarkM; 01.05.2018

пожалуйста, ознакомьтесь с системой конфигурации IPython, реализованной с помощью трейлетов для принудительного применения типов, которое вы делаете вручную.

Вырежьте и вставьте сюда, чтобы соответствовать рекомендациям SO, а не просто удалять ссылки, поскольку содержание ссылок меняется с течением времени.

документация по трейтлетам

Вот основные требования, которые мы хотели, чтобы наша система конфигурации имела:

Поддержка иерархической информации о конфигурации.

Полная интеграция с парсерами параметров командной строки. Часто вы хотите прочитать файл конфигурации, но затем переопределить некоторые значения с помощью параметров командной строки. Наша система конфигурации автоматизирует этот процесс и позволяет связать каждый параметр командной строки с определенным атрибутом в иерархии конфигурации, который он переопределяет.

Файлы конфигурации, которые сами по себе являются действительным кодом Python. Этим достигается многое. Во-первых, становится возможным поместить в ваши файлы конфигурации логику, которая устанавливает атрибуты в зависимости от вашей операционной системы, настройки сети, версии Python и т. д. Во-вторых, Python имеет очень простой синтаксис для доступа к иерархическим структурам данных, а именно обычный доступ к атрибутам (Foo. Бар.Бам.имя). В-третьих, использование Python позволяет пользователям легко импортировать атрибуты конфигурации из одного файла конфигурации в другой. В-четвертых, несмотря на то, что Python имеет динамическую типизацию, в нем есть типы, которые можно проверить во время выполнения. Таким образом, 1 в конфигурационном файле — это целое число «1», а «1» — это строка.

Полностью автоматизированный метод получения информации о конфигурации для классов, которым она нужна во время выполнения. Написание кода, который обходит иерархию конфигурации для извлечения определенного атрибута, является болезненным. Когда у вас есть сложная информация о конфигурации с сотнями атрибутов, вам хочется плакать.

Проверка типов и проверка, не требующие статического указания всей иерархии конфигурации перед выполнением. Python — очень динамичный язык, и вы не всегда знаете все, что нужно настроить при запуске программы.

Для этого они в основном определяют 3 класса объектов и их отношения друг к другу:

1) Конфигурация - в основном ChainMap/базовый словарь с некоторыми улучшениями для слияния.

2) Configurable — базовый класс для подкласса всего, что вы хотите настроить.

3) Приложение — объект, созданный для выполнения определенной прикладной функции, или ваше основное приложение для одноцелевого программного обеспечения.

По их словам:

Применение: Приложение

Приложение — это процесс, выполняющий определенную работу. Наиболее очевидным приложением является программа командной строки ipython. Каждое приложение считывает один или несколько файлов конфигурации и один набор параметров командной строки, а затем создает главный объект конфигурации для приложения. Затем этот объект конфигурации передается настраиваемым объектам, которые создает приложение. Эти настраиваемые объекты реализуют фактическую логику приложения и знают, как настроить себя с учетом объекта конфигурации.

Приложения всегда имеют атрибут журнала, который является настроенным Logger. Это позволяет централизованно настроить ведение журнала для каждого приложения. Настраиваемый: Настраиваемый

Конфигурируемый — это обычный класс Python, который служит базовым классом для всех основных классов в приложении. Базовый класс Configurable легковесен и выполняет только одну функцию.

Этот Configurable является подклассом HasTraits, который умеет настраивать себя. Черты уровня класса с метаданными config=True становятся значениями, которые можно настроить из командной строки и файлов конфигурации.

Разработчики создают подклассы Configurable, реализующие всю логику приложения. Каждый из этих подклассов имеет свою собственную информацию о конфигурации, которая управляет тем, как создаются экземпляры.

person jLi    schedule 14.04.2017