[源码分析] Flask 配置管理与描述符析

avatar
Chaojie

在 Flask 中可以通过 app.config[‘NAME’] = what 的形式指定一些配置,比如设置 debug = True

app.debug = True
# 或者
app.config['DEBUG'] = True

有些配置比如设置 ENV 和 TESTING 还可以直接利用 Flask 对象来设置,像这样:

app.testing = True

除了在程序中指定配置,也可以将配置写在单独的文件中,比如:

app = Flask(__name__)
app.config.from_object('yourapplication.default_settings')
app.config.from_envvar('YOURAPPLICATION_SETTINGS')

应用首先从 yourapplication.default_settings 模块载入配置,然后根据 YOURAPPLICATION_SETTINGS 环境变量所指向的文件的内容重载配置的值。

除了从配置文件加载,也可以定义类类指定配置,具体用法去看看官方文档就知道了。

知道了在 Flask 中如何使用配置,我们来看看它是如何实现的。

首先 config 肯定是个变量,在 Flask 这个类中被定义为:

self.config = self.make_config(instance_relative_config)

然后 make_config 的代码是这样的:

def make_config(self, instance_relative=False):

    root_path = self.root_path
    if instance_relative:
        root_path = self.instance_path
    # 默认配置
    defaults = dict(self.default_config)
    defaults['ENV'] = get_env()
    defaults['DEBUG'] = get_debug_flag()
    return self.config_class(root_path, defaults)

make_config 方法获取 flask 中默认的配置,以及 ENV 和 DEBUG 这两个配置,之后返回了 self.config_class 对象,它在类中是这样定义的:

config_class = Config

class Config(dict):
    def __init__(self, root_path, defaults=None):
        # dict.__init__让 Config 实例拥有字典行为 config['ENV']
        dict.__init__(self, defaults or {})
        self.root_path = root_path

config_class 本质上是 Config 类,注意看 Config 类的初始化方法,

dict.__init__(self, defaults or {})

这一行代码使得 Config 类可以像字典一样使用,比如 app.config['TESTING']=True。当然你也可以使用 __getitem____setitem__ 内置方法使得类具有字典的行为。

那么像 app.testing = True 这样的配置是如何实现的?

在 Flask 类中可以看到,这些类变量都是 ConfigAttribute 对象。

testing = ConfigAttribute('TESTING')
secret_key = ConfigAttribute('SECRET_KEY')

ConfigAttribute类如下:

class ConfigAttribute(object):
    def __init__(self, name, get_converter=None):
        self.__name__ = name
        self.get_converter = get_converter

    # obj 是被托管类实例
    def __get__(self, obj, type=None):
        # 如果被托管实例不存在,返回描述符自身
        if obj is None:
            return self
        # 返回 Flask 实例的 config[name]
        rv = obj.config[self.__name__]
        if self.get_converter is not None:
            rv = self.get_converter(rv)
        return rv

    def __set__(self, obj, value):
        obj.config[self.__name__] = value

ConfigAttribute 是一个描述符类,描述符是什么?

描述符是对多个属性运用相同存取逻辑的一种方式。——《流畅的 python》

描述符实现了特定的内置方法,__get____set____delete__ ,常见的比如 Django 中 ORM 中的实现就是用的描述符:

class Person(models.Model):
    # models.CharField 就是一个描述符
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

还有 python 内置的 @property @classmethod staticmethod 装饰器就是用描述符实现的。

说了这么多,来看看描述符到底怎么用。

首先来看ConfigAttribute类中的 __get__ 方法:

# obj 是被托管类实例
def __get__(self, obj, type=None):
    # 如果被托管实例不存在,返回描述符自身
    if obj is None:
        return self
    # 返回 Flask 实例的 config[name]
    rv = obj.config[self.__name__]
    if self.get_converter is not None:
        rv = self.get_converter(rv)
    return rv

__get__ 方法的参数 obj 是被托管类的实例,在这里就是 Flask 类,方法中首先判断被托管类是否存在,不存在就返回描述符本身。之后返回 Flask 类实例中的 config[name],看到没有 env 类变量实际上就是 config[name] 中指定的值。

来看 __set__ 方法:

def __set__(self, obj, value):
    obj.config[self.__name__] = value

__set__方法中的 obj 同样是被托管类的实例,然后 value 被存储在被托管类的 config 变量中。所以,我们才可以用 app.testing = True 来指定配置。

说到底,描述符有什么用?我们来看,在 Flask 类中我们要指定类变量 testing, env, secret_key, session_cookie_name 等等,都需要从 config 变量中接收(保证配置的一致性)。我们为这些变量都写一个存取方法是不是很麻烦,使用描述符就可以简化流程,对外封装了具体的存取细节,并且减少代码量。

当然我们也可以使用一个函数,通过构建特性工厂的方式来实现,比如:

def config_attribute(name):
    def getter(instance):
        return instance.config[name]

    def setter(instance, value):
        instance.config[name] = value

    # property 实际上就是@property 装饰器
    return property(getter, setter)

# 在 Flask 中调用方式一样
testing = config_attribute('TESTING')

关于描述符的更多细节可以查看 《流畅的 python》 这本书中第 20 章的内容,有详细的介绍。

© CC BY-NC-SA 4.0 | Chaojie