從原始碼看Flask框架配置管理

奧辰發表於2019-08-13

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種配管管理方式。本文通過程式碼例項演示每種方式的使用方法,還深度剖析了原始碼,總結思路,相信你不進可以知其然還可以知其所以然。

 
 

相關文章