源码看Flask框架配置管理

wushaojun 2019-08-17

1 引言

Flask作为Python语言web开发的三大顶梁柱框架之一,对于配置的管理当然必不可少。一个应用从开发到测试到最后的产品发布,往往都需要多种不同的配置,例如是否开启调试模式、使用哪个数据库等等,这些配置都可能因开发阶段和环境而异。

2 Flask配置类:Config

为了达到对配置方便快捷而又灵活管理的目的,Flask提供了一个名为“config的”属性,这个属性在Flask应用实例化时创建,所以,只要创建了Flask应用,就可以使用这个config属性进行配置管理。我们先创建一个Flask应用,去看一看这个config属性:

from flask import Flask

app = Flask(__name__)
print(type(app.config))

 输出结果:

<class 'flask.config.Config'>

可以看出,app.config是一个类,一个定义在flask.config模块中的类。既然是一个类,我们就可以推测,Flask在实例化应用时,也实例化了这个Config类,我们通过这个类提供的各种属性、方法来进行配置管理。如果你用的IDE是pycharm,按住Ctrl鼠标左键点击app.config中的config就可以定位到Flask类中定义config属性的源码,这一行源码如下:

self.config = self.make_config(instance_relative_config)

在一行代码在Flask构造方法__init__()中,正如刚才所说,确实是在实例化Flask应用时创建了config属性。不过在值是Flask类中的make_config()方法的返回值,参数instance_relative_config是__init__()方法的参数,默认为False,具体功能我们在下文解析时用到再说。现在,我们去看一下make_config()方法的源码:

def make_config(self, instance_relative=False):
        root_path = self.root_path  # root_path是主模块所在绝对路径
        # 下面这个instance_relative就是Flask构造方法里面的instance_relative_config
        if instance_relative:  # 如果实例化Flask传入的instance_relative_config为True
            root_path = self.instance_path  # instance_path也是Flask构造方法中的参数,是一个路径,如果实例化Flask时没有为instance_path传参则默认路径为Flask实例同级目录下的instance目录
        defaults = dict(self.default_config)  # 读取Flask初始化时的默认配置
        defaults["ENV"] = get_env()  # 判断环境类型:production或development,即生产环境或开发环境,设置这个值是因为有些应用需要根据这个值来改变行为
        defaults["DEBUG"] = get_debug_flag()  # 是否开启调试模式
        return self.config_class(root_path, defaults)  # 实例化一个Config类

make_config()方法执行可以分为4个步骤:读取配置文件路径、读取默认配置、设置环境和模式、创建Config配置类。可以说,前面3个步骤都是为创建Config类做准备,里面的细节大家看上面代码注释就明白了,重点在于创建Config类,继续往下查看config_class:

config_class = Config  # 将Config赋值给config_class

config_class就是Config类,这里只不过做了一个赋值。继续查看Config:

class Config(dict):
    def __init__(self, root_path, defaults=None):
        dict.__init__(self, defaults or {})
        self.root_path = root_path

当看到这个Config类代码时,仿佛一切都恍然大悟——一切配置操作都在这里。从源码中我们可以看到,Config类继承了dict,也即是说,Config类就是一个字典,一切字典所拥有的使用方法,在Config类上也行得通。

大概浏览config.py文件,可以看到,在Config类中还提供了几个名称很相似的方法:

from_object(self, obj)

from_pyfile(self, filename, silent=False)

from_envvar(self, variable_name, silent=False)

from_json(self, filename, silent=False)

from_mapping(self, *mapping, **kwargs)

阅读方法文档获知,这几个方法是读取配置用的,只不过读取的目标不一样,也就是说,Flask通过提供这几个方法为用户提供了多种配置管理方式。

接下来,我们来捋一捋Flask的配置管理方式。

3 配置方式1:直接赋值

通过上面的分析我们知道,Config类继承类字典类,所以我们可以用字典的方式进行配置管理,例如是config['key'] = value的方式赋值,通过config.get(key)方式取值:

from flask import Flask
 
app = Flask(__name__)
app.config['DEBUG'] = True
print('是否开始调试模式:', app.config.get('DEBUG'))

输出:

是否开始调试模式: True

注意:Flask中所有配置名称都是大写。上面DEBUG配置中,如果写成了debug,那就会在app.config中添加一个debug的配置,而不是修改DEBUG,开启调试模式就会失败。

也可以调用字典类中的一些方法,例如调用update方法一次性设置多个值:

from flask import Flask
 
app = Flask(__name__)
app.config.update(
    DEBUG=True,
    TESTING=True
)
print('debug:', app.config.get('DEBUG'))
print('testing:', app.config.get('TESTING'))

甚至可以将一些默认配置中没有的值存入配置中:

from flask import Flask
 
app = Flask(__name__)
app.config['aaaaa'] = '我是aaaaa'
print(app.config['aaaaa'])

输出:

我是aaaaa

对于一些小应用来说,这种确实很是简单方便,但是对于更为复杂的应用,可能需要针对不同的环境使用不同的配置,配置的内容又多,这种方法就显得麻烦了。这时候就需要用到Config类中实现的几个方法了。

4 配置方式2-对象中配置:from_object(推荐)

先来看看from_object()方法的源码:

def from_object(self, obj):
        if isinstance(obj, string_types):  # 判断obj是否是str类型
            obj = import_string(obj)  # 如果是str类型,就根据这个字符串导入对象
        for key in dir(obj):  # 遍历obj的所有值
            if key.isupper():
                self[key] = getattr(obj, key)  # self指的就是config实例本身,通过getattr取出对应的值进行

从源码可以看出,from_object()方法说接收的参数obj可以使str类型,可以是一个模块,甚至是一个类。

我们先尝试一下是一个模块的情况,创建一个settings.py模块,内容如下:

DEBUG = False
TESTING = False

这里只写了两个配置,你可以写更多,无所谓。怎么使用呢?

from flask import Flask
import settings
 
app = Flask(__name__)
app.config.from_object(settings)
print('DEBUG:', app.config.get('DEBUG'))
print('TESTING:', app.config.get('TESTING'))
print('A:', app.config.get('A'))

输出:

DEBUG: True

TESTING: True

A: 123

当obj是一个字符串时:

from flask import Flask
 
app = Flask(__name__)
app.config.from_object('settings')
print('DEBUG:', app.config.get('DEBUG'))
print('TESTING:', app.config.get('TESTING'))
print('A:', app.config.get('A'))

输出:

DEBUG: True

TESTING: True

A: 123

看出来了吗?无论是使用app.config.from_object(settings)还是app.config.from_object('settings')使用的都是使用settings.py文件中的配置,至于原因,如果不明白就回去看看上面的源码。

如果obj是一个类时,我们修改一下settings.py,如下:

class Config(object):
    DEBUG = False
    TESTING = False
    DATABASE_URI = 'sqlite://memory:'

class ProductionConfig(Config):
    DATABASE_URI = 'mysql://user@localhost/foo'

class DevelopmentConfig(Config):
    DEBUG = True

class TestingConfig(Config):
    TESTING = True

在settings.py模块中,我们定义了多个类,首先是Config类,这个类定义的是默认配置,其他类都继承Config类,每一个之类代表一种配置,如果需要子类中可以覆写Config,如果不覆写则使用Config中的默认配置。怎么使用呢?

from flask import Flask
import settings
 
app = Flask(__name__)
app.config.from_object(settings.ProductionConfig)
print('DEBUG:', app.config.get('DEBUG'))
print('TESTING:', app.config.get('TESTING'))
print('DATABASE_URI:', app.config.get('DATABASE_URI'))

输出:

DEBUG: False

TESTING: False

DATABASE_URI: mysql://user@localhost/foo

使用这种方法的好处是可以充分利用面向对象中继承等的优良特性共享配置,设置多套配置,使用时,只需要针对实际需要修改app.config.from_object(settings.ProductionConfig)中传入的类即可。这种方法在实际开发中也是使用最多的。

5 配置方式3-py文件:from_pyfile

继续解析源码:

def from_pyfile(self, filename, silent=False):
        filename = os.path.join(self.root_path, filename)  # 拼接路径
        d = types.ModuleType("config")  # 创建一个模块对象
        d.__file__ = filename
        try:
            with open(filename, mode="rb") as config_file:  #将文件内容解析到d
                exec(compile(config_file.read(), filename, "exec"), d.__dict__)
        except IOError as e:
            if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
                return False
            e.strerror = "Unable to load configuration file (%s)" % e.strerror
            raise
        self.from_object(d)  # 调用上面说到过的from_object()方法
        return True

在上一章节分析from_object()方法时,我们说到,from_object()方法可以接受一个模块作为参数,from_pyfile()方法接受的就是一个py文件作为参数,在Python中一个py文件就是一个模块,那from_object()方法与from_pyfile()方法有什么区别呢?从源码汇总我们可以看出,from_pyfile()方法接受一个文件名作为参数,我们可以认为,使用from_pyfile()方法读取配置时,我们只能直接将配置写在py文件中,而不能是写在py文件中定义的类。from_pyfile()方法思路就是传入一个py文件名,然后对文件进行解析,转为模块对象,调用from_object()方法对解析到的模块对象读取配置。

分析完了我们就来使用一下吧,settings.py文件内容如下:

DEBUG = True
TESTING = True
A = 123

读取配置:

from flask import Flask
 
app = Flask(__name__)
app.config.from_pyfile('settings.py')
print('DEBUG:', app.config.get('DEBUG'))
print('TESTING:', app.config.get('TESTING'))
print('A:', app.config.get('A'))

输出:

DEBUG: True

TESTING: True

A: 123

6 配置方式4-字典元组:from_mapping

这种方式是以元组或者字典的形式来管理配置,先来看看源码:

def from_mapping(self, *mapping, **kwargs):
        mappings = []  # 用于存放待会儿解析出来的数据
        if len(mapping) == 1:  # 只能接受一个位置参数
            if hasattr(mapping[0], "items"):  # 如果是字典
                mappings.append(mapping[0].items()) # 以(key, value)的形式放到mappings列表中
            else:  
                mappings.append(mapping[0])  # 如果不是字典,直接放到mappings列表中
        elif len(mapping) > 1:  # 如果位置参数数量多于1个就会抛出异常
            raise TypeError(
                "expected at most 1 positional argument, got %d" % len(mapping)
            )
        mappings.append(kwargs.items())  # 对于关键字参数,则直接以(key, vlaue)形式放到mappings列表中
        for mapping in mappings:
            for (key, value) in mapping:
                if key.isupper():  # 如果key是大写的,才会修改配置
                    self[key] = value
        return True

就算看完了上面的代码解析,你也许知道了代码做了什么,但是却还不知道为什么这么做,来,我们尝试使用一下也许你就明白了:

from flask import Flask

app = Flask(__name__)
tuple_config = (
    ('DEBUG', True),
    ('TESTING', False)
)
dict_config = {
    'DEBUG': True,
    'TESTING': False
}
# app.config.from_mapping(tuple_config, A=123, B=456)  # 使用元组
app.config.from_mapping(dict_config, A=123, B=456)  # 使用字典
print('DEBUG:', app.config.get('DEBUG'))
print('DEBUG:', app.config.get('DEBUG'))
print('TESTING:', app.config.get('TESTING'))
print('A:', app.config.get('A'))
print('B:', app.config.get('B'))

上面代码中,我们定义了元组和字典(实际开发中最好在一个专门的模块中定义),使用元组进行配置的方法我注释掉了,运行效果都是一样的,你可以调试一下,加深理解源码。输入如下:

DEBUG: True

TESTING: False

A: 123

B: 456

7 配置方式5-json文件:from_json

如果你喜欢用json文件的方式来管理配置,那么,from_json()方法刚好适合你,我们来了看看这个方法的实现:

def from_json(self, filename, silent=False):
        filename = os.path.join(self.root_path, filename)  # 拼接路径
        try:
            with open(filename) as json_file:  # 读取文件
                obj = json.loads(json_file.read())  # 对文件内容字符串反序列化成字典
        except IOError as e:
            if silent and e.errno in (errno.ENOENT, errno.EISDIR):
                return False
            e.strerror = "Unable to load configuration file (%s)" % e.strerror
            raise
        return self.from_mapping(obj)  # 调用上面介绍过的from_mapping方法

如果你理解了上面from_mapping()方法,那么,对于这个from_json()方法也很好理解了,因为from_json()方法只是读取json文件成字符串后反序列化成字段传入from_mapping()。

在使用from_json()方法之前,我们得先创建一个json文件来写入配置,假设文件名为settings.json,内容如下:

{
  "DEBUG": true,
  "TESTING": false,
  "A": 123
}

使用方法:

from flask import Flask
 
app = Flask(__name__)
 
app.config.from_json('settings.json') #传入json文件
print('DEBUG:', app.config.get('DEBUG'))
print('TESTING:', app.config.get('TESTING'))
print('A:', app.config.get('A'))

输出:

DEBUG: True

TESTING: False

A: 123

8 配置方式6-系统环境变量:from_envvar

from_envvar()是从系统环境变量中读取配置,源码如下:

def from_envvar(self, variable_name, silent=False):
        rv = os.environ.get(variable_name)  # 读取指定的系统环境变量
        if not rv:  # 如果系统环境中并没有配置这一变量
            if silent:
                return False
            raise RuntimeError(
                "The environment variable %r is not set "
                "and as such configuration could not be "
                "loaded.  Set this variable and make it "
                "point to a configuration file" % variable_name
            )
        return self.from_pyfile(rv, silent=silent)  # 调用from_pyfile方法

这个方法的源码应该是上面介绍过的这么多方法中最好理解的了。从源码中可以看出,这个方法的功能就是根据传入的variable_name,去系统环境中读取变量名为variable_name的环境变量,而这个变量的值必须是一个py文件的完整路径,因为在最后是调用from_pyfile()方法出导入配置的,我相信,只要你会使用from_pyfile()方法,就会使用这个方法,毕竟搞IT的,配置个环境变量应该都会。

9 总结

本文结合对Flask源码的分析总结分析了Flask配置管理的使用方法。Flask通过Config配置类中的6个方法,对应得提供了6种配管管理方式。本文通过代码实例演示每种方式的使用方法,还深度剖析了源码,总结思路,相信你不进可以知其然还可以知其所以然。

相关推荐