繁体   English   中英

当我需要一本自参考字典时我该怎么办?

[英]What do I do when I need a self referential dictionary?

我是 Python 新手,有点惊讶我不能这样做。

dictionary = {
    'a' : '123',
    'b' : dictionary['a'] + '456'
}

我想知道在我的脚本中正确执行此操作的 Pythonic 方法是什么,因为我觉得我不是唯一一个尝试执行此操作的人。

编辑:有足够多的人想知道我在做什么,所以这里有更多关于我的用例的细节。 假设我想保留字典对象来保存文件系统路径。 路径相对于字典中的其他值。 例如,这就是我的一本词典的样子。

dictionary = {
    'user': 'sholsapp',
    'home': '/home/' + dictionary['user']
}

重要的是,我可以随时更改dictionary['user']并让所有字典值反映更改。 同样,这是我使用它的一个例子,所以我希望它传达了我的目标。

根据我自己的研究,我认为我需要实现一个类来做到这一点。

不用担心创建新类 - 您可以利用 Python 的字符串格式化功能,只需执行以下操作:

class MyDict(dict):
   def __getitem__(self, item):
       return dict.__getitem__(self, item) % self

dictionary = MyDict({

    'user' : 'gnucom',
    'home' : '/home/%(user)s',
    'bin' : '%(home)s/bin' 
})


print dictionary["home"]
print dictionary["bin"]

最近我没有做对象就出现了:

dictionary = {
    'user' : 'gnucom',
    'home' : lambda:'/home/'+dictionary['user'] 
}

print dictionary['home']()
dictionary['user']='tony'
print dictionary['home']()
>>> dictionary = {
... 'a':'123'
... }
>>> dictionary['b'] = dictionary['a'] + '456'
>>> dictionary
{'a': '123', 'b': '123456'}

它工作正常,但是当您尝试使用dictionary它尚未定义(因为它必须首先评估该文字字典)。

但要小心,因为这就赋予的关键'b'通过的关键参考值'a'在转让时,并不会每次都做查找。 如果这就是您正在寻找的,那是可能的,但需要做更多的工作。

您在编辑中描述的是 INI 配置文件的工作方式。 Python 确实有一个名为ConfigParser的内置库,它应该适用于您所描述的内容。

这是一个有趣的问题。 看起来 Greg 有一个很好的解决方案 但这并不好玩;)

jsbueno 作为一个非常优雅的解决方案,但这仅适用于字符串(根据您的要求)。

“通用”自引用字典的诀窍是使用代理对象。 需要几行(轻描淡写的)代码才能完成,但用法与您想要的有关:

S = SurrogateDict(AdditionSurrogateDictEntry)
d = S.resolve({'user': 'gnucom',
               'home': '/home/' + S['user'],
               'config': [S['home'] + '/.emacs', S['home'] + '/.bashrc']})

实现这一目标的代码并没有那么短。 它分为三个类:

import abc

class SurrogateDictEntry(object):
    __metaclass__ = abc.ABCMeta
    def __init__(self, key):
        """record the key on the real dictionary that this will resolve to a 
           value for
        """
        self.key = key

    def resolve(self, d):
        """ return the actual value"""
        if hasattr(self, 'op'):
            # any operation done on self will store it's name in self.op. 
            # if this is set, resolve it by calling the appropriate method 
            # now that we can get self.value out of d
            self.value = d[self.key]
            return getattr(self, self.op + 'resolve__')()
        else:
            return d[self.key]

    @staticmethod
    def make_op(opname):
        """A convience class. This will be the form of all op hooks for subclasses
           The actual logic for the op is in __op__resolve__ (e.g. __add__resolve__)
        """
        def op(self, other):
            self.stored_value = other
            self.op = opname
            return self
        op.__name__ = opname
        return op

接下来是具体的类。 足够简单。

class AdditionSurrogateDictEntry(SurrogateDictEntry):

    __add__ = SurrogateDictEntry.make_op('__add__')
    __radd__ = SurrogateDictEntry.make_op('__radd__')

    def __add__resolve__(self):
        return self.value + self.stored_value 

    def __radd__resolve__(self):
        return self.stored_value + self.value

这是最后一堂课

class SurrogateDict(object):
    def __init__(self, EntryClass):
        self.EntryClass = EntryClass

    def __getitem__(self, key):
        """record the key and return""" 
        return self.EntryClass(key)

    @staticmethod
    def resolve(d):
        """I eat generators resolve self references"""
        stack = [d]
        while stack:
            cur = stack.pop()
            # This just tries to set it to an appropriate iterable
            it = xrange(len(cur)) if not hasattr(cur, 'keys') else cur.keys()
            for key in it:
                # sorry for being a duche. Just register your class with
                # SurrogateDictEntry and you can pass whatever.
                while isinstance(cur[key], SurrogateDictEntry):
                    cur[key] = cur[key].resolve(d)
                # I'm just going to check for iter but you can add other
                # checks here for items that we should loop over. 
                if hasattr(cur[key], '__iter__'):
                    stack.append(cur[key])
        return d

为了回答 gnucoms 关于我为什么按照我的方式命名这些类的问题。

surrogate 这个词通常与代表其他事物相关联,所以它看起来很合适,因为这就是SurrogateDict类所做的:一个实例替换字典文字中的“自我”引用。 话虽如此,(除了有时只是直接愚蠢)命名可能是我编码中最困难的事情之一。 如果您(或其他任何人)可以建议一个更好的名字,我会全力以赴。

我将提供一个简短的解释。 整个S指的是 SurrogateDict 的一个实例,而d是真正的字典。

  1. 引用S[key]触发S.__getitem__SurrogateDictEntry(key)被放置在d

  2. 当构造S[key] = SurrogateDictEntry(key) ,它存储key 这将是dkey用于表示SurrogateDictEntry此条目充当其代理项的值。

  3. 返回S[key]后,它要么被输入到d ,要么对其执行一些操作。 如果对其执行操作,它会触发相关的__op__方法,该方法简单地存储执行操作的值和操作的名称,然后返回自身。 我们实际上无法解析操作,因为d还没有被构造。

  4. d构造完成后,将其传递给S.resolve 该方法通过d循环查找SurrogateDictEntry任何实例,并将它们替换为对该实例调用resolve方法的结果。

  5. SurrogateDictEntry.resolve方法接收现在构造的d作为参数,并且可以使用它在构造时存储的key的值来获取它作为代理的值。 如果在创建后对其执行了操作,则op属性将设置为已执行操作的名称。 如果该类有一个__op__方法,那么它就有一个__op__resolve__方法,其实际逻辑通常在__op__方法中。 所以现在我们有了逻辑 ( self.op__resolve ) 和所有必要的值 (self.value, self.stored_value) 来最终获得d[key]的真实值。 所以我们返回第 4 步放在字典中的那个。

  6. 最后SurrogateDict.resolve方法返回d并解析所有引用。

这是一个粗略的草图。 如果您还有其他问题,请随时提问。

如果您像我一样徘徊如何使@jsbueno 片段与 {} 样式替换一起工作,下面是示例代码(虽然可能效率不高):

import string

class MyDict(dict):
    def __init__(self, *args, **kw):
        super(MyDict,self).__init__(*args, **kw)
        self.itemlist = super(MyDict,self).keys()
        self.fmt = string.Formatter() 

    def __getitem__(self, item):
        return self.fmt.vformat(dict.__getitem__(self, item), {}, self)


xs = MyDict({
    'user' : 'gnucom',
    'home' : '/home/{user}',
    'bin' : '{home}/bin'
})


>>> xs["home"]
'/home/gnucom'
>>> xs["bin"]
'/home/gnucom/bin'

我试图通过将% self简单替换为.format(**self)来使其工作,但结果证明它不适用于嵌套表达式(如上面列表中的 'bin',它引用了 'home',它有由于评估顺序,它自己引用了“用户”)(** 扩展在实际格式调用之前完成,并且不像原始 % 版本那样延迟)。

写一个类,也许是一些有属性的东西:

class PathInfo(object):
    def __init__(self, user):
        self.user = user

    @property
    def home(self):
        return '/home/' + self.user

p = PathInfo('thc')
print p.home # /home/thc 

作为@Tony's answer的扩展版本,您可以构建一个字典子类,如果它们是可调用的,则调用其值:

class CallingDict(dict):
    """Returns the result rather than the value of referenced callables.

    >>> cd = CallingDict({1: "One", 2: "Two", 'fsh': "Fish",
    ...                   "rhyme": lambda d: ' '.join((d[1], d['fsh'],
    ...                                                d[2], d['fsh']))})
    >>> cd["rhyme"]
    'One Fish Two Fish'
    >>> cd[1] = 'Red'
    >>> cd[2] = 'Blue'
    >>> cd["rhyme"]
    'Red Fish Blue Fish'
    """
    def __getitem__(self, item):
        it = super(CallingDict, self).__getitem__(item)
        if callable(it):
            return it(self)
        else:
            return it

当然,这仅在您实际上不打算将可调用对象存储为值时才可用。 如果您需要能够做到这一点,您可以将 lambda 声明包装在一个函数中,该函数为结果 lambda 添加一些属性,并在CallingDict.__getitem__检查它,但此时它变得复杂且冗长,足以让首先为您的数据使用类可能会更容易。

这在惰性评估语言(haskell)中非常容易。

由于 Python 是严格评估的,我们可以做一个小技巧来让事情变得懒惰:

Y = lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))

d1 = lambda self: lambda: {
  'a': lambda: 3,
  'b': lambda: self()['a']()
}

# fix the d1, and evaluate it
d2 = Y(d1)()

# to get a
d2['a']() # 3

# to get b
d2['b']() # 3

语法明智这不是很好。 这是因为我们需要使用lambda: ...显式构造惰性表达式,并使用...()显式计算惰性表达式。 在需要严格注释的惰性语言中,这是相反的问题,在 Python 中,我们最终需要惰性注释。

我认为通过更多元编程和更多技巧,可以使上述内容更易于使用。

请注意,这基本上是 let-rec 在某些函数式语言中的工作方式。

Python 3 中的 jsbueno 答案:

class MyDict(dict):
    def __getitem__(self, item):
        return dict.__getitem__(self, item).format(self)

dictionary = MyDict({
    'user' : 'gnucom',
    'home' : '/home/{0[user]}',
    'bin' : '{0[home]}/bin' 
})

print(dictionary["home"])
print(dictionary["bin"])

她的母羊使用带有花括号{}的 python 3 字符串格式和.format()方法。

文档: https : //docs.python.org/3/library/string.html

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM