我正在尝试制作字典(从yaml数据中读取),其行为类似于类。因此,如果我要求class.key
我会收回他的价值。代码如下所示:
import errno
import sys
import yaml
backup_conf="""
loglevel: INFO
username: root
password: globalsecret
destdir: /dsk/bckdir/
avoidprojects:
matchregex: /bkp/
depots:
server1:
password: asecret
server2:
username: root
server3:
server4:
destdir: /disk2/bkp/
projects:
proj1:
matchregex:
- /backups/
- /bkp/
"""
class Struct:
def __init__(self, **entries):
self.__dict__.update(entries)
class Config:
def __init__(self, filename="backup.cfg", data=None):
self.cfg = {}
if data is None:
try:
fd = open(filename,'r')
try:
yamlcfg = yaml.safe_load(fd)
except yaml.YAMLError as e:
sys.exit(e.errno)
finally:
fd.close()
except ( IOError, OSError ) as e:
sys.exit(e.errno)
else:
try:
yamlcfg = yaml.safe_load(data)
except yaml.YAMLError as e:
sys.exit(e.errno)
self.cfg = Struct(**yamlcfg)
def __getattribute__(self, name):
try:
return object.__getattribute__(self, name)
except AttributeError:
return self.cfg.__getattribute__(name)
def get_depot_param(self,depot,param):
try:
self.depot_param = self.cfg.depots[depot][param]
except ( TypeError, KeyError) as e:
try:
self.depot_param = getattr(self.cfg, param)
except KeyError as e:
sys.exit(e.errno)
return self.depot_param
def get_project_param(self,project,param):
try:
self.project_param = self.cfg.projects[project][param]
except ( TypeError, KeyError) as e:
try:
self.project_param = getattr(self.cfg, param)
except KeyError as e:
sys.exit(e.errno)
return self.project_param
def get_project_matches(self,project):
try:
self.reglist = self.cfg.projects[project]['matchregex']
except KeyError as e:
try:
self.reglist = self.cfg.matchregex
except KeyError as e:
print "Error in configuration file: {0}: No default regex defined. Please add a matchregex entry on conf file".format(e)
sys.exit(e.errno)
if isinstance(self.reglist, str):
self.reglist = self.reglist.split()
return self.reglist
def get_depots(self):
return self.cfg.depots.keys()
if __name__ == '__main__':
# Read config file to cfg
config = Config(data=backup_conf)
代码运行良好,我能够获取以下数据: config.cfg.loglevel
按预期返回INFO
。但是我希望知道如何调用config.loglevel
从我的self.cfg
实例变量中删除清除的cfg
。(当然,欢迎任何增强代码的提示)。
最简单的解决方案是使用 PYYaml 构造函数,即将类映射到 yaml 类型。
(1) 使用构造函数
您所要做的就是使您的类成为yaml.YAMLObject
的子类,添加yaml_tag
成员以告诉yaml何时使用该类来构造该类的实例(而不是字典),然后您就设置了:
class Config(yaml.YAMLObject):
yaml_tag = '!Config'
@classmethod
def load(self, filename="backup.cfg", data=None):
self.cfg = {}
if data is None:
with open(filename,'r') as f:
yamlcfg = yaml.load(f)
else:
yamlcfg = yaml.load(data)
return yamlcfg
backup_conf="""
!Config
loglevel: INFO
username: root
password: globalsecret
destdir: /dsk/bckdir/
avoidprojects:
matchregex: /bkp/
depots:
server1:
password: asecret
server2:
username: root
server3:
server4:
destdir: /disk2/bkp/
projects:
proj1:
matchregex:
- /backups/
- /bkp/
"""
if __name__ == '__main__':
# Read config file to cfg
config = Config.load(data=backup_conf)
如您所见,我正在使用工厂方法来加载数据并创建实例,这就是 load
类方法的用途。
该方法的优点之一是,您可以通过在 yaml 数据中编写类型标记来直接键入所有元素。因此,如果您愿意,也可以使用类似的方法键入服务器,使您的yaml如下所示:
depots:
server1: !Server
password: asecret
server2: !Server
username: root
server3: !Server
server4: !Server
destdir: /disk2/bkp
项目键中的每个项目都以相同的方式。
(2) 使用namedtuple
如果不想更改 YAML,则可以将Config
类设置为namedtuple
的子类,并且在加载 YAML 数据时,可以从字典中创建namedtuple
。
下面的代码片段中,我将创建一个递归函数(嵌套在 load class 方法中),该函数遍历所有dict
(和嵌套的 dict
s)并将它们转换为 namedtuple
s。
import yaml
from collections import namedtuple
class Config:
@classmethod
def load(self, filename='backup.cfg', data=None):
"""Load YAML document"""
def convert_to_namedtuple(d):
"""Convert a dict into a namedtuple"""
if not isinstance(d, dict):
raise ValueError("Can only convert dicts into namedtuple")
for k,v in d.iteritems():
if isinstance(v, dict):
d[k] = convert_to_namedtuple(v)
return namedtuple('ConfigDict', d.keys())(**d)
if data is None:
with open(filename, 'r') as f:
yamlcfg = yaml.load(f)
else:
yamlcfg = yaml.load(data)
return convert_to_namedtuple(yamlcfg)
当你运行它时:
>>> cfg = Config.load(data=backup_conf)
>>> print cfg.username, cfg.destdir
root /dsk/bckdir/
>>> print cfg.depots.server4.destdir
/disk2/bkp/
>>> print cfg.depots.server2.username
root
(3) 使用自定义yaml.Loader
构建namedtuple
我试图找出一种方法来做到这一点,但经过一些尝试和错误,我明白这将花费我太多时间来弄清楚它,而且它会变得太复杂,以至于它作为一个易于理解的解决方案是可行的。只是为了好玩,这就是难以实现的原因。
有一种方法可以创建自己的默认加载器,并更改默认节点的转换方式。在默认加载器中,您可以重写创建 dict
s 的方法,使其创建 namedtuple
s:
class ConfigLoader(yaml.Loader):
def construct_mapping(self, node, deep=False):
# do whatever it does per default to create a dict, i.e. call the ConfigLoader.construct_mapping() method
mapping = super(ConfigLoader, self).construct_mapping(node, deep)
# then convert the returned mapping into a namedtuple
return namedtuple('ConfigDict', mapping.keys())(**mapping)
唯一的问题是调用该方法的另一个方法期望首先构建dict
树,然后才使用值更新它:
def construct_yaml_map(self, node):
data = {}
yield data ## the object is returned here, /before/ it is being populated
value = self.construct_mapping(node)
data.update(value)
所以,正如我所说,当然有办法,但如果它花了我太多时间来弄清楚,那么没有必要向你展示如何做到这一点,因为这会让你(和未来的读者)难以理解。正如我看到@user1340544的答案,您可能需要考虑使用 EasyDict
而不是 collections.namedtuple
(如果您没问题使用外部包)。
结论
因此,正如您在此处看到的,data
字段被构建为一个空字典,在将值添加到调用方之前,该dict
被yield
给调用方。因此,只有在构建字典后才会添加值。但是namedtuple
需要在一步中构建(即:您需要事先知道所有密钥),因此无法使用该方法。
我个人更喜欢选项 (1),使用标签,因为您可以使用它映射到的类来验证配置(并在缺少配置项、错误键入或额外项时发出警报)。您还可以从每种类型使用不同的名称中受益,从而在解析配置文件时轻松报告错误,并且只需最少的额外代码即可完成所有操作。当然,选项(2)做得很好。
呵呵
不同的映射键分配为属性后,您不能轻易迭代它们,但您可以执行以下操作:
from __future__ import print_function
import errno
import sys
import yaml
backup_conf="""
loglevel: INFO
username: root
password: globalsecret
destdir: /dsk/bckdir/
avoidprojects:
matchregex: /bkp/
depots:
server1:
password: asecret
server2:
username: root
server3:
server4:
destdir: /disk2/bkp/
projects:
proj1:
matchregex:
- /backups/
- /bkp/
"""
class Struct:
pass
def __repr__(self):
res = {}
for x in dir(self):
if x.startswith('__'):
continue
res[x] = getattr(self, x)
return repr(res)
def assign_dict_as_attr(obj, d):
assert isinstance(d, dict)
for key in d:
value = d[key]
if isinstance(value, dict):
x = Struct()
setattr(obj, key, x)
assign_dict_as_attr(x, value)
else:
setattr(obj, key, value)
class Config:
def __init__(self, filename="backup.cfg", data=None):
self.cfg = {}
if data is None:
try:
fd = open(filename,'r')
try:
yamlcfg = yaml.safe_load(fd)
except yaml.YAMLError as e:
sys.exit(e.errno)
finally:
fd.close()
except ( IOError, OSError ) as e:
sys.exit(e.errno)
else:
try:
yamlcfg = yaml.safe_load(data)
except yaml.YAMLError as e:
sys.exit(e.errno)
print('yamlcfg', yamlcfg)
assign_dict_as_attr(self, yamlcfg)
if __name__ == '__main__':
# Read config file to cfg
config = Config(data=backup_conf)
print('loglevel', config.loglevel)
print('depots.server1', config.depots.server1)
print('depots.server1.password', config.depots.server1.password)
获得:
loglevel INFO
depots.server1 {'password': 'asecret'}
depots.server1.password asecret
另一种解决方案是使__getattr__()
更智能:
class Struct:
def __init__(self, d):
self._cfg = d
def __getattr__(self, name):
res = self._cfg[name]
if isinstance(res, dict):
res = Struct(res)
return res
def __str__(self):
res = {}
for x in self._cfg:
if x.startswith('__'):
continue
res[x] = self._cfg[x]
return repr(res)
class Config:
def __init__(self, filename="backup.cfg", data=None):
self.cfg = {}
if data is None:
try:
fd = open(filename,'r')
try:
self._cfg = yaml.safe_load(fd)
except yaml.YAMLError as e:
sys.exit(e.errno)
finally:
fd.close()
except ( IOError, OSError ) as e:
sys.exit(e.errno)
else:
try:
self._cfg = yaml.safe_load(data)
except yaml.YAMLError as e:
sys.exit(e.errno)
def __getattr__(self, name):
res = self._cfg[name]
if isinstance(res, dict):
res = Struct(res)
return res
if __name__ == '__main__':
# Read config file to cfg
config = Config(data=backup_conf)
print('loglevel', config.loglevel)
print('depots.server1', config.depots.server1)
print('depots.server1.password', config.depots.server1.password)
这为您提供了与以前相同的输出。
只需将 easydict 与 anyconfig 结合使用即可。