如何在Python 3中正确地包装一个字典?

4
有时候,我们可以像使用对象一样使用字典(这样就不用总是写mydict['blah']了,而是可以写成mydict.blah)。
在Python 3中,覆盖/包装Python字典有了新规则。在Python2中,仅需要包装__getattr____setattr__方法即可。当你包装这些方法时,你可以添加一些特殊处理的能力,比如在添加某个属性时进行数据清洗/过滤等操作。
在Flask模板(HTML模板)中,这种情况非常有用。通过使用__getattr__过滤器,您可以在离开dict之前对数据进行格式化。这样,在模板中(Python表达式可能看起来有点复杂),你只需写mymodel.blah,并知道从blah出来的文本已经是你想要的方式。
在Python 3中包装一个dict有点麻烦。我不确定现在该怎么做。以下是两个粗略的实现,效果不佳:
# messed up Python3 wrapped dictionary (sets work, but gets do not)
class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self
        self.clean_strings()
    def clean_strings(self):
        for key, value in self.items():
            self[key] = string_empty_to_none(value)
    def __getattr__(self, name):
        #this never gets called in Python 3
        return self[name]

这里还有一个:

# sets don't work - Always throws: TypeError: 'AttrDict' object does not support item assignment
class AttrDict():
    def __init__(self, *args, **kwargs):
        self.data = dict()
        self.data.__init__(*args, **kwargs)
        self.clean_strings()
    def clean_strings(self):
        for key, value in self.data.items():
            self.data[key] = string_empty_to_none(value)
    def __getattr__(self, attr):
        if attr in self.data:
            return self.data[attr]
        else:
            raise AttributeError("--%r object has no attribute %r" % (type(self).__name__, attr)) 
    def __setattr__(self, name, value):
        if name == 'data':
            super(AttrDict, self).__setattr__(name, value)
        else:
            self.data[name] = value

这是我的小工具方法的样子:

def string_empty_to_none(s):
    if type(s) == str:
            return None if (s.strip() == '') else s

    return s

我知道在Python3中,你应该使用__getattribute__而不是__getattr__。但是当我这样做时,总是陷入无限循环。

注意:我要求的最终语法应该像这样:

>>> x = AttrDict({'some': 'value'})
>>> x['blah'] = 'hello world'
>>> print(x.blah)
hello world
>>> print(x.some)
value

1
Python 2的实现在Python 3上不需要修改即可正常工作。关于包装字典没有新规则。你混乱的实现在Python 3上失败,但它们也会在Python 2上失败。 - user2357112
3个回答

2
你可以通过在__getattr__中使用__getitem__来实现这一点。
class AttrDict(dict):

    def __getattr__(self, item):
        return super().__getitem__(item)

    def __setattr__(self, item, value):
        return super().__setitem__(item, value)


x = AttrDict({'some': 'value'})
x['blah'] = 'hello world'
print(x.blah)  # hello world
print(x.some)  # value

# you can also assign value this way
x.foo = 'bar'
print(x['foo'])   # bar

你会在回答中如何改进我的__init__方法? - 101010

1
实际上,在一个继承自字典的类的 __init__ 方法中,没有必要执行 self.__dict__ 赋值操作 - 只需将参数传递给 super(),然后进行处理即可。实际上,我甚至不确定 self.__dict__ = self 意味着什么 - 我可以想象,如果你重写了它们的 __dict__(即使是 "their selves"),它会破坏字典的某些内部行为。
另外,修改 __getattr__ 方法更好,因为这将是 Python 的后备方法,如果 __getattribute__ 找不到任何内容就会使用它。另一方面,如果您知道您将大部分时间使用基于属性的访问器,则可以切换该逻辑。
请参阅以下示例:
def string_empty_to_none(s):
  if type(s) == str:
    return None if (s.strip() == '') else s
  return s

# This will always have issues, even in IDE's
print('AttrDict')
class AttrDict(dict):
  def __init__(self, *args, **kwargs):
    super(AttrDict, self).__init__(*args, **kwargs)
    self.__dict__ = self
    self.clean_strings()
  def clean_strings(self):
    for key, value in self.items():
      self[key] = string_empty_to_none(value)

test = AttrDict({'a': 1})
test['x'] = 2
test.z = ""
print(test.a)
print(test['a'])
print(test.x)
print(test['x'])
print(test.z)
print(test['z'])


print('MostlyPropertiesAccessDict')
class MostlyPropertiesAccessDict(dict):
  def __init__(self, *args, **kwargs):
    # No need for the self.__dict__ part
    super().__init__(*args, **kwargs)
    self.clean_strings()

  def clean_strings(self):
    for key, value in self.items():
      self[key] = string_empty_to_none(value)

  def __getattr__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    return self[name]

  def __setattr__(self, name, value):
    self[name] = string_empty_to_none(value)

  def __delattr__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    del self[name]

test2 = MostlyPropertiesAccessDict({'a': 1})
test2['x'] = 2
test2.z = ""
print(test2.a)
print(test2['a'])
print(test2.x)
print(test2['x'])
print(test2.z)
print(test2['z'])

print("MostlyKeysAccessDict")
class MostlyKeysAccessDict(dict):
  def __init__(self, *args, **kwargs):
    # No need for the self.__dict__ part
    super().__init__(*args, **kwargs)
    self.clean_strings()

  def clean_strings(self):
    for key, value in self.items():
      self[key] = string_empty_to_none(value)

  def __getattribute__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    return self[name]

  def __getattr__(self, name):
    return super().__getattribute__(name)

  def __setattr__(self, name, value):
    self[name] = string_empty_to_none(value)

  def __delattr__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    del self[name]

test3 = MostlyKeysAccessDict({'a': 1})
test3['x'] = 2
test3.z = ""
print(test3.a)
print(test3['a'])
print(test3.x)
print(test3['x'])
print(test3.z)
print(test3['z'])

很棒的答案。另一个答案(@bubble...)使用getitem/setitem。你的方法有什么优势吗?感谢您解释如何改进__init__ - 101010
一般来说,通过__getItem__(name)或者self[name]检索值并不重要(对于__setItem__(name, value)self[name] = value同理)。后者在Python内部始终调用前者,并且是默认方式,当没有特定原因需要使用内部时,可以使用后者。实际上真正重要的是__init__那一部分。还更新了代码,在添加值时进行空字符串检查。 - wiesion

0

你所需要做的就是创建一个包含字典的封装类,然后实现__getitem__方法:

class DictionaryWrapper():

    _dict = {"key1": "value1", "key2": "value2"}

    def __getitem__(self, item):
        return self._dict[item]

    @attribute
    def key1(self):
        return self._dict["key1"]

现在,您可以将DictionartWrapper视为字典。(此有限实现仅允许读取。)
my_dictionary = DictionaryWrapper()
print(my_dictionary["key1"])

如果您的字典中的键已知,甚至可以通过包装类上的属性公开它们的值。
print(my_dictionary.key1)

有关类似的讨论,请参见此处


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接