本文最后更新于:2023年4月24日 下午
深入 Flask 配置 在 Flask 中,提供了丰富的全局配置来方便开发,以下是开发过程当中几个常用的配置选项。
配置名
作用
DEBUG
启用 / 禁用调试模式
SECRET_KEY
密钥
SERVER_NAME
服务器名和端口。需要这个选项来支持子域名 (例如: 'myapp.dev:5000'
)。注意 localhost 不支持子域名,所以把这个选项设置为 “localhost” 没有意义。设置 SERVER_NAME
默认会允许在没有请求上下文而仅有应用上下文时生成 URL
SESSION_COOKIE_NAME
会话 cookie 的名称。
更多的配置详见官方文档 。
如果要在 Flask
中激活某些的配置,通常有以下 8 种方式,前面的两种都是针对某个单独配置。第三种方式,就是采用字典的更新键值对的方法,因为 Config
类本身就是继承自字典,所以同时也继承了字典的 update
方法。 而后面的几种方式,则可以对多个配置项进行处理。
1 2 3 4 5 6 7 8 app.debug = True app.config["debug" ] = True app.config.update() app.config.from_envvar() app.config.from_json() app.config.from_mapping() app.config.from_pyfile() app.config.from_object()
下面谈谈其他几种方法的内部操作以及原理。
一、Config
配置类的创建过程 首先 Config
类是在 flask/config.py
文件里面。
1 2 3 4 5 class Config (dict ): def __init__ (self, root_path, defaults=None ): dict .__init__(self, defaults or {}) self.root_path = root_path
下面是 flask/app.py
里面的 Flask
类,由于这个类的代码数量庞大,所以只贴出一点用到了 Config
类的地方。
可以看到 default_config
是一个 ImmutableDict
(不可变字典对象) ,里面是所有支持的配置项,并且都给出了默认值。
而 Config
类会被赋值给 Flask
的成员对象 config_class
,但此时这个成员对象,也就是字典对象,还没有任何数据;所以要通过 Flask
的 make_config
来为 config_class
赋值字典数据,而此时传入的配置就是 defaults
,包含了 Flask
全部配置项。 初始时的两个配置项 ENV
和 DEBUG
会通过 get_env()
方法 和 get_debug()
方法设置为 production
和 False
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Flask (_PackageBoundObject ): config_class = Config default_config = ImmutableDict( { "ENV" : None , "DEBUG" : None , "TESTING" : False , "PROPAGATE_EXCEPTIONS" : None , "PRESERVE_CONTEXT_ON_EXCEPTION" : None , "SECRET_KEY" : None , "PERMANENT_SESSION_LIFETIME" : timedelta(days=31 ), "USE_X_SENDFILE" : False , "SERVER_NAME" : None , "APPLICATION_ROOT" : "/" , "SESSION_COOKIE_NAME" : "session" , "SESSION_COOKIE_DOMAIN" : None , "SESSION_COOKIE_PATH" : None , "SESSION_COOKIE_HTTPONLY" : True , "SESSION_COOKIE_SECURE" : False , "SESSION_COOKIE_SAMESITE" : None , "SESSION_REFRESH_EACH_REQUEST" : True , "MAX_CONTENT_LENGTH" : None , "SEND_FILE_MAX_AGE_DEFAULT" : timedelta(hours=12 ), "TRAP_BAD_REQUEST_ERRORS" : None , "TRAP_HTTP_EXCEPTIONS" : False , "EXPLAIN_TEMPLATE_LOADING" : False , "PREFERRED_URL_SCHEME" : "http" , "JSON_AS_ASCII" : True , "JSON_SORT_KEYS" : True , "JSONIFY_PRETTYPRINT_REGULAR" : False , "JSONIFY_MIMETYPE" : "application/json" , "TEMPLATES_AUTO_RELOAD" : None , "MAX_COOKIE_SIZE" : 4093 , } ) 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)
二、从环境变量中读取配置属性 当在环境变量中设置了配置文件的环境变量,那么则可以使用这个方法。
首先是使用 os
模块的 environ.get()
方法来获取环境变量属性值,而后再调用另一个方法,这个环境变量的属性值是一个文件路径,通常的话,这个配置文件应该方法在和启动文件在同一个路径下。
1 2 3 4 5 6 7 8 9 10 11 12 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)
在项目的同目录下创建一个 config.cfg
配置文件,写入两个简单的配置项。
1 2 DEBUG =True SECRET_KEY ="something"
使用 os
模块临时设置一个环境变量,当从环境变量中读取到配置文件后,在网页中能打印到配置属性的值。
还有一种办法是 (针对 Linux
环境),新开一个终端,切换到项目的目录下,在启动项目前,先使用 export FLASK_CONFIG=config.cfg
,然后启动文件里面只需要写 app.config.from_envvar("FLASK_CONFIG")
就可以,当然使用 export
设置的也只是一个临时变量,只对目前的终端有效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import osfrom flask import Flask app = Flask(__name__) os.environ.setdefault("FLASK_CONFIG" , "config.cfg" ) app.config.from_envvar("FLASK_CONFIG" )@app.route("/" ) def index (): return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG" ), app.config.get("SECRET_KEY" ))if __name__ == "__main__" : app.run()
我比较在意的是这个 silent
参数, 这个参数的含义是,当配置文件丢失时,或者环境变量没有设置时,设置 silent
参数为 True,那么就等于没有配置这个文件。
那么可以写一个函数来检测这个配置文件是否存在,是否设置环境变量,当两个都没有时,返回 True
,然后 from_envvar
方法将会不起作用,触发异常。
下面是一个简单的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from flask import Flaskimport os app = Flask(__name__)def is_set (): if not os.environ.get("FLASK_CONFIG" ): return True elif not os.path.exists("config.cfg" ): return True else : return False app.config.from_envvar("FLASK_CONFIG" , silent=is_set())@app.route("/" ) def index return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG" ), app.config.get("SECRET_KEY" ))if __name__ == "__main__" : app.run()
针对 silent
参数,写出一个方法来最终决定 silent
的值,这样防止了中间环境变量配置出错以及文件不存在等等情况。
三、 从 python
文件中读取配置属性 当创建一个 Flask
的实例对象之后,使用 app.config.from_pyfile
方法,传入一个配置文件字符串,从配置文件中读取属性并且写入,前面的 from_envvar
方法获取到配置文件后最终也会调用这个方法,并且,这个方法最终也会调用下一个方法 from_object
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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: 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) return True
在项目的根目录下创建一个名为 config.cfg
简单配置文件。
1 2 DEBUG =True SECRET_KEY ="something"
先看 from_pyfile
文件前面的三段代码, 首先使用 os
获取配置文件的绝对路径,然后用 types.ModuleType
方法动态创建了一个 config
模块,并且设置文件名为传进来的文件名的绝对路径文件名,此时这个 config
算是一个模块了,不是用普通的 import
方法导入的。
1 2 3 filename = os.path.join(self.root_path, filename) d = types.ModuleType("config" ) d.__file__ = filename
types
属于 Python
的标准库,里面的几个常用的方法没怎么了解,有如下几个。
FunctionType
:通过不使用 def
的方式动态创建一个函数。
MethodType
:将创建在类外的某个方法动态绑定到类的实例上。
ModuleType
:动态的创建一个临时的模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 >>> import sys>>> import types>>> m = types.ModuleType("sample" , "sample module." ) >>> m <module 'sample' >>>> m.__dict__ {'__name__' : 'sample' , '__doc__' : 'sample module.' , '__package__' : None , '__loader__' : None , '__spec__' : None }>>> m in sys.modules False >>> class Person :... pass ... >>> p = Person()>>> def say (self ): print ("hello" )>>> p.say = types.MethodType(say, p)>>> p.say() hello >> foo_code = compile ('def foo(): return "bar"' , "<string>" , "exec" ) >> foo_func = types.FunctionType(foo_code.co_consts[0 ], globals (), "foo" ) >> print (foo_func()) bar
继续看 from_pyfile
方法剩下的代码,读取配置文件的配置属性,此时把属性放进动态创建的模块的字典里头,最后是调用另一个方法。
1 2 3 4 5 6 7 8 9 10 try : with open (filename, mode="rb" ) as config_file: 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)return True
四、从 python
对象中提取属性 从 python
对象中提取配置相对简单,一般写一个配置类的 python
文件,里面定义一个基类,设定一些基本配置,然后使用类继承的方法为各种环境设置扩展配置类,一个简单的配置类如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 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
之后就是在 from_object
方法里传入一个类名,或者一个完整的模块字符串就可以配置好配置属性。如果传入的是一个配置类,那么 if isinstance(obj, string_types)
直接为 False
,然后这个传入的配置类的所有属性,如果包含大写的属性,将存入 app.config
的字典中。如果是字符串,那么会先把模块里面的类导入再提取属性。
1 2 3 4 5 6 def from_object (self, obj ): if isinstance (obj, string_types): obj = import_string(obj) for key in dir (obj): if key.isupper(): self[key] = getattr (obj, key)
string_types
,这个变量在 flask
包的 _compat.py
下,其实就是string
类型,所以简单对传进来的参数检查是不是个字符串,如果是字符串,则会调用 import_string
方法,如果不是字符串而是一个具体的类则直接执行 for
循环对类的属性遍历。
1 2 3 4 5 6 7 8 try : text_type = unicode string_types = (str , unicode) integer_types = (int , long)except NameError: text_type = str string_types = (str ,) integer_types = (int ,)
然后是 import_string
方法,因为传进来的仅仅是个字符串,还没对模块进行导入,所以会用到 werkzeug.utils
包下的 import_string
方法对模块进行导入。
可以看到 import_string
的第一行代码会对字符串进行替换,那说明有两种写法,而恰恰函数文档也说明了。
1 2 3 4 5 6 7 8 """ 提供两种模块写法:例如 xml.sax.saxutils.escape xml.sax.saxutils:escape 无论是那种写法,最后都会变成下面这种写法 xml.sax.saxutils.escape """ import_name = str (import_name).replace(":" , "." )
接下来就是使用不寻常的导包方式,一般导包都是两种方式,import package
或者是 from package import module
,因为我们这里传进来的是字符串,所以不能用正常的导包方式,只能使用 __import__
这个内建方法,实际上 import
也是调用 __import__
。假设配置类 BaseConfig
在 Config
包下的 Settings.py
模块下,那么可以写为 Config.Settings:BaseConfig
,或者 Config.Settings.BaseConfig
,两者的可以。最终,如果导入为空时,那么会从 sys.modules
里面查询这个包。
1 2 3 4 5 6 7 try : __import__ (import_name)except ImportError: if "." not in import_name: raise else : return sys.modules[import_name]
如果这个传入的配置类字符串为 Config.Settings.BaseConfig
,那么先把模块名和对象名分开,再尝试使用 __import__
方法导入,此时已经把模块名和对象名分开了,分别把模块名和对象名传入 __import__
就可以正常导入。再使用 getattr
方法获取模块里面的对象,这个方法告一段落。最后就是把返回的对象遍历获取里面的配置属性添加到 Config
对象中。
__import_
的四个参数:
name (required)
: 被加载 module
的名称
globals (optional)
: 包含全局变量的字典,该选项很少使用,采用默认值 global()
locals (optional)
: 包含局部变量的字典,内部标准实现未用到该变量,采用默认值 local()
fromlist (Optional)
: 被导入的子模块名称
1 2 3 4 5 6 module_name, obj_name = import_name.rsplit("." , 1 ) module = __import__ (module_name, globals (), locals (), [obj_name])try : return getattr (module, obj_name)except AttributeError as e: raise ImportError(e)
总的来说这个方法提供了两种导入配置的选择,一种是传入模块字符串,一种是直接传入配置类。便于导入的时候选择导入方式和可扩展性。
五、从 json
文件中读取配置属性到映射为字典 这个方法最终也会调用 Config
类的最后一个方法,对于这个方法而言,只是简单的读取一下 json
文件,并且把 json
文件里面的数据转化为 Python
当中的字典类型。同时可以设置 silent
为 True
,当文件读取失败的时候,方法直接失效,如果不设置为 True
的话,也可以,直接触发标准错误。
1 2 3 4 5 6 7 8 9 10 11 12 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)
在项目的根目录下创建一个名为 config.json
的配置文件。
1 2 3 4 { "DEBUG" : "True" , "SECRET_KEY" : "something" }
示例代码,当程序启动时,打开 http://127.0.0.1:5000/
就可以看到 DEBUG
和 SECRET_KEY
的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 from flask import Flask app = Flask(__name__) app.config.from_json("config.json" )@app.route("/" ) def index (): return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG" ), app.config.get("SECRET_KEY" ))if __name__ == "__main__" : app.run() print (app.config.items())
六、从 python
键值对 ( dict )
中配置属性 设置一个简单的字典对象,待会传入 from_mappings
方法。
1 2 3 4 configs = { "DEBUG" : True , "SECRET_KEY" : "Something" }
最后一个方法,针对传进来的键值对,也就是字典,这里对应第三个参数 **kwargs
,使用 kwargs.items()
提取出所有的键值对 (列表格式) 存放进 mappings
列表里,然后再通过二层循环提取出每个配置的键和值,存放进 Config
类的里面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def from_mapping (self, *mapping, **kwargs ): mappings = [] if len (mapping) == 1 : if hasattr (mapping[0 ], "items" ): mappings.append(mapping[0 ].items()) else : mappings.append(mapping[0 ]) elif len (mapping) > 1 : raise TypeError( "expected at most 1 positional argument, got %d" % len (mapping) ) mappings.append(kwargs.items()) for mapping in mappings: for (key, value) in mapping: if key.isupper(): self[key] = value return True
例子示范,当启动程序时,在浏览器进入 http://127.0.0.1:5000/
就能看到 DEBUG
和 SECRET_KEY
的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from flask import Flask app = Flask(__name__) configs = { "DEBUG" : True , "SECRET_KEY" : "Something" } app.config.from_mappings(configs)@app.route("/" ) def index (): return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG" ), app.config.get("SECRET_KEY" ))if __name__ == "__main__" : app.run() print (app.config.items())
然后这个方法还提供另一种细化的使用,上面只是传入了第三个参数,第二个参数还没使用,显然这个函数是会使用到第二参数,那么这个参数格式有几种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 mapping = ( { "DEBUG" : True , "SECRET_KEY" : "Something" } ) mapping = ( ('DEBUG' , True ), ('TESTING' , False ) )
此时就不需要传入第三个参数,也就是说这个方法提供两种参数传入方式,也方便扩展,如果单纯使用键值对,那么前面的代码将不会被执行,如果使用 tuple
,也会对这个参数进行操作,提取里面的属性值。
七、自定义读取 yaml/properties 配置文件 上面提到可以用 types.MethodType
来创建动态方法,这里就可以利用这个来为 config
扩展读取更多类型的配置文件。
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import typesclass Person : pass def say (self, name ): self.name = name print ("hello" , name) p = Person() p.say = say p.say(name="nick" ) p.say = types.MethodType(say, p) p.say(name="nick" )
创建一个名为 config.yaml
的配置文件,写入两个简单的配置项。
1 2 DEBUG: True SECRET_KEY: something
读取 yaml
文件,编写读取 yaml
文件方法,利用了 pyyaml
库,使用 yaml
读取出来的数据是字典格式,然后传递给 Config
对象的 from_mapping
方法,然后利用 types.MethodType
方法为 Config
类动态添加方法,绑定在 config
对象上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from flask import Flaskimport typesimport osimport yaml app = Flask(__name__)def from_yaml (self, filename, silent=False ): filename = os.path.join(self.root_path, filename) try : with open (filename) as yaml_file: obj = yaml.load(yaml_file.read(), Loader=yaml.FullLoader) except IOError as e: if silent: return False return self.from_mapping(obj) app.config.from_yaml = types.MethodType(from_yaml, app.config) app.config.from_yaml("config.yaml" )@app.route("/" ) def index (): return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG" ), app.config.get("SECRET_KEY" ))if __name__ == "__main__" : app.run() print (app.config.items())
创建一个名为 config.properties
的简单配置文件,写入以下简单配置项。
1 2 DEBUG =True SECRET_KEY =something
创建一个读取 from_properties
方法, 这个方法遍历 properties
文件的每一行,把 =
两边的属性名和属性值放进 obj
中,最终会调用现有的 from_mapping
方法,最后还是要利用 types.MethodType
方法为 Config
类动态添加方法,绑定在 config
对象上,这样这个方法才会起作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 app = Flask(__name__)def from_properties (self, filename, silent=False , encode=None ): filename = os.path.join(self.root_path, filename) try : with open (filename) as properties_file: obj = {} for line in properties_file: if line.find('=' ) > 0 : s = line.replace('\n' , '' ).split("=" ) obj[s[0 ]] = s[1 ] except IOError as e: if silent: return False return self.from_mapping(obj) app.config.from_properties = types.MethodType(from_properties, app.config) app.config.from_properties("config.properties" )
当然,这两个方法可能也有不完善的地方,例如,yaml
文件可能是多层级的,这里只考虑到一层级,什么时候下才会出现多层级的配置项,例如,可以在一个 yaml
文件里面设置多个环境配置,开发环境配置,生产环境配置,部署环境配置等。