如何进行python项目配置管理?这才是你最需要的python技术

Python015

如何进行python项目配置管理?这才是你最需要的python技术,第1张

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

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

先说说我要解决什么哪些Python项目的配置管理问题吧:

可以区分各种环境。

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

可以有通用的配置项。

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

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

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

可以直接使用 Python 代码。

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

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

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

工作中我先后使用了几种Python项目的配置管理方式,主要使用的就两种:

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

如何进行python项目配置管理?使用分布式的配置服务,从远程获取配置。

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

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

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

如何进行python项目配置管理?于是定义一个 Config 类,让其他环境的配置都继承这个类:

# config/default.pyclass Config(object):

DEBUG_MODE = True

PORT = 12345

COOKIE_SECRET = 'default'

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

# config/development.pyfrom .default import Configclass DevelopmentConfig(Config):

COOKIE_SECRET = 'dev'

# config/unit_testing.pyfrom .default import Configclass UnitTestingConfig(Config):

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

# config/production.pyfrom .default import Configclass ProductionConfig(Config):

COOKIE_SECRET = '...'

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

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

# config/__init__.pyimport loggingimport os

env = os.getenv('ENV') # 可以改成其他名字,自己进行设置try: if env == 'PRODUCTION': from .production import

ProductionConfig as CONFIG

logging.info('Production config loaded.') elif env == 'TESTING': from .testing import TestingConfig as CONFIG

logging.info('Testing config loaded.') elif env == 'UNIT_TESTING': from .unit_testing import UnitTestingConfig as

CONFIG

logging.info('Unit testing config loaded.') else: # 默认使用本地开发环境的配置,省去设置环境变量的环节

from .development import DevelopmentConfig as CONFIG

logging.info('Development config loaded.')except ImportError:

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

from .default import Config as CONFIG

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

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

如何进行python项目配置管理?现在前两个需求都解决了,再来看分模块的功能。

这个需求正好对应 Python 的 package,于是把每个Python项目的配置管理文件改成一个 package 即可。接着是如何同时满足第二和第三个需求。

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

# config/default.pyclass Config(object):

ROOT_PATH = '/'

LOGIN_PATH = ROOT_PATH + 'login'

SCHEME = 'http'

DOMAIN = 'localhost'

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

# config/production.pyfrom .default import Configclass ProductionConfig(Config):

ROOT_PATH = '/blog/'

LOGIN_PATH = ROOT_PATH + 'login'

DOMAIN = 'www.keakon.net'

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

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

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

class _AfterMeta(type):

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

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

cls._after()class Config(object):

__metaclass__ = _AfterMeta

ROOT_PATH = '/'

SCHEME = 'http'

DOMAIN = 'localhost' @classmethod

def _after(cls):

cls.LOGIN_PATH = cls.ROOT_PATH + 'login'

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

# config/production.pyfrom .default import Configclass ProductionConfig(Config):

ROOT_PATH = '/blog/'

DOMAIN = 'www.keakon.net'

如何进行python项目配置管理?所有有依赖的设置项,都在 _after 方法里赋值即可。

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

class _AfterMeta(type):

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

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

cls._after(dct)class Config(object):

__metaclass__ = _AfterMeta

ROOT_PATH = '/'

LOGIN_PATH = ROOT_PATH + 'login'

SCHEME = 'http'

DOMAIN = 'localhost'

ROOT_URL = '%s://%s%s' % (SCHEME, DOMAIN, ROOT_PATH) @classmethod

def _after(cls, own_attrs):

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

cls.LOGIN_PATH = cls.ROOT_PATH + 'login'

if 'ROOT_URL' not in own_attrs and ('SCHEME' in own_attrs or 'DOMAIN' in own_attrs or 'ROOT_PATH' in

own_attrs):

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

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

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

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

└── config

├── __init__.py

├── default.py

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

├── development_sample.py

├── private (cloned from another private repository)

│ ├── development.py

│ └── production.py

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

└── production_sample.py

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

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

doodle

└── data

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

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

如何进行python项目配置管理?这才是你最需要的python技术,我用于创建和管理虚拟环境的模块称为 venv。venv 通常会安装你可用的最新版本的 Python。如果您的系统上有多个版本的 Python,你能处理好吗?如果您还担心自己入门不顺利,那么本科目的其他文章一定会帮助你。

一、 为什么要使用配置

如果我们在较复杂的项目中不使用配置文件,我们可能会面临下面的情况:

你决定更改你的项目中数据库的 host, 因为你要将项目从测试环境转移到实际的生产环境中。如果你的项目中多个位置用到了这个 host,那你不得不一个一个找到这些位置再修改成新的 host。花了半天,然后过了一天,你发现项目在生产环境有些问题,需要重新移回测试环境,你得再次修改,这样工作很繁琐很不优雅。

你开发了一个很棒的开源项目,你想将其放到版本控制系统例如github上,但是你服务器的主机的地址、账号、密码也都上传上去了,但是你没有意识到,直到有个 bad guy 拿到了你的信息,从你的服务器窃取信息、攻击你的服务器,让你产生了极大的损失。然后你想把程序改动一下,把涉密的信息比如地址密码都删掉,可是由于版本控制的原因,别人依然能看到你以前版本的代码。于是你不得不改掉你的账户、密码等,真的是个悲伤的开源项目经历。

但是,如果你使用了配置管理呢,那会有以下几个优点:

这样就提高了代码的重用性,不再每次都去修改代码内部

这意味着其他不太懂你代码内部的人也可以使用你的项目,只用根据需求更改配置即可

有利于团队协作

有利于安全数据/秘密数据的管理

二、Python 中进行配置管理的几种方式

由于使用 Python 较多,因此基于 Python 进行配置管理的相关说明,当然其他语言也都是大同小异,主要思想还是不变。

2.1 使用 Python 内置的数据结构(如字典)

2.1.1单个文件下的单个配置

我们很自然就能想到这一点,例如以下代码:

在上面的代码中,我们可以看到,同一数据库配置,我们反复使用了两次,如果我们需要更改数据库相关的数据如password,我们不需要在两个方法内部修改,而是只用修改DATABASE_CONFIG字典中的相关值即可。和以前没有配置管理的时候相比,减少了太多的工作量了。

2.1.2多个文件下的单个配置

但是当你的项目开始变得复杂的时候,你的文件就不止一个这么简单了,这时候如果我需要在 main2.py 里面需要用 DATABASE_CONFIG 的时候就不是很方便了,因为如果直接 import main 的时候,虽然能够使用 main.DATABASE_CONFIG ,但同时 mian.py 中的

也被执行了,这可不是我们想看到的,因此我们有了新的需求,能在同一个项目下的不同文件里简单快速的导入我们的数据库配置 DATABASE_CONFIG,于是我们想出了下面的方法来解决这个问题:

按照上面的代码,我们可以在两个不同的文件 main1.py 和 main2.py 中分别引用 config.py 中配置了,我们的配置管理看起来更进一步了。

2.1.3 单个文件下的多个配置

有可能我们的项目需要多个配置文件,比如测试环境和生产环境。先从单个文件讲起,我们可以采用如下解决方案:

这样我们就可以从一个配置文件中获取不同级别的不同配置了。

2.1.4 多个文件下的多个配置

和上面类似,只不过换成了从不同的文件中读取同一个配置文件的不同配置:

这样使用更加灵活了,从不同的文件里读取不同的配置,而我们对于配置的增删改只需要在 config.py 中进行,配置管理技能再次进阶!

2.2 使用外部配置文件

比起使用 Python 内建的数据结构,更加通用的方法是使用外部配置文件,因为这些文件只会被视为配置文件,而不会像 config.py 一样有代码的属性。外部配置文件的格式多种多样,我们在使用它的时候会根据文件格式有不同的读取方式。例如:*.yaml 或者 *.yml、*.json、*.cfg 或 *.conf 、*.ini , 甚至是你自定义的文件 *.yourname 。

2.2.1 YAML

YAML(/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达数据序列化的格式。YAML参考了其他多种语言,包括:C语言、Python、Perl,并从XML、电子邮件的数据格式(RFC 2822)中获得灵感。Clark Evans在2001年首次发表了这种语言[1],另外Ingy döt Net与Oren Ben-Kiki也是这语言的共同设计者[2]。当前已经有数种编程语言或脚本语言支持(或者说解析)这种语言。

----- 中文维基百科

YAML 看起来像下面这种格式:

如果需要从 python 写入配置到 YAML 也很容易,只需要使用 yaml.dump(dict) 即可,dict 指的是配置的字典。更加详细的内容可以查看 PyYAML Documentation

2.2.2 INI

INI文件是一个无固定标准格式的配置文件。它以简单的文字与简单的结构组成,常常使用在Windows操作系统,或是其他操作系统上,许多程序也会采用INI文件做为设置程序之用。Windows操作系统后来以注册表的形式取代掉INI档。INI文件的命名来源,是取自英文“初始(Initial)”的首字缩写,正与它的用途——初始化程序相应。有时候,INI文件也会以不同的扩展名,如“.CFG”、“.CONF”、或是“.TXT”代替。

----- 中文维基百科

它长得像这样:

这将输出 INI 配置文件中的 mysql section 中的 host 值

要写入 INI 配置文件也很简单,参考如下代码即可:

2.2.3 JSON

JSON是JavaScript对象表示法的缩写。它非常广泛,因此对许多编程语言都有很好的支持。它的格式大家也很眼熟,看起来和 Python 中的字典很像:

要将配置写入json中也很简单,参考以下代码:

其他格式的文件大多如此,就不赘述了。并且外部的配置文件中也可以配置多个配置(mysql, other等)

2.3 使用环境变量

但是,回到我们开篇讲的问题,以上的两种配置管理方案(使用 Python 内置的数据结构、使用外部配置文件) 都忽略了两个问题:

其一,我们如何应对安全数据直接曝光于公众的可能问题呢,如果我们需要使用版本控制系统例如 Github,或许我们可以尝试将 config.py 文件放到 .gitignore 里面,但我们如果哪一天修改了仓库,忘了将 config.py 忽略掉而 push 到了GitHub 上,那么我们的安全敏感信息仍然会向公众泄露,由于版本控制的存在,即使你删掉了还会有这条提交记录,处理起来会很麻烦。

其二,如果我们要在我们本地新开一个项目,这个项目也需要引用一样的数据库配置文件,或许我们可以找到第一个项目的文件夹,复制出 config.py 到 新的项目文件夹。嗯,看起来可行,但是,如果你要新开十几个项目呢,几百个项目呢?

因此我们可以引入下一种配置管理的方式,对解决上面提出的两个问题都是较为友好的解决方案,即使用环境变量,各种开发环境(Win、Mac、Linux)的系统环境变量的设置方式有所不同,可以参考这篇文章。

另外 PyCharm 和 VS Code 有更加方便的配置方式,可以为不同的项目分配不同的设置。

PyCharm 中,在菜单 Run->Edit configurations 中,手动设置Environment variables

VS Code 中,在 Setting 中搜索 env ,在 Terminal 中选择你的操作系统相关的Terminal >Integrated >Env: Your OS ,点击 settings.json 进行添加

使用环境变量配置值不用作为单独的文件进行管理,因此有较小的安全风险,它很容易使用,可以在你的开发环境中的任何项目任何代码库中使用,但是它的管理方式可能有些复杂。有些环境无法使用环境变量,比如Apache,Nginx等Web服务器,这时候就需要采用其他的方式。

2.4 使用动态加载

这种方法比利用 Python 内置的数据结构更加先进,内置数据结构的方法要求配置文件必须要在可以直接 import 的路径上。但是动态加载中,配置文件不必在可直接导入的路径上,甚至可以位于其他存储库中,这样的话,配置文件就和项目分隔开了,其他的项目也可以动态加载这个配置文件,例如:

三、总结

以上归纳了四种配置管理的方式,总体来说没有优劣之分,看个人的需要,甚至上面的几种方法可以混合使用,对于一些软件项目,它自身可能就提供了相关的变量配置入口,比如 airbnb 的 Airflow 。而且,当系统规模非常大时,最好使用主要提供配置管理的第三方工具或服务,相关服务可以参考这里。