社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

谈 Python 项目的配置管理

编程派 • 8 年前 • 980 次点击  

原文:http://img2.100weidu.com/get?src=http://www.keakon.net/2016/10/22/Python项目的配置管理

作者:keakon

全文约 5874 字,读完可能需要 9 分钟。

每次开始一个新的 Python 项目,我都会为怎么管理配置文件而头疼。不过在迁移我的博客时,终于有空花了点时间,把这件事想清楚。

一年多的时间过去了,一切似乎都很顺利,连我在知乎所做的新项目也沿用了该方案,于是决定把解决方案记录下来。

先说说我要解决什么哪些问题吧:

  1. 可以区分各种环境。

在开发、测试和生产等环境,都可能用到不同的配置,所以能区分它们是一个很基本的需求。

  1. 可以有通用的配置项。

各种环境的配置中,需要修改的只占一小部分。因此通用的部分应该不需要重复定义,否则会带来维护成本。

  1. 可以分成多个部分/模块。

随着配置项的增多,找起配置来会花大量时间,所以划分它们对维护配置很有帮助。

  1. 可以直接使用 Python 代码。

从文本文件中解析出变量值太麻烦,而且不方便生成批量的数据(例如数组),也不好通过函数调用来生成配置值(例如获取文件路径)。

  1. 可以将公开和私有的配置文件分开管理。

在开源项目中,应只包含公开的配置项,而不包含私有的配置。不过这个需求对私有项目而言,没什么意义。

工作中我先后使用了几种方式,主要使用的就两种:

  1. 为每个环境分别写一个配置文件,到相应的环境里,将该环境的配置文件软链接到正确的路径。

  2. 使用分布式的配置服务,从远程获取配置。

前者用起来其实蛮麻烦的,特别是想在本地跑单元测试时,需要替换成单元测试环境的配置文件。所以我又把环境变量给加了进来,检测到指定的环境变量,就加载单元测试的配置。而其他几个需求也能勉强实现,不过并不优雅。

后者不能直接使用 Python 代码,网络不好时需要降级成使用本地缓存,获取配置服务器的地址需要配置,配置服务器自己也需要配置,而且配置服务器还可能挂掉(知乎内网遇到过全部五台配置服务器都挂掉的情况),所以我用得比较少。

其实仔细想想就能发现,「使用 Python 代码」也就意味着是 Python 源文件,「有通用的配置项」用 Python 实现就是继承,似乎没更好的选择了。

于是定义一个 Config 类,让其他环境的配置都继承这个类:

  1. # config/default.py

  2. class Config(object):

  3.    DEBUG_MODE = True

  4.    PORT = 12345

  5.    COOKIE_SECRET = 'default'

  6.    REDIS_CONFIG = { 'host': 'localhost', 'port': 6379, 'db': 0}

  7.    # ...

  1. # config/development.py

  2. from .default import Config

  3. class DevelopmentConfig(Config):

  4.    COOKIE_SECRET = 'dev'

  1. # config/unit_testing.py

  2. from .default import Config

  3. class UnitTestingConfig(Config):

  4.    REDIS_CONFIG = {'host': 'localhost', 'port': 6379, 'db': 1}

  1. # config/production.py

  2. from .default import Config

  3. class ProductionConfig(Config):

  4.    COOKIE_SECRET = '...'

  5.    REDIS_CONFIG = {'unix_socket_path': '/tmp/redis.sock'}

为了让每种环境都只有一个配置生效,还需要加一个策略:

  1. # config/__init__.py

  2. import logging

  3. import os

  4. env = os.getenv('ENV' )  # 可以改成其他名字,自己进行设置

  5. try:

  6.    if env == 'PRODUCTION':

  7.        from .production import ProductionConfig as CONFIG

  8.            logging.info('Production config loaded.')

  9.    elif env == 'TESTING':

  10.        from .testing import TestingConfig as CONFIG

  11.            logging.info('Testing config loaded.')

  12.    elif env == 'UNIT_TESTING':

  13.        from .unit_testing import UnitTestingConfig as CONFIG

  14.            logging.info('Unit testing config loaded.')

  15.    else:  # 默认使用本地开发环境的配置,省去设置环境变量的环节

  16.        from .development import DevelopmentConfig as CONFIG

  17.            logging.info('Development config loaded.')

  18. except ImportError:

  19.    logging.warning('Loading config for %s environment failed, use default config instead.', env or 'unspecified')

  20.    from .default import Config as CONFIG

这样只需要在跑应用前,设置不同的环境变量即可。如果是用 Supervisor 维护进程的话,加上一行 environment = ENV="PRODUCTION"配置即可。

当然还可以加其他的规则,例如没环境变量时,再检查机器名等。

现在前两个需求都解决了,再来看分模块的功能。

这个需求正好对应 Python 的 package,于是把每个配置文件改成一个 package 即可:

接着是如何同时满足第二和第三个需求。

举例来说,有这样的配置:

  1. # config/default.py

  2. class Config(object):

  3.    ROOT_PATH = '/'

  4.    LOGIN_PATH = ROOT_PATH + 'login'

  5.    SCHEME = 'http'

  6.    DOMAIN = 'localhost'

  7.    ROOT_URL = '%s://%s%s' % (SCHEME, DOMAIN, ROOT_PATH)

  1. # config/production.py

  2. from .default import Config

  3. class ProductionConfig(Config):

  4.    ROOT_PATH = '/blog/'

  5.    LOGIN_PATH = ROOT_PATH + 'login'

  6.    DOMAIN = 'www.keakon.net'

  7.    ROOT_URL = '%s://%s%s' % (Config.SCHEME, DOMAIN, ROOT_PATH)

其中,LOGINPATH 和 LOGINURL 的设置逻辑其实是一样的,但值却不同,在 ProductionConfig 中重新赋值一次有点不太优雅。

于是把这些设置提取出来,在基本设置初始化以后,再进行设置:

  1. class _AfterMeta(type):

  2.    def __init__(cls, name, bases, dct):

  3.        super(_AfterMeta, cls).__init__(name, bases, dct)

  4.        cls._after()

  5. class Config(object):

  6.    __metaclass__ = _AfterMeta

  7.    ROOT_PATH = '/'

  8.    SCHEME = 'http'

  9.    DOMAIN = 'localhost'

  10.    @classmethod

  11.    def _after(cls):

  12.        cls.LOGIN_PATH = cls.ROOT_PATH + 'login'

  13.        cls.ROOT_URL = '%s://%s%s' % (cls.SCHEME, cls.DOMAIN, cls.ROOT_PATH)

  1. # config/production.py

  2. from .default import Config

  3. class ProductionConfig(Config):

  4.    ROOT_PATH = '/blog/'

  5.    DOMAIN = 'www.keakon.net'

所有有依赖的设置项,都在 _after 方法里赋值即可。

不过这样可能导致静态检查和代码提示出问题,而且使得所有子类都重新定义这些属性,即便没有修改父类的属性,或是覆盖掉手动设置的值。所以可以再修改一下:

  1. class _AfterMeta(type):

  2.    def __init__(cls, name, bases, dct):

  3.        super(_AfterMeta, cls).__init__(name, bases, dct)

  4.        cls._after(dct)

  5. class Config(object):

  6.    __metaclass__ = _AfterMeta

  7.    ROOT_PATH = '/'

  8.    LOGIN_PATH = ROOT_PATH + 'login'

  9.    SCHEME = 'http'

  10.    DOMAIN = 'localhost'

  11.    ROOT_URL = '%s://%s%s' % (SCHEME, DOMAIN, ROOT_PATH)

  12.    @classmethod

  13.    def _after(cls, own_attrs):

  14.        if 'LOGIN_PATH' not in own_attrs and 'ROOT_PATH' in own_attrs:

  15.            cls.LOGIN_PATH = cls.ROOT_PATH + 'login'

  16.        if 'ROOT_URL' not in own_attrs and ('SCHEME' in own_attrs or 'DOMAIN' in own_attrs or 'ROOT_PATH' in own_attrs):

  17.            cls.ROOT_URL = '%s://%s%s' % (cls.SCHEME, cls.DOMAIN, cls.ROOT_PATH)

虽然问题是解决了,不过代码量似乎大了点,写起来很麻烦。只是似乎也没有更好解决办法,所幸这类配置并不多,所以重写一次倒也无妨。

最后只剩下分离公开和私有配置这个需求了。

既然要有私有配置,很容易想到的就是把私有配置放在另一个仓库里,再 link 到配置文件夹即可:

  1. .

  2. └── config

  3.    ├── __init__.py

  4.    ├── default.py

  5.    ├── development.py -> private/development.py

  6.    ├── development_sample.py

  7.    ├── private (cloned from another private repository)

  8.       ├── development.py

  9.       └── production.py

  10.    ├── production.py -> private/production.py

  11.    └── production_sample.py

为了避免文件被提交到公共仓库,私有的配置文件可以加到 .gitignore 里。

顺带一提,我的博客数据全存放在 Redis 中,备份时只要备份 rdb 文件即可。不过用另一台服务器来备份显得太奢侈了,所以我在服务器上装了个 Dropbox,然后把 Dropbox 文件夹里的数据文件夹 link 到博客的数据文件夹里,即:

  1. doodle

  2. └── data

  3.    └── redis -> ~/Dropbox/doodle/redis

这样一旦文件有改动,Dropbox 就会自动进行备份,而且保留了所有的历史版本,简直完美。


题图:pexels,CC0 授权。

点击阅读原文,查看更多 Python 教程和资源。

今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/weixin/iVxMBVfFCs
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/1840
 
980 次点击