중첩 된 사전을 구현하는 가장 좋은 방법은 무엇입니까?

본질적으로 중첩 된 사전에 해당하는 데이터 구조가 있습니다. 다음과 같이 가정 해 봅시다.

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

자, 이것을 유지하고 만드는 것은 꽤 고통 스럽습니다. 새로운 주 / 군 / 직업을 가질 때마다 난해한 try / catch 블록을 통해 하위 계층 사전을 만들어야합니다. 또한 모든 값을 다루려면 성가신 중첩 반복자를 만들어야합니다.

튜플을 키로 사용할 수도 있습니다.

{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'salesmen'): 62,
 ('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

이렇게하면 값을 매우 간단하고 자연스럽게 반복 할 수 있지만 집계와 같은 작업을 수행하고 사전의 하위 집합을 보는 것이 더 구문 적으로 고통 스럽습니다 (예 : 상태별로 이동하려는 경우).

기본적으로 때로는 중첩 사전을 플랫 사전으로 생각하고 때로는 복잡한 계층 구조로 생각하고 싶습니다. 나는 이것을 모두 한 수업에 넣을 수는 있지만 누군가가 이미 이것을 한 것처럼 보입니다. 또는,이를 위해 정말 우아한 구문 구조가있을 수 있습니다.

어떻게하면 더 잘할 수 있습니까?

부록 : 알고 setdefault()있지만 실제로 구문을 깨끗하게 만들지는 않습니다. 또한 생성 한 각 하위 사전은 여전히 setdefault()수동으로 설정해야합니다.



답변

파이썬에서 중첩 사전을 구현하는 가장 좋은 방법은 무엇입니까?

이것은 나쁜 생각입니다.하지 마십시오. 대신 일반 사전을 사용 dict.setdefault하고 apropos를 사용하십시오 . 따라서 일반적인 사용법으로 키가 누락되면 예상을 얻습니다 KeyError. 이 행동을 취해야한다고 주장하는 경우, 발로 자신을 쏘는 방법은 다음과 같습니다.

새 인스턴스를 설정하고 반환하기 __missing__위해 dict서브 클래스를 구현하십시오 .

이 접근법은 Python 2.5부터 사용 가능 하고 문서화 되어 있으며 (특히 나에게 귀중한) 자동 활성화 된 defaultdict의 추악한 인쇄 대신 일반 dict처럼 인쇄합니다.

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)() # retain local pointer to value
        return value                     # faster to return than dict lookup

(메모 self[key]는 과제의 왼쪽에 있으므로 재귀는 없습니다.)

데이터가 있다고 가정 해보십시오.

data = {('new jersey', 'mercer county', 'plumbers'): 3,
        ('new jersey', 'mercer county', 'programmers'): 81,
        ('new jersey', 'middlesex county', 'programmers'): 81,
        ('new jersey', 'middlesex county', 'salesmen'): 62,
        ('new york', 'queens county', 'plumbers'): 9,
        ('new york', 'queens county', 'salesmen'): 36}

사용 코드는 다음과 같습니다.

vividict = Vividict()
for (state, county, occupation), number in data.items():
    vividict[state][county][occupation] = number

그리고 지금:

>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

비판

이 유형의 컨테이너에 대한 비판은 사용자가 키의 철자를 잘못 입력하면 코드가 자동으로 실패 할 수 있다는 것입니다.

>>> vividict['new york']['queens counyt']
{}

또한 이제 데이터에 철자가 틀린 카운티가 있습니다.

>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36},
              'queens counyt': {}}}

설명:

우리는 Vividict키에 액세스 할 때마다 클래스의 또 다른 중첩 인스턴스를 제공하고 있습니다. (값 할당을 반환하면 dict에서 getter를 추가로 호출하지 않아도되므로 설정이 완료되면 반환 할 수 없기 때문에 유용합니다.)

이것들은 가장 많이 응답 된 답변과 동일한 의미이지만 코드 라인의 절반에 해당합니다-nosklo의 구현 :

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

사용법의 데모

다음은이 dict를 사용하여 중첩 된 dict 구조를 즉시 작성하는 방법에 대한 예입니다. 이를 통해 원하는만큼 깊이 계층 적 트리 구조를 신속하게 만들 수 있습니다.

import pprint

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

d = Vividict()

d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)

어떤 출력 :

{'fizz': {'buzz': {}},
 'foo': {'bar': {}, 'baz': {}},
 'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}

마지막 줄에서 알 수 있듯이 수동 검사를 위해 아름답게 인쇄됩니다. 그러나 데이터를 시각적으로 검사 __missing__하려면 클래스의 새 인스턴스를 키로 설정하고 반환하는 것이 훨씬 나은 솔루션입니다.

대조를위한 다른 대안들 :

dict.setdefault

asker는 이것이 깨끗하지 않다고 생각하지만, Vividict나 자신 보다 선호한다고 생각합니다 .

d = {} # or dict()
for (state, county, occupation), number in data.items():
    d.setdefault(state, {}).setdefault(county, {})[occupation] = number

그리고 지금:

>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

철자가 틀리면 실패 할 수 있으며 잘못된 정보로 인해 데이터가 복잡해지지 않습니다

>>> d['new york']['queens counyt']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'

또한 setdefault가 루프에서 사용될 때 훌륭하게 작동한다고 생각하며 키에 대해 무엇을 얻을지 모르지만 반복적 인 사용법은 상당히 부담이되며 사람은 다음을 유지하고 싶지는 않습니다.

d = dict()

d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})

또 다른 비판은 setdefault가 사용 여부에 관계없이 새 인스턴스를 필요로한다는 것입니다. 그러나 파이썬 (또는 적어도 CPython)은 사용되지 않고 참조되지 않은 새로운 인스턴스를 처리하는 데 다소 똑똑합니다. 예를 들어 메모리의 위치를 ​​재사용합니다.

>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)

자동 활성화 된 defaultdict

이것은 깔끔하게 보이는 구현이며 데이터를 검사하지 않는 스크립트의 사용법은 구현하는 것만 큼 유용합니다 __missing__.

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

그러나 데이터를 검사해야하는 경우 동일한 방식으로 데이터로 채워진 자동 활성화 된 기본 결과는 다음과 같습니다.

>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint; 
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict 
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar': 
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function 
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>, 
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at 
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})

이 출력은 매우 우아하지 않으며 결과를 읽을 수 없습니다. 일반적으로 제공되는 솔루션은 수동 검사를 위해 재귀 적으로 사전으로 변환하는 것입니다. 이 사소한 해결책은 독자를위한 연습으로 남습니다.

공연

마지막으로 성능을 살펴 보겠습니다. 인스턴스화 비용을 빼고 있습니다.

>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747

성능에 dict.setdefault따라 가장 잘 작동합니다. 실행 속도에 관심이있는 경우 프로덕션 코드로 사용하는 것이 좋습니다.

IPython 노트북에서 대화 형 사용을 위해 이것이 필요한 경우 성능은 실제로 중요하지 않습니다.이 경우 출력의 가독성을 위해 Vividict와 함께 할 것입니다. AutoVivification 객체 ( 이 목적으로 만들어진 __getitem__대신 대신 사용 __missing__)에 비해 훨씬 뛰어납니다.

결론

새 인스턴스를 설정하고 반환하기 __missing__위해 서브 클래스 dict에 구현 하는 것은 대안보다 약간 어렵지만 다음과 같은 이점이 있습니다.

  • 쉬운 인스턴스화
  • 쉬운 데이터 수집
  • 쉬운 데이터보기

수정하는 것보다 덜 복잡하고 성능이 좋기 때문에 __getitem__해당 방법보다 선호되어야합니다.

그럼에도 불구하고 단점이 있습니다.

  • 잘못된 조회는 자동으로 실패합니다.
  • 잘못된 조회는 사전에 남아 있습니다.

따라서 나는 개인적으로 setdefault다른 솔루션을 선호 하며 모든 종류의 상황에서 이런 종류의 행동이 필요했습니다.


답변

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

테스트 :

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

산출:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}


답변

내가이 작은 것을 보지 못했기 때문에 여기에 땀을 들이지 않고 원하는만큼 중첩되는 구술이 있습니다.

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)


답변

YAML 파일을 만들고 PyYaml 을 사용하여 읽을 수 있습니다 .

1 단계 : “employment.yml”YAML 파일을 만듭니다.

new jersey:
  mercer county:
    pumbers: 3
    programmers: 81
  middlesex county:
    salesmen: 62
    programmers: 81
new york:
  queens county:
    plumbers: 9
    salesmen: 36

2 단계 : 파이썬으로 읽어보기

import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()

이제 my_shnazzy_dictionary모든 가치가 있습니다. 이 작업을 즉시 수행해야하는 경우 YAML을 문자열로 생성하여에 입력 할 수 있습니다 yaml.safe_load(...).


답변

스타 스키마 디자인이 있으므로 관계형 테이블처럼 구성하고 사전보다는 덜 구성 할 수 있습니다.

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

이런 종류의 일은 SQL 오버 헤드없이 데이터웨어 하우스와 같은 디자인을 만드는 데 큰 도움이 될 수 있습니다.


답변

중첩 수준의 수가 적 으면 이것을 사용 collections.defaultdict합니다.

from collections import defaultdict

def nested_dict_factory():
  return defaultdict(int)
def nested_dict_factory2():
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

사용 defaultdict지저분한 많은 같이 피한다 setdefault(), get()


답변

임의의 깊이로 중첩 된 사전을 반환하는 함수입니다.

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

다음과 같이 사용하십시오.

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

다음과 같이 모든 것을 반복하십시오.

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

인쇄됩니다 :

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

결국 새 항목을 dict에 추가 할 수 없도록 만들 수도 있습니다. 이 모든 것들을 재귀 적 defaultdict으로 정상으로 쉽게 변환 할 수 dict있습니다.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)