【问题标题】:Trying to make a dict behave like a clean class/method structure试图让 dict 表现得像一个干净的类/方法结构
【发布时间】:2016-06-24 05:19:45
【问题描述】:

我正在尝试制作一个字典(从 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 删除那个 cleary 来自我的 self.cfg 实例变量的 cfg。 (当然欢迎任何增强代码的技巧)。

【问题讨论】:

  • 您似乎已经尝试使用__getattribute__。当您尝试使用config.loglevel 时会发生什么?你有例外吗?
  • 是的。我试过了,但没有奏效。我得到:AttributeError: Config instance has no attribute 'loglevel',实际上我在__getattribute__ 中添加了except,但仍然没有用。我正在运行python 2.6.6(环境需要它)
  • 字典已经确实表现得像一个类。它有一个get(keyname) method,它接受一个字符串。您的意思是,您希望 dict 的行为类似于 object,即 允许按名称访问其属性(/properties)而无需引用。这个问题在此之前已经回答了很多次,请参阅重复项。

标签: python class dictionary yaml


【解决方案1】:

嗯,最简单的解决方案是使用 PYYaml 构造函数,即将一个类映射到一个 yaml 类型。

①使用构造函数

您所要做的就是让您的类成为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

在项目键中的每个项目都采用相同的方式。

② 使用namedtuples

如果您不想更改 yaml,则可以将 Config 类设为 namedtuple 的子类,并且在加载 yaml 数据时,可以从字典中创建 namedtuple

为此,在下面的 sn-p 中,我创建了一个递归函数(嵌套在 load 类方法中),它遍历所有 dicts(和嵌套的 dicts)并将它们转换为namedtuples.

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

③使用自定义yaml.Loader构建namedtuples

我试图找出一种方法来做到这一点,但经过一番尝试和错误后,我明白我需要花费太多时间才能弄清楚,而且它会变得过于复杂以至于无法作为一种易于理解的方法来实现解决方案。 只是为了好玩,这就是难以实施的原因。

有一种方法可以制作您自己的默认加载器,并更改默认节点的转换方式。在默认加载器中,您可以覆盖创建dicts 的方法,使其创建namedtuples:

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)

唯一的问题是another method calling that one 期望首先构建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's answer,您可能需要考虑使用EasyDict 而不是collections.namedtuple(如果您没问题 与外部包)。

结论

因此,您可以在此处看到 data 字段构建为一个空字典,在将值添加到其中之前,dict 是调用者的 yielded。因此,只有在构建 dict 之后才添加这些值。 但是namedtuple 需要一步构建(即:您需要事先知道所有键),因此无法使用该方法。

我个人更喜欢选项①,使用标签,因为您可以使用它映射到的类来验证配置(并在缺少配置项、错误键入或额外的配置项时发出警报)。 您还可以通过为每种类型使用不同的名称来获得收益,从而在解析配置文件时轻松报告错误,并且所有这些都只需最少的额外代码。当然,选项②做得很好。

HTH

【讨论】:

  • 我喜欢阅读更多关于您的想法,但实际上,我不想更改 YAML,也不想添加标签或 yaml 交叉引用(如 & 或 *)。
  • 好的,那么我将向您展示collections.namedtuple 的想法。我想我明天会在一台真正的计算机上回来炫耀它:-) 整个想法实际上是让Config 成为namedtuple 的子类,使用工厂方法加载 yaml 并转换每个 dict进入一个命名元组。
  • 另一种策略是在解析时使用add_constructor()将所有dict转换为namedtuple
  • 给你,正如我所承诺的,我举了几个例子。尽管正如我在更新中所说,使用自定义构造函数将所有 dicts 转换为 namedtuples 的想法并不像我写评论时想象的那样好。
【解决方案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)

这会为您提供与以前相同的输出。

【讨论】:

    【解决方案3】:

    只需将easydictanyconfig 结合使用即可。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-10-07
      • 1970-01-01
      • 2015-04-09
      • 2023-03-22
      • 2019-09-27
      • 1970-01-01
      • 2015-10-31
      • 1970-01-01
      相关资源
      最近更新 更多