Что мне делать, когда мне нужен самоссылочный словарь? - PullRequest
36 голосов
/ 17 сентября 2010

Я новичок в Python и удивлен, что не могу этого сделать.

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

Мне интересно, как Pythonic правильно делает это в моем сценарии, потому что я чувствую, что я не единственный, кто пытался это сделать.

РЕДАКТИРОВАТЬ: Достаточно, чтобы люди задавались вопросом, что я делаю с этим, поэтому вот больше деталей для моих вариантов использования. Допустим, я хочу, чтобы объекты словаря содержали пути файловой системы. Пути относительно других значений в словаре. Например, вот так может выглядеть один из моих словарей.

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

Важно, чтобы в любой момент времени я мог изменить dictionary['user'] и чтобы все значения словарей отражали это изменение. Опять же, это пример того, для чего я его использую, поэтому я надеюсь, что он передает мою цель.

Исходя из моих собственных исследований, я думаю, что мне потребуется реализовать класс для этого.

Ответы [ 9 ]

46 голосов
/ 18 сентября 2010

Не бойтесь создавать новые классы - Вы можете воспользоваться возможностями форматирования строк в 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"]
13 голосов
/ 18 сентября 2010

Ближайший я пришел без выполнения объекта:

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

print dictionary['home']()
dictionary['user']='tony'
print dictionary['home']()
8 голосов
/ 17 сентября 2010
>>> dictionary = {
... 'a':'123'
... }
>>> dictionary['b'] = dictionary['a'] + '456'
>>> dictionary
{'a': '123', 'b': '123456'}

Он отлично работает, но когда вы пытаетесь использовать dictionary, он еще не определен (потому что сначала он должен оценить этот буквальный словарь).

Но будьте осторожны, потому что это присваивает ключу 'b' значение, на которое ссылается ключ 'a' во время присвоения , и не будет выполнять поиск каждый раз. Если это то, что вы ищете, это возможно, но с большим количеством работы.

5 голосов
/ 18 сентября 2010

Это интересная проблема.Похоже, у Грега есть хорошее решение .Но это не весело;)

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 о том, почему я назвал классы так, как я это сделал.

Слово «суррогат» обычно ассоциируется с заменой чего-то другого, поэтому оно кажется уместным, потому что именно так поступает класс SurrogateDict: экземпляр заменяет ссылки «self» в литерале словаря.Это, как говорится, (кроме того, чтобы быть просто глупым иногда) именование, вероятно, одна из самых сложных вещей для меня в кодировании.Если вы (или кто-либо еще) можете предложить более подходящее имя, я весь слух.

Я приведу краткое объяснение.В течение S относится к экземпляру SurrogateDict, а d - настоящий словарь.

  1. Ссылка S[key] вызывает S.__getitem__ и SurrogateDictEntry(key) для размещения в d.

  2. Когда построено S[key] = SurrogateDictEntry(key), оно хранит key.Это будет key в d для значения, для которого эта запись 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 с разрешением всех ссылок.

Это грубый набросок.Если у вас есть еще вопросы, не стесняйтесь спрашивать.

5 голосов
/ 18 сентября 2010

То, что вы описываете в своем редактировании, - это то, как работает файл конфигурации INI. Python имеет встроенную библиотеку под названием ConfigParser , которая должна работать для того, что вы описываете.

3 голосов
/ 22 октября 2014

Если вы, как и я, бродите, как заставить @ jsbueno snippet работать с {} заменами стилей, ниже приведен пример кода (который, вероятно, не очень эффективен):

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», который имеетэто собственная ссылка на 'user') из-за порядка оценки (** расширение выполняется до фактического вызова формата, и оно не задерживается, как в исходной версии%).

2 голосов
/ 18 сентября 2010

Напишите класс, возможно что-нибудь со свойствами:

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 
1 голос
/ 24 апреля 2019

Это очень легко для лениво оцененного языка (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 на некоторых функциональных языках.

1 голос
/ 19 сентября 2010

В качестве расширенной версии @ ответа Тони , вы можете создать подкласс словаря, который будет вызывать его значения, если они являются вызываемыми:можно использовать, если вы не собираетесь хранить вызываемые объекты как значения.Если вам нужно это сделать, вы можете обернуть лямбда-объявление в функцию, которая добавляет какой-то атрибут к результирующей лямбде, и проверить его в CallingDict.__getitem__, но в этот момент он становится сложным и многословнымДостаточно того, что было бы проще использовать класс для ваших данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...