Pythonは強力な構成ライブラリを実現
22470 ワード
アプリケーションは常にプロファイルを読み込み、入力が有効で、構成がない場合にデフォルト値を使用することを検証するため、このプロセスをどのように簡略化したいのでしょうか.
ファイル形式
まず,ユーザが直接書き換えるのを容易にするために,ファイルフォーマットは必ず読み取り可能である.Python標準ライブラリで読み書き可能な構成のライブラリにはconfigparser(iniファイル)、json、xmlがあり、ini、xmlファイルはタイプを問わず、文字列が読み取れるのでjsonを選択しました.ただしJSONではコメントがサポートされていないという欠点があります.この欠点を無視して、良いドキュメントを提供すればいいのです
データ検証、デフォルト
どんな形式のデータを検証し、必要なフォーマットに変換できるPythonライブラリを見つけました.
これは
まず、個
オプションのキーを宣言します
コンフィグクラスの実装
trafaret変換後に得られたのは
様々なORMフレームワークにヒントを得て、クラス属性に構成のフィールドを宣言するつもりです.書き方は以下の通りです.
OptionalKey
これは実はいくつかのデフォルトのパラメータのKeyクラスを指定して、このように私達はパラメータの中でただデフォルトの値を書くだけで、変換した名前はクラスの属性の名前によって指定します
宣言されたフィールドをDictに変換
Pythonのメタクラスで簡単に実現できることをクラス作成時に宣言したフィールドをスキャンし、
Configクラスのコンストラクタ
コンストラクション関数は、
パラメータは
Configをdictに戻す
保存時には
フルソース
その後、読み取りと保存の構成を加えると完了します.以下は完全なソースコードです.
使用方法
ファイル形式
まず,ユーザが直接書き換えるのを容易にするために,ファイルフォーマットは必ず読み取り可能である.Python標準ライブラリで読み書き可能な構成のライブラリにはconfigparser(iniファイル)、json、xmlがあり、ini、xmlファイルはタイプを問わず、文字列が読み取れるのでjsonを選択しました.ただしJSONではコメントがサポートされていないという欠点があります.この欠点を無視して、良いドキュメントを提供すればいいのです
データ検証、デフォルト
どんな形式のデータを検証し、必要なフォーマットに変換できるPythonライブラリを見つけました.
これは
dict
をdatetime
に変換した例です.import datetime
import trafaret as t
date = t.Dict({
'year': t.Int,
'month': t.Int,
'day': t.Int
}) >> (lambda d: datetime.datetime(**d))
assert date.check({'year': 2012, 'month': 1, 'day': 12}) == datetime.datetime(2012, 1, 12)
まず、個
Dict
構造を宣言し、keyと値のタイプを指定する>>
オペレータはカスタムの変換関数を指定することができ、変換関数にはDataError
エラーを投げ出すことができるdate.check()
検証して変換したオブジェクトに戻ることができるDict
のkeyは文字列以外にも指定できるKey
タイプ、Key
デフォルト値を指定し、元の名前を別の名前に変換できる:>>> c = t.Dict({t.Key('un', 'default_user_name', True) >> 'user_name': t.String})
>>> c.check({'un': 'Adam'})
{'user_name': 'Adam'}
>>> c.check({})
{'user_name': 'default_user_name'}
オプションのキーを宣言します
'un'
、デフォルト値は'default_user_name'
、>>
オペレータ指定読み出し後名前変換'user_name'
、パラメータに変換の名前を指定することもできますコンフィグクラスの実装
trafaret変換後に得られたのは
dict
、dict
文字列タイプのkeyを使用しています.これはエディタに友好的ではなく、コードを書くときに自動的に補完されていません.また、名前を変更したいときにも自動的に一括変更できないので、アクセス属性でアクセス構成にアクセスするつもりです(.
オペレータでアクセス)様々なORMフレームワークにヒントを得て、クラス属性に構成のフィールドを宣言するつもりです.書き方は以下の通りです.
class NestedConfig(config):
pass
class MyConfig(Config):
bool_field = {OptionalKey(True): t.Bool}
int_field = {OptionalKey(123): t.Int}
nested_config = {OptionalKey({}): NestedConfig}
OptionalKey
これは実はいくつかのデフォルトのパラメータのKeyクラスを指定して、このように私達はパラメータの中でただデフォルトの値を書くだけで、変換した名前はクラスの属性の名前によって指定します
class OptionalKey(t.Key):
def __init__(self, default, name=None):
super().__init__(name, default, True)
宣言されたフィールドをDictに変換
Pythonのメタクラスで簡単に実現できることをクラス作成時に宣言したフィールドをスキャンし、
Dict
__struct__
クラス属性に変換して保存します.ここではConfig
の継承も実現しており、作成__struct__
の際にベースクラスが既に作成されている__struct__
を統合すればよいので、ベースクラスはサブクラスのフィールドを上書きできないことに注意class ConfigMeta(type):
""" Config __struct__
"""
def __new__(mcs, name, bases: Tuple[type, ...], namespace: Dict[str, Any]):
#
fields = set()
cls_struct = {}
for key, value in list(namespace.items()):
if type(value) is dict and len(value) == 1:
field_key, field_checker = list(value.items())[0]
if isinstance(field_key, t.Key):
del namespace[key]
field_key: t.Key
# name
if field_key.name is None:
field_key.name = key
# to_name, to_name None
if field_key.to_name is None:
field_key.to_name = key
assert field_key.to_name == key, f'{field_key.to_name} != {key} key.to_name '
fields.add(field_key.to_name)
cls_struct[field_key] = field_checker
#
base_keys: List[t.Key] = []
for base in bases:
if issubclass(base, Config):
for key in base.__struct__.keys:
# ,
if key.to_name not in fields:
base_keys.append(key)
fields.update(key.to_name)
cls = type.__new__(mcs, name, bases, namespace)
cls.__struct__ = t.Dict(cls_struct, *base_keys)
return cls
Configクラスのコンストラクタ
コンストラクション関数は、
dict
を1つ受け入れ、__struct__
変換してインスタンス属性に付与します.ここではついでに,入力キーワードパラメータ構造Config
をサポートした.また、check()
前に未知のキーを削除しないとエラーになるので注意してくださいパラメータは
dict
なので、ここではネストConfig
をサポートしています.原理はtrafaretのcheckerが元のデータ型を受け入れて変換したデータ型を返すことで、Pythonのクラスは工場関数として使えるのでConfig
クラスはcheckerとしてネストできますclass Config(metaclass=ConfigMeta):
# , trafaret
__struct__: t.Dict
def __init__(self, raw: dict=None, **kwargs):
# dict
if raw is None:
raw = {}
raw = dict(raw, **kwargs)
# key, check()
known_keys = {key.name for key in self.__struct__.keys}
for key in list(raw.keys()):
if key not in known_keys:
del raw[key]
raw = self.__struct__.check(raw)
for key, value in raw.items():
setattr(self, key, value)
Configをdictに戻す
保存時には
Config
変換dict
、JSONがサポートします.ネストがサポートされているためConfig
ここでは下位のConfig
にも変換dict
ここでは他のJSONでサポートされていないデータ型については処理していないのでConfig
にはJSONでサポートされているタイプしか含まれていません.そうでなければ保存できません.他のデータ型を使用する場合は@property
def to_dict(self):
res = {key.to_name: getattr(self, key.to_name) for key in self.__struct__.keys}
# dict list , Config dict
queue: List[Union[dict, list]] = [res]
while queue:
node = queue.pop(0)
for index, value in (node.items() if isinstance(node, dict)
else enumerate(node)):
if isinstance(value, Config):
node[index] = value.to_dict()
elif isinstance(value, (dict, list)):
queue.append(value)
return res
フルソース
その後、読み取りと保存の構成を加えると完了します.以下は完全なソースコードです.
# -*- coding: utf-8 -*-
"""
"""
import json
from typing import Union, List, Dict, Any, Tuple
import trafaret as t
class OptionalKey(t.Key):
def __init__(self, default, name=None):
super().__init__(name, default, True)
class ConfigMeta(type):
""" Config __struct__
"""
def __new__(mcs, name, bases: Tuple[type, ...], namespace: Dict[str, Any]):
#
fields = set()
cls_struct = {}
for key, value in list(namespace.items()):
if type(value) is dict and len(value) == 1:
field_key, field_checker = list(value.items())[0]
if isinstance(field_key, t.Key):
del namespace[key]
field_key: t.Key
# name
if field_key.name is None:
field_key.name = key
# to_name, to_name None
if field_key.to_name is None:
field_key.to_name = key
assert field_key.to_name == key, f'{field_key.to_name} != {key} key.to_name '
fields.add(field_key.to_name)
cls_struct[field_key] = field_checker
#
base_keys: List[t.Key] = []
for base in bases:
if issubclass(base, Config):
for key in base.__struct__.keys:
# ,
if key.to_name not in fields:
base_keys.append(key)
fields.update(key.to_name)
cls = type.__new__(mcs, name, bases, namespace)
cls.__struct__ = t.Dict(cls_struct, *base_keys)
return cls
class Config(metaclass=ConfigMeta):
""" , ,
JSON , property
, :
name = {OptionalKey(0): t.Int}
, :
nested_config = {OptionalKey({}): Config}
nested_config = {OptionalKey({}): t.Dict >> Config}
"""
# , trafaret
__struct__: t.Dict
def __init__(self, raw: dict=None, **kwargs):
# dict
if raw is None:
raw = {}
raw = dict(raw, **kwargs)
# key, check()
known_keys = {key.name for key in self.__struct__.keys}
for key in list(raw.keys()):
if key not in known_keys:
del raw[key]
raw = self.__struct__.check(raw)
for key, value in raw.items():
setattr(self, key, value)
def __repr__(self):
return repr(self.to_dict())
@classmethod
def from_file(cls, filename):
"""
"""
try:
with open(filename, encoding='utf-8') as f:
return cls(json.load(f))
except FileNotFoundError:
return cls()
@classmethod
def from_str(cls, s):
return cls(json.loads(s))
def to_dict(self):
res = {key.to_name: getattr(self, key.to_name) for key in self.__struct__.keys}
# dict list , Config dict
queue: List[Union[dict, list]] = [res]
while queue:
node = queue.pop(0)
for index, value in (node.items() if isinstance(node, dict)
else enumerate(node)):
if isinstance(value, Config):
node[index] = value.to_dict()
elif isinstance(value, (dict, list)):
queue.append(value)
return res
def save(self, filename):
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
使用方法
class BaseConfig(Config):
#
override_field = {OptionalKey(1): t.Int}
#
inherited_field = {OptionalKey(2): t.Int}
class NestedConfig(Config):
test = {OptionalKey(3): t.Int}
class MyConfig(BaseConfig):
#
override_field = {OptionalKey(True): t.Bool}
# Config
nested_config: NestedConfig = {OptionalKey({}): NestedConfig}
nested_config_list: List[NestedConfig] = {OptionalKey([{'test': 123}]):
t.List(NestedConfig)}
# [1, 5]
int_field = {OptionalKey(1): t.Int(1, 5)}
# {1, 2, 3}
int_field2 = {OptionalKey(1): t.Int >>
(lambda x: x if x in (1, 2, 3) else t.DataError('int_field2 {1, 2, 3} '))}
# Config
config = MyConfig({'int_field': 3, 'nested_config': {'test': 0}})
# {'inherited_field': 2, 'override_field': True,
# 'nested_config': {'test': 0}, 'nested_config_list': [{'test': 123}],
# 'int_field': 3, 'int_field2': 1}
print(repr(config))
#
config = MyConfig(int_field=4)
# {'inherited_field': 2, 'override_field': True,
# 'nested_config': {'test': 3}, 'nested_config_list': [{'test': 123}],
# 'int_field': 4, 'int_field2': 1}
print(repr(config))
#
print(config.int_field) # 4
print(config.nested_config.test) # 3
#
# trafaret.dataerror.DataError: {'override_field': DataError(value should be True or False)}
# config = MyConfig({'override_field': 1})
# trafaret.dataerror.DataError: {'int_field2': DataError(int_field2 {1, 2, 3} )}
# config = MyConfig({'int_field2': 4})