覆盖一组命名空间常量

0 投票
2 回答
544 浏览
提问于 2025-04-17 15:47

为了减少程序中硬编码的值,我定义了一组常量,像下面这个 Constants.py 文件:

FOO = 42
HOSTNAME=socket.gethostname()
BAR = 777

所有想使用这些常量的模块,只需执行 import Constants,然后就可以直接使用 Constants.FOO

不过,有些常量可能会依赖于程序运行的具体主机。因此,我想根据应用程序运行的实际环境,有选择地覆盖其中的一些常量。

我最初的尝试看起来像这样 Constants.py

FOO = 42
HOSTNAME=socket.gethostname()
if HOSTNAME == 'pudel':
   BAR = 999
elif HOSTNAME == 'knork'
   BAR = 888
else:
   BAR = 777

虽然这样可以工作,但会让文件变得杂乱,因为里面有很多特殊情况,我想避免这种情况。

如果我在写 shell 脚本,我会用类似这样的方式 Constants.sh

 FOO = 42
 HOSTNAME=$(hostname)
 BAR = 777

 # load host-specific constants
 if [ -e Constants_${HOSTNAME}.sh ]; then
   . Constants_${HOSTNAME}.sh
 fi

还有一个可选的 Constants_pudel.sh,内容如下:

BAR = 999

这样可以把常用的常量放在一起,并且方便在不同的文件中覆盖它们。

但我现在不是在写 shell 脚本,而是在写 Python 程序,所以我想知道如何实现同样的效果。

我尝试了一些方法,比如:

FOO = 42
HOSTNAME=socket.gethostname()
BAR = 777
try:
  __import__('Constants_'+HOSTNAME)
except ImportError:
  pass

Constants_poodle.py 的内容会是:

import Constants
Constants.BAR = 999

这样在 Constants_poodle 内部是可以工作的,但当我在另一个 Python 文件中尝试 import Constants 时,得到的还是原来的 Constants.BAR

除了完全不工作之外,使用 __import__() 看起来也特别丑陋,所以我想知道有没有更合适的方法来为特定的设置覆盖导出的常量?

2 个回答

1

你可以用下面的方式来实现这个功能,这个方法来源于Activestate的一个关于Python常量的示例:Python中的常量

import os as _os
import socket as _socket

class _constants(object):
    def __init__(self):
        self.FOO = 42
        self.HOSTNAME = _socket.gethostname()
        self.BAR = 777

        # load host-specific constants
        hostconst = 'Constants_{}'.format(self.HOSTNAME)
        if _os.path.exists(hostconst):
            localdict = {}
            execdict = self.__dict__.copy()
            execdict['__builtins__'] = None
            execfile(hostconst, execdict, localdict)
            self.__dict__.update(localdict) # add/override attributes defined

    def __setattr__(self, name, value):
        self.__dict__[name] = value

# replace module entry in sys.modules[__name__] with instance of _constants
# (and create additional reference to module so it's not deleted --
# see Stack Overflow question: http://bit.ly/ff94g6)
import sys
_ref, sys.modules[__name__] = sys.modules[__name__], _constants()

if __name__ == '__main__':
    import constants

    print constants.FOO
    print constants.HOSTNAME
    print constants.BAR

举个例子,如果_socket.gethostname()返回了'pudel',并且有一个名为Constants_pudel的文件,里面包含以下内容:

BAR = 999
FOO += 1

那么从print语句输出的结果将会是:

43   
pudel
999  
1

你的解决方案有几个问题。首先,import 并不会把导入的模块添加到当前的命名空间里。我不太确定在 Python 3.x 中是怎样的,但在 Python 2.x 中,你可以这样做:

FOO = 42
BAR = 777
HOSTNAME=socket.gethostname()
try:
    _imp = __import__('Constants_'+HOSTNAME)
    _locals = locals()
    for _name in dir(_imp):
        if not _name.startswith('_'):
            _locals[_name] = getattr(_imp, _name)
    del _imp, _locals, _name
except ImportError:
    pass

但是下一个问题是,所有的 constants_xxx.py 文件都必须在 Python 的路径中。

我认为一个更好的替代方案是把配置放在用户目录下的 .ini 文件中,然后使用 ConfigParser(或者根据你的喜好使用 yaml 或 xml)。

撰写回答