毫無疑問, 你需要設定檔。據說設定檔能讓你的系統變得可定製。
讓系統變得可定製, 有三大思潮, 這決定了三種設定檔的格式:
1. 圖形介面組態工具。
微機和 Windows 開始流行之後, 我們的使用者再也不是駭客了,
或許你的使用者永遠不會開啟你的設定檔, 你需要給他一個圖形工具, 並且幫他換尿布。
在人類永遠不會閱讀設定檔的情況下, 讓系統變得更簡潔的方法就是使用
機器最容易理解的格式來儲存配置, 通常這是一個二進位檔案。
Pickle 是 Python 中的典型選項。
幾十年後, 有人開始以為使用機器和人都能理解 (或者都不能理解) 的格式是一個好主意, 這樣就有了 XML。
在這裡, 我只想告訴大家:
XML 確實是好東西, 但是在絕大多數情況下你其實並不真的需要他。
而在 Zope/Plone 中, 配置被大規模地儲存在 ZODB 資料庫中。
這樣就產生了第一種設定檔類型, 他是人類不可讀的。
2. 常規格式的設定檔, 有著名的 httpd.conf 和 INI 等。
基本上, INI 很弱, 而且 INI 的支援庫並沒有提供諸如值驗證甚至設定變數預設值的功能,
因此, 除非項目非常小你不應該使用 INI 格式。
而在任何時候都不使用 INI 格式, 有助於你養成良好的習慣。
httpd.conf Like 的設定檔, 是設定檔格式的巔峰之作。
在 Python 中 ZConfig (位於 Zope2/Zope3 中) 提供了對於此種格式的支援。
我這裡有一份入門簡介:
http://eishn.blog.163.com/blog/static/65231820069310828642/
它只是入門介紹, 你需要閱讀文檔、源碼或者範例才能完全瞭解 ZConfig 的工作方式。
儘管並不容易, 但是理解 ZConfig 會協助你快速掌握設定檔的設計哲學。
常規的設定檔設計和解析過於複雜, 因此你可以僅作瞭解。
3. 使用 Python 本身來作為設定檔。
Why not? 目前有許多 C 程式正在使用 Python 作為設定檔,
而 Lighttpd 的設定檔也已經八九不離十了。
這裡是一個例子:
# demo_conf.py
# 設定檔
host = '127.0.0.1'
port = 8080
# demo.py
import demo_conf
print demo_conf.host, demo_conf.port
利用 Python 指令碼作為設定檔, 在程式實現上簡單, 無須格式轉換, 而且功能強大。
這個方案已經開始向傳統的設定檔格式宣戰。
最後我修改了原定準備介紹 ZConfig 的計劃, 今天著重來介紹這個方案。
下面, 就讓我們來設計自己的設定檔格式。
要搞清楚如何設計一個設定檔, 我們首先需要搞清楚系統中有哪些資料類型是需要配置的,
並且弄清楚他們在多大程度上是可以配置的。
根據經驗, 一個系統中可以定製的資料有三種:
1. "靜態" 資料, 在 C 語言中, 是用 #define 定義的。
此類資料僅在開發期具有動態性, 一旦系統完成開發, 此類資料就是靜態。
此類資料的變化頻率最低。
Python 程式員通常將變數名大寫, 並且放在 "config.py" 中,
通過 import config 匯入使用。
2. "預設" 配置, 這是項目配置部分最大的亂源。
預設配置是一種使用者可以在設定檔中修改的資料, 他們不是必須的, 他們有預設值。
你很快就會發現許多 "預設" 配置實際上使用者永遠不會去修改他,
在這種情況下, 這些預設配置實際上是上面所說的 "靜態" 資料, 應該寫入 "config.py"。
而另一些則是常用配置, 只是帶有預設值而已, 這些資料應該寫入 "etc" 設定檔中。
顯而易見, 一個變數不能同時存在於 config.py 和 etc 設定檔中。
問題在於搞清楚一個變數是屬於 "靜態" 資料還是 "預設" 配置是困難和不確定的。
這最終導致你的設定檔定義朝令夕改, 而你的項目則風雨飄搖。
3. 配置項, 毫無疑義地應該放在設定檔中。在系統投入使用後, 這是變化頻率最高的資料類型。
在設定檔中, 你又將面臨另一大挑戰, 就是變數類型驗證。
變數類型驗證之所以討厭, 就是因為你要同時判斷一個組態變數是否存在、類型是否匹配或者是否越界,
並且是否給以預設值。最後最糟糕的是你還需要回答一個問題:
預設值是否需要寫在設定檔中。
為瞭解決所有這些問題, 人們發明了 Schema。
和傳統設定檔不同, 使用 Python 作為設定檔, Schema 的編寫具有相當大的靈活性。
而且 Schema 能協助你把 Python 格式的設定檔變得更加可讀。
下面我就示範一下在 Python 作為設定檔格式時 Schema 的寫法。
# /PATH/TO/ETC/demo_conf.py
# 設定檔
import schema
Server(
host = '127.0.0.1',
port = 8080
)
# -EOF-
# /PATH/TO/ETC/schema.py
#
config = {}
def Server(**args):
config['host'] = args.get('host', '0.0.0.0') # 有預設值
try:
config['port'] = int(args.get('port', 8080)) # 帶驗證
except ValueError:
raise ValueError, '你必須是整型'
# -EOF-
# /PATH/TO/BIN/demo.py
#
import sys
sys.path.append(r'/PATH/TO/ETC')
import schema, demo_conf
print schema.config['host'], schema.config['port']
# -EOF-
好了, 接下來我們需要把 demo_conf.py 變得更像一個設定檔,
因為 "import schema" 不像是一個設定檔中應該有的東西, 我們要讓 "Server" 成為預匯入的變數。
最後, 我們還要把 demo_conf.py 變成 demo.conf。
這樣繼 PyQt4 一招鮮之後, 我們又要再次接觸到自訂匯入技術。
這次我們將使用類似的技術, 也是一個匯入鉤子 (hook), 不同的是, 這次我們不將鉤子掛接到 Python 解譯器。
################################################################## # -BOF-
# pyetc.py
# Python 格式的設定檔支援庫
#
import sys, os.path
Module = type(sys) # 故技重演
modules = {} # 緩衝已經匯入的 etc (配置) 模組
# 匯入任意符合 Python 文法的檔案
# 用法:
# module = pyetc.load(完整檔案路徑並包含副檔名, 預載入變數, 自訂返回模組類型)
#
def load(fullpath, env={}, module=Module):
try:
code = open(fullpath).read()
except IOError:
raise ImportError, 'No module named %s' %fullpath
filename = os.path.basename(fullpath)
try:
return modules[filename]
except KeyError:
pass
m = module(filename)
m.__module_class__ = module
m.__file__ = fullpath
m.__dict__.update(env)
exec compile(code, filename, 'exec') in m.__dict__
modules[filename] = m
return m
# 移除已經匯入的模組
# 用法:
# module = unload(module)
#
def unload(m):
filename = os.path.basename(m.__file__)
del modules[filename]
return None
# 重新匯入模組
# 用法:
# module = pyetc.reload(module)
#
def reload(m):
fullpath = m.__file__
try:
code = open(fullpath).read()
except IOError:
raise ImportError, 'No module named %s' %fullpath
env = m.__dict__
module_class = m.__module_class__
filename = os.path.basename(fullpath)
m = module_class(filename)
m.__file__ = fullpath
m.__dict__.update(env)
m.__module_class__ = module_class
exec compile(code, filename, 'exec') in m.__dict__
modules[filename] = m
return m
################################################################## # -EOF-
下面就讓我們來嘗試一下。
# /PATH/TO/ETC/demo.conf
# 設定檔
host = '127.0.0.1'
port = 8080
# -EOF-
# demo.py
import pyetc
conf = pyetc.load(r'/PATH/TO/ETC/demo.conf')
print conf.host, conf.port
# -EOF-
最後是一個較為完整的例子:
# /PATH/TO/ETC/demo.conf
# 伺服器選項
#
Server(
port = 8080 # 監聽 8080 連接埠
)
# 進程式控制制器選項
#
Daemon(
# 使用 Socket 發布進程式控制制器
# address = ('0.0.0.0', 10080),
# 使用檔案發布進程式控制制器
address = var('demo.pid'),
# 伺服器處理序
program = bin('server.py'),
# 調試開關
verbose = True
)
# -EOF-
# /PATH/TO/BIN/schema.py
# 路徑工具
import sys, os.path
DEMO_HOME = r'/PATH/TO/DEMO'
ETC = lambda filename: os.path.join(DEMO_HOME, 'etc', filename)
VAR = lambda filename: os.path.join(DEMO_HOME, 'var', filename)
BIN = lambda filename: os.path.join(DEMO_HOME, 'bin', filename)
#
class Config(dict):
# 可以像屬性一樣訪問字典的 Key
# dict.key 等同於 dict[key]
def __getattr__(self, name):
return self[name]
# 配置預設值
config = Config({
'server': Config({
'port': 8080 # 伺服器使用 8080 連接埠
}),
'daemon': Config({
'address': VAR('daemon.pid'), # pid 檔案
'program': BIN('server.py' ), # 伺服器程式
'verbose': True
})
})
# 配置介面 (不帶驗證)
def Server(**args):
config['server'].update(args)
def Daemon(**args):
config['daemon'].update(args)
# 設定檔 "demo.conf" 可見的變數
env = {'Server': Server, 'Daemon': Daemon,
'etc': ETC, 'var': VAR, 'bin': BIN}
# -EOF-
# /PATH/TO/BIN/demo.py
# 這裡啟動一個 Daemon 管理器,
# 注意: 這裡的 daemon.py 是一個假想庫, 無須理會
import pyetc
from daemon import Daemon
def start():
# 讀取設定檔
# demo.conf
pyetc.load(schema.ETC('demo.conf'), env=schema.env)
conf = schema.config.daemon
# 建立 Daemon 對象
daemon = Daemon(
address = conf.address, # 進程式控制制器地址/pid 檔案位置
program = conf.program, # 後台進程程式位置
verbose = conf.verbose # 調試
)
print '進程管理器已經啟動'
daemon()
if __name__ == '__main__':
start()
# -EOF-
from: http://eishn.blog.163.com/blog/static/6523182007236721876/