配置文件一招鲜 转载


               
配置文件一招鲜
沈崴 | 2007-3-2 13:59:25
毫无疑问, 你需要配置文件。据说配置文件能让你的系统变得可定制。
让系统变得可定制, 有三大思潮, 这决定了三种配置文件的格式:
    1. 图形界面配置工具。
       微机和 Windows 开始流行之后, 我们的用户再也不是黑客了,
       或许你的用户永远不会打开你的配置文件, 你需要给他一个图形工具, 并且帮他换尿布。
       在人类永远不会阅读配置文件的情况下, 让系统变得更简洁的方法就是使用
       机器最容易理解的格式来存储配置, 通常这是一个二进制文件。
       Pickle 是 Python 中的典型选项。
       几十年后, 有人开始以为使用机器和人都能理解 (或者都不能理解) 的格式是一个好主意, 这样就有了 XML。
       在这里, 我只想告诉大家:
           XML 确实是好东西, 但是在绝大多数情况下你其实并不真的需要他。
       而在 Zope/Plone 中, 配置被大规模地保存在 ZODB 数据库中。
       这样就产生了第一种配置文件类型, 他是人类不可读的。
    2. 常规格式的配置文件, 有著名的 httpd.conf 和 INI 等。
       基本上, INI 很弱, 而且 INI 的支持库并没有提供诸如值验证甚至设置变量默认值的功能,
       因此, 除非项目非常小你不应该使用 INI 格式。
           而在任何时候都不使用 INI 格式, 有助于你养成良好的习惯。
       httpd.conf Like 的配置文件, 是配置文件格式的巅峰之作。
       在 Python 中 ZConfig (位于 Zope2/Zope3 中) 提供了对于此种格式的支持。
       我这里有一份入门简介:
        http://eishn.blog.163.com/blog/static/65231820069310828642/
       它只是入门介绍, 你需要阅读文档、源码或者范例才能完全了解 ZConfig 的工作方式。
       尽管并不容易, 但是理解 ZConfig 会帮助你快速掌握配置文件的设计哲学。
       常规的配置文件设计和解析过于复杂, 因此你可以仅作了解。
    3. 使用 Python 本身来作为配置文件。
       Why not? 目前有许多 C 程序正在使用 Python 作为配置文件,
       而 Lighttpd 的配置文件也已经八九不离十了。
       这里是一个例子:
           # demo_conf.py
           # 配置文件
           host = '127.0.0.1'
           port = 8080
           # demo.py
           import demo_conf
           print demo_conf.host, demo_conf.port
       利用 Python 脚本作为配置文件, 在程序实现上简单, 无须格式转换, 而且功能强大。
       这个方案已经开始向传统的配置文件格式宣战。
       最后我修改了原定准备介绍 ZConfig 的计划, 今天着重来介绍这个方案。
下面, 就让我们来设计自己的配置文件格式。
要搞清楚如何设计一个配置文件, 我们首先需要搞清楚系统中有哪些数据类型是需要配置的,
并且弄清楚他们在多大程度上是可以配置的。
根据经验, 一个系统中可以定制的数据有三种:
    1. "静态" 数据, 在 C 语言中, 是用 #define 定义的。
       此类数据仅在开发期具有动态性, 一旦系统完成开发, 此类数据就是静态的。
       此类数据的变化频率最低。
       Python 程序员通常将变量名大写, 并且放在 "config.py" 中,
       通过 import config 导入使用。
    2. "默认" 配置, 这是项目配置部分最大的乱源。
       默认配置是一种用户可以在配置文件中修改的数据, 他们不是必须的, 他们有默认值。
       你很快就会发现许多 "默认" 配置实际上用户永远不会去修改他,
       在这种情况下, 这些默认配置实际上是上面所说的 "静态" 数据, 应该写入 "config.py"。
       而另一些则是常用配置, 只是带有默认值而已, 这些数据应该写入 "etc" 配置文件中。
       显而易见, 一个变量不能同时存在于 config.py 和 etc 配置文件中。
       问题在于搞清楚一个变量是属于 "静态" 数据还是 "默认" 配置是困难和不确定的。
       这最终导致你的配置文件定义朝令夕改, 而你的项目则风雨飘摇。
    3. 配置项, 毫无疑义地应该放在配置文件中。在系统投入使用后, 这是变化频率最高的数据类型。
       在配置文件中, 你又将面临另一大挑战, 就是变量类型验证。
       变量类型验证之所以讨厌, 就是因为你要同时判断一个配置变量是否存在、类型是否匹配或者是否越界,
       并且是否给以默认值。最后最糟糕的是你还需要回答一个问题:
           默认值是否需要写在配置文件中。
为了解决所有这些问题, 人们发明了 Schema。
和传统配置文件不同, 使用 Python 作为配置文件, Schema 的编写具有相当大的灵活性。
而且 Schema 能帮助你把 Python 格式的配置文件变得更加可读。
下面我就演示一下在 Python 作为配置文件格式时 Schema 的写法。
    # /PATH/TO/ETC/demo_conf.py
    # 配置文件
    import schema
    Server(
        host = '127.0.0.1',
        port = 8080
    )
    # -EOF-
    # /PATH/TO/ETC/schema.py
    #
    config = {}
    def Server(**args):
        config['host'] = args.get('host', '0.0.0.0') # 有默认值
        try:
            config['port'] = int(args.get('port', 8080)) # 带验证
        except ValueError:
            raise ValueError, '你必须是整型'
    # -EOF-
    # /PATH/TO/BIN/demo.py
    #
    import sys
    sys.path.append(r'/PATH/TO/ETC')
    import schema, demo_conf
    print schema.config['host'], schema.config['port']
    # -EOF-
好了, 接下来我们需要把 demo_conf.py 变得更像一个配置文件,
因为 "import schema" 不像是一个配置文件中应该有的东西, 我们要让 "Server" 成为预导入的变量。
最后, 我们还要把 demo_conf.py 变成 demo.conf。
这样继 PyQt4 一招鲜之后, 我们又要再次接触到自定义导入技术。
这次我们将使用类似的技术, 也是一个导入钩子 (hook), 不同的是, 这次我们不将钩子挂接到 Python 解释器。
################################################################## # -BOF-
# pyetc.py
# Python 格式的配置文件支持库
#
import sys, os.path
Module = type(sys) # 故技重演
modules = {}       # 缓存已经导入的 etc (配置) 模块
# 导入任意符合 Python 语法的文件
# 用法:
# module = pyetc.load(完整文件路径并包含扩展名, 预载入变量, 自定义返回模块类型)
#
def load(fullpath, env={}, module=Module):
    try:
        code = open(fullpath).read()
    except IOError:
        raise ImportError, 'No module named  %s' %fullpath
    filename = os.path.basename(fullpath)
    try:
        return modules[filename]
    except KeyError:
        pass
    m = module(filename)
    m.__module_class__ = module
    m.__file__ = fullpath
    m.__dict__.update(env)
    exec compile(code, filename, 'exec') in m.__dict__
    modules[filename] = m
    return m
# 移除已经导入的模块
# 用法:
# module = unload(module)
#
def unload(m):
    filename = os.path.basename(m.__file__)
    del modules[filename]
    return None
# 重新导入模块
# 用法:
# module = pyetc.reload(module)
#
def reload(m):
    fullpath = m.__file__
    try:
        code = open(fullpath).read()
    except IOError:
        raise ImportError, 'No module named  %s' %fullpath
    env = m.__dict__
    module_class = m.__module_class__
    filename = os.path.basename(fullpath)
    m = module_class(filename)
    m.__file__ = fullpath
    m.__dict__.update(env)
    m.__module_class__ = module_class
    exec compile(code, filename, 'exec') in m.__dict__
    modules[filename] = m
    return m
################################################################## # -EOF-
下面就让我们来尝试一下。
    # /PATH/TO/ETC/demo.conf
    # 配置文件
    host = '127.0.0.1'
    port = 8080
    # -EOF-
    # demo.py
    import pyetc
    conf = pyetc.load(r'/PATH/TO/ETC/demo.conf')
    print conf.host, conf.port
    # -EOF-
最后是一个较为完整的例子:
    # /PATH/TO/ETC/demo.conf
    # 服务器选项
    #
    Server(
        port = 8080 # 监听 8080 端口
    )
    # 进程控制器选项
    #
    Daemon(
        # 使用 Socket 发布进程控制器
        #     address = ('0.0.0.0', 10080),
        # 使用文件发布进程控制器
        address = var('demo.pid'),
        # 服务器进程
        program = bin('server.py'),
        # 调试开关
        verbose = True
    )
    # -EOF-
    # /PATH/TO/BIN/schema.py
    # 路径工具
    import sys, os.path
    DEMO_HOME = r'/PATH/TO/DEMO'
    ETC = lambda filename: os.path.join(DEMO_HOME, 'etc', filename)
    VAR = lambda filename: os.path.join(DEMO_HOME, 'var', filename)
    BIN = lambda filename: os.path.join(DEMO_HOME, 'bin', filename)
    #
    class Config(dict):
        # 可以像属性一样访问字典的 Key
        #     dict.key 等同于 dict[key]
        def __getattr__(self, name):
            return self[name]
    # 配置默认值
    config = Config({
        'server': Config({
            'port': 8080 # 服务器使用 8080 端口
            }),
        'daemon': Config({
            'address': VAR('daemon.pid'), # pid 文件
            'program': BIN('server.py' ), # 服务器程序
            'verbose': True
            })
    })
    # 配置接口 (不带验证)
    def Server(**args):
        config['server'].update(args)
    def Daemon(**args):
        config['daemon'].update(args)
    # 配置文件 "demo.conf" 可见的变量
    env = {'Server': Server, 'Daemon': Daemon,
        'etc': ETC, 'var': VAR, 'bin': BIN}
    # -EOF-
    # /PATH/TO/BIN/demo.py
    # 这里启动一个 Daemon 管理器,
    # 注意: 这里的 daemon.py 是一个假想库, 无须理会
    import pyetc
    from daemon import Daemon
    def start():
        # 读取配置文件
        # demo.conf
        pyetc.load(schema.ETC('demo.conf'), env=schema.env)
        conf = schema.config.daemon
        # 创建 Daemon 对象
        daemon = Daemon(
            address = conf.address, # 进程控制器地址/pid 文件位置
            program = conf.program, # 后台进程程序位置
            verbose = conf.verbose  # 调试
            )
        print '进程管理器已经启动'
        daemon()
    if __name__ == '__main__':
        start()
    # -EOF-
附件: pyetc.py    1 KB    by 沈崴  (2007-03-02 13:59:25)