在 config.py 中提供全局配置变量的大多数 Pythonic 方式? [关闭]

Posted

技术标签:

【中文标题】在 config.py 中提供全局配置变量的大多数 Pythonic 方式? [关闭]【英文标题】:Most Pythonic way to provide global configuration variables in config.py? [closed] 【发布时间】:2011-09-06 02:28:25 【问题描述】:

在将简单的东西过度复杂化的无尽探索中,我正在研究最“Pythonic”的方法,以在 Python egg 包中的典型“config.py”中提供全局配置变量。

传统方式(aah, good ol' #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

或:

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

这似乎是一种在包内存储和获取全局变量的更具可读性、表现力和灵活性的方式。

有史以来最糟糕的想法?应对这些情况的最佳做法是什么? 你的在你的包中存储和获取全局名称和变量的方式是什么?

【问题讨论】:

您已经在这里做出了一个可能好也可能不好的决定。配置本身可以以不同的方式存储,例如 JSON、XML、*nixes 和 Windows 的不同语法等等。根据谁编写配置文件(工具、人、什么背景?)不同的语法可能更可取。大多数情况下,让配置文件以您用于程序的相同语言编写可能不是一个好主意,因为它为用户提供了太多的权力(可能是您自己,但您自己可能不记得所有可以几个月前出错)。 通常我最终会编写一个 JSON 配置文件。它可以很容易地读入 python 结构,也可以通过工具创建。它似乎具有最大的灵活性,唯一的成本是一些可能会让用户烦恼的牙套。不过,我从来没有写过鸡蛋。也许这是标准的方式。在这种情况下,请忽略我上面的评论。 你可以使用“vars(self)”代替“self.__dict__.keys()” What's the best practice using a settings file in Python? 的可能重复项他们回答“有很多方法是可能的,并且已经存在一个自行车棚线程。除非您关心安全性,否则 config.py 很好。” 当我读到“在我对过于复杂的简单事物的无尽追求中......”时,我突然大笑起来 【参考方案1】:

像这样使用内置类型怎么样:

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 这样的库来方便地解析和访问配置文件

【讨论】:

但通常您希望拥有不同的配置文件,因此您的代码中没有任何配置数据。因此,“config”将是一个外部 JSON / YAML 文件,每次您想在每个类中访问它时都必须从磁盘加载它。我相信问题是“加载一次”并对加载的数据进行类似全局的访问。您将如何使用您建议的解决方案来做到这一点? 如果存在将数据保存在内存中的东西^^ 不要忘记在配置文件中包含from config import *【参考方案2】:

我喜欢这种解决方案适用于小型应用程序

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,但这需要每个项目更多的变量处理代码并且是基于对象的。 只需极少的更改即可添加新的配置项并设置其可变性。

--编辑--: 对于大型应用程序,将值存储在 YAML(即属性)文件中并将其作为不可变数据读取是一种更好的方法(即blubb/ohaal's answer)。 对于小型应用程序,上面的这个解决方案更简单。

【讨论】:

【参考方案3】:

使用类怎么样?

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

# main.py
from config import MYSQL

print(MYSQL.PORT) # 3306

【讨论】:

我绝对更喜欢这种方法,因为它适用于您的 IDE 中的代码完成,而且您不必关心字符串 ID 中的拼写错误。我不确定性能。当使用内部类进一步分隔条目时,效果也很好。【参考方案4】:

说实话,我们可能应该考虑使用 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)
 ....

缺点:

不受控制的全局可变状态。

【讨论】:

如果您需要在其他文件中应用 if 语句来更改配置,则使用 .ini 文件是没有用的。改用config.py会更好,但是如果值不变,你直接调用使用,我同意使用.ini文件。 嗯,configparser 确实有 read/write 和部分 set/get 功能。 但是,是的,将您的配置处理为dict,然后是json.dump(d)write,是(更少的步骤,更常见的)一种更“现代”(当前)的方式来管理配置....【参考方案5】:

我使用的赫斯基想法的一个小变化。创建一个名为“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)

人们忘记了类的存在,即使没有实例化任何属于该类的对象。以及类中不以“self”开头的变量。在类的所有实例之间共享,即使没有。一旦任何代码更改了“调试”,所有其他代码都会看到更改。

通过将其作为 gl 导入,您可以拥有多个此类文件和变量,让您可以跨代码文件、函数等访问和设置值,但不会有命名空间冲突的危险。

这缺乏其他方法的一些巧妙的错误检查,但简单易懂。

【讨论】:

不建议将模块命名为globals,因为它是一个内置函数,它返回一个包含当前全局范围内每个符号的字典。此外,PEP8 建议类(即DBInfo)使用 CamelCase(所有首字母大写),所谓常量(即DEBUG)使用带下划线的大写。 感谢@NunoAndré 的评论,在我读到它之前,我一直在想这个答案对globals 有什么奇怪之处,作者应该更改名称 这种方法是我的首选。然而,我看到很多人们说是“最好的”的方法。你能说出这样实现 config.py 的一些缺点吗? 这是the global object pattern,另见the singleton pattern [正如文章提到的后者不那么Pythonic]【参考方案6】:

类似于 blubb 的回答。我建议使用 lambda 函数构建它们以减少代码。像这样:

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

【讨论】:

pass 是一个不幸的变量名,因为它也是一个关键字。 哦,是的......我只是把这个愚蠢的例子放在一起。我要改名字 对于这种方法,您可以考虑使用类而不是 mkDict lambda。如果我们调用我们的类User,您的“配置”字典键将被初始化为'st3v3': User('password','blonde','Steve Booker')。当您的“用户”位于user 变量中时,您可以通过user.hair 等方式访问其属性。 如果你喜欢这种风格,你也可以选择使用collections.namedtuple。 User = namedtuple('User', 'passwd hair name'); config = 'st3v3': User('password', 'blonde', 'Steve Booker')【参考方案7】:

我做过一次。最终我发现我的简化版basicconfig.py 足以满足我的需求。如果需要,您可以传入带有其他对象的名称空间以供参考。您还可以从代码中传递其他默认值。它还将属性和映射样式语法映射到相同的配置对象。

【讨论】:

我知道这已经有几年了,但我是一个初学者,我认为这个配置文件本质上是我正在寻找的(可能太高级了),我想更好地理解它.我是否只需使用我想设置的配置字典传递初始化 ConfigHolder 并在模块之间传递? @Jinx 此时我将使用(并且目前正在使用)一个 YAML 文件和 PyYAML 进行配置。我还使用了一个名为confit 的第三方模块,它支持合并多个源。它是新的devtest.config 模块的一部分。【参考方案8】:

请查看 IPython 配置系统,通过 traitlets 实现您手动执行的类型强制。

在此处剪切和粘贴以遵守 SO 准则,不要只是随着链接内容的变化而丢弃链接。

traitlets documentation

以下是我们希望配置系统具备的主要要求:

支持分层配置信息。

与命令行选项解析器完全集成。通常,您希望读取配置文件,然后使用命令行选项覆盖某些值。我们的配置系统会自动执行此过程,并允许将每个命令行选项链接到配置层次结构中将覆盖的特定属性。

配置文件本身就是有效的 Python 代码。这完成了很多事情。首先,可以将逻辑放入配置文件中,根据操作系统、网络设置、Python 版本等设置属性。其次,Python 具有访问分层数据结构的超级简单语法,即常规属性访问(Foo. Bar.Bam.name)。第三,使用 Python 使用户可以轻松地将配置属性从一个配置文件导入到另一个配置文件。 第四,尽管 Python 是动态类型的,但它确实具有可以在运行时检查的类型。因此,配置文件中的 1 是整数“1”,而“1”是字符串。

一种完全自动化的方法,用于在运行时将配置信息传递给需要它的类。编写遍历配置层次结构以提取特定属性的代码是痛苦的。当你有数百个属性的复杂配置信息时,这让你想哭。

类型检查和验证不需要在运行前静态指定整个配置层次结构。 Python 是一种非常动态的语言,您并不总是知道程序启动时需要配置的所有内容。

为了实现这一点,他们基本上定义了 3 个对象类及其相互关系:

1) 配置 - 基本上是一个 ChainMap / 基本字典,带有一些合并增强功能。

2) 可配置 - 基类来继承您希望配置的所有内容。

3) 应用程序 - 被实例化以执行特定应用程序功能的对象,或用于单一用途软件的主应用程序。

用他们的话来说:

应用:应用

应用程序是执行特定工作的进程。最明显的应用是 ipython 命令行程序。每个应用程序读取一个或多个配置文件和一组命令行选项,然后为应用程序生成一个主配置对象。然后将此配置对象传递给应用程序创建的可配置对象。这些可配置对象实现了应用程序的实际逻辑,并且知道如何在给定配置对象的情况下配置自己。

应用程序始终有一个日志属性,即配置的 Logger。这允许每个应用程序集中日志配置。 可配置:可配置

可配置是一个常规的 Python 类,它充当应用程序中所有主要类的基类。 Configurable 基类是轻量级的,只做一件事。

这个 Configurable 是 HasTraits 的子类,它知道如何配置自己。元数据 config=True 的类级特征成为可以从命令行和配置文件配置的值。

开发人员创建可配置的子类来实现应用程序中的所有逻辑。这些子类中的每一个都有自己的配置信息,用于控制如何创建实例。

【讨论】:

以上是关于在 config.py 中提供全局配置变量的大多数 Pythonic 方式? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

API接口自动化测试框架搭建(二十二)-全局变量config.py完整代码及解读

为什么更新全局变量在导入的函数中定义的函数中不起作用

Flask app.config 的配置

Flask:通过 OS 环境变量覆盖 config.py 文件的环境变量

多个进程之间共享配置变量

python项目中包含多个文件&全局内容&函数定义时,语句的执行顺序