본문 바로가기
Python

[Py] collections.ChainMap

by ds31x 2025. 4. 4.

1. ChainMap 이란?

  • ChainMap은 Python의 collections 모듈에서 제공하는 클래스로,
  • 여러 매핑(딕셔너리 등)을 단일 뷰로 그룹화하는 기능을 제공.

ChainMap은 여러 딕셔너리를 연결(chain)하여
마치 하나의 딕셔너리처럼 사용 가능한 자료구조.

 

내부적으로 매핑 목록을 유지하면서 이들을 함께 검색하는 구조임.

2023.07.11 - [Python] - [Python] dictionary (Mapping type) : basic

 

[Python] dictionary (Mapping type) : basic

dictionary (dict)Python에서 dictionary는key-value pair를 item으로 가지는unorderedmutablecollection임.set과 함께 curly bracket (or brace)를 사용하는데, empty dictionary가 바로 {}로 표현됨(dictionary가 set보다 많이 이용되는

ds31x.tistory.com


2. 기본 문법

2.1. 생성자

collections.ChainMap(*maps)

생성자 매개변수:

  • *maps:
    • 연결할 매핑 객체들(딕셔너리).
      매개변수를 제공하지 않으면 기본적으로 하나의 빈 딕셔너리를 포함하는 ChainMap 인스턴스 생성.

2.2. 반환값

생성자는 여러 매핑의 단일 통합 뷰를 제공하는 ChainMap 인스턴스 반환.


3. ChainMap 사용 튜토리얼

3.1. 기본 사용법

from collections import ChainMap

# 1. 두 개의 딕셔너리 정의
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}

# 2. ChainMap 생성
chain = ChainMap(dict1, dict2)

# 3. ChainMap 사용
print(chain['a'])  # 1 (dict1에서 찾음)
print(chain['b'])  # 2 (dict1에서 먼저 찾음)
print(chain['c'])  # 4 (dict2에서 찾음)

# 4. 존재하지 않는 키 접근 시 KeyError 발생
# print(chain['d'])  # KeyError: 'd'

# 5. 키 목록 확인 (중복 제거된 모든 키)
print(list(chain.keys()))  # ['a', 'b', 'c']

# 6. 값 목록 확인 (첫 번째 발견된 키의 값)
print(list(chain.values()))  # [1, 2, 4]

3.2. ChainMap 탐색 순서

  • ChainMap은 리스트에 저장된 매핑을 순서대로 검색하여 첫 번째로 발견되는 키 반환.
  • ChainMap에서는 앞쪽에 위치한 매핑(child라고 불림)이 높은 우선순위를 가지는 구조.

주의점

  • 키 검색 시 첫 번째 맵부터 순차적 탐색 진행.
  • 먼저 발견된 키의 값 반환 및 이후 맵 탐색 중단.
  • 모든 맵에서 키를 찾지 못한 경우 KeyError 발생.
from collections import ChainMap

# 여러 딕셔너리 정의
defaults = {'theme': 'default', 'language': 'en', 'showIndex': True, 'showFooter': True}
user_settings = {'theme': 'dark'}
site_settings = {'showFooter': False}

# ChainMap 생성 (검색 순서: user_settings -> site_settings -> defaults)
settings = ChainMap(user_settings, site_settings, defaults)

# 탐색 동작 확인
print(settings['theme'])      # 'dark' (user_settings에서 발견)
print(settings['language'])   # 'en' (defaults에서 발견)
print(settings['showFooter']) # False (site_settings에서 발견)

# 첫 번째 맵 확인
print(settings.maps[0])  # {'theme': 'dark'}

# 모든 맵 확인
print(settings.maps)  # [{'theme': 'dark'}, {'showFooter': False}, {...}]

3.3. ChainMap 수정

ChainMap 객체의 수정은 첫 번째 매핑에만 영향을 준다.
즉, 키 추가, 변경, 삭제와 같은 모든 수정 작업이 첫 번째 맵에서만 수행되는 특성을 가짐.

  • 새 키-값 쌍 추가 시 첫 번째 맵에만 추가.
  • 키 값 변경 시 첫 번째 맵의 해당 키만 변경.
  • 키 삭제 시 첫 번째 맵에 존재하는 키만 삭제 가능.
  • 첫 번째 맵에 없는 키 삭제 시도 시 KeyError 발생.
from collections import ChainMap

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
chain = ChainMap(dict1, dict2)

# 1. 키 수정 (첫 번째 맵에만 적용)
chain['b'] = 10
print(chain['b'])  # 10
print(dict1)       # {'a': 1, 'b': 10} - dict1이 수정됨
print(dict2)       # {'b': 3, 'c': 4} - dict2는 변경 없음

# 2. 새 키 추가 (첫 번째 맵에만 적용)
chain['d'] = 5
print(chain['d'])  # 5
print(dict1)       # {'a': 1, 'b': 10, 'd': 5} - dict1에 추가됨
print(dict2)       # {'b': 3, 'c': 4} - dict2는 변경 없음

# 3. del 연산 (첫 번째 맵에서만 삭제)
del chain['b']
print('b' in chain)  # True - dict2에 여전히 존재
print(dict1)         # {'a': 1, 'd': 5} - dict1에서 삭제됨
print(dict2)         # {'b': 3, 'c': 4} - dict2는 변경 없음

# 4. 첫 번째 맵에 없는 키 삭제 시도시 KeyError 발생
# del chain['c']  # KeyError: "Key not found in the first mapping: 'c'"

3.4. child와 parent 개념

ChainMap에서 'child'와 'parent'는 계층 구조를 표현하는 중요한 개념.

  • child(자식):
    • ChainMap에서 앞쪽(인덱스가 작은)에 위치한 맵.
    • 높은 우선순위를 가지며 수정 작업이 이루어지는 위치.
    • 보통 첫 번째 맵(maps[0])이 기본 child.
  • parent(부모):
    • 첫 번째 맵을 제외한 나머지 맵들의 ChainMap.
    • parents 속성으로 접근 가능한 구조.
  • 계층 관계:
    • 자식 맵이 부모 맵보다 우선순위가 높으며, 변경 가능한 상태를 표현할 때 유용한 구조.
from collections import ChainMap

# 기본 맵 정의
local_settings = {'debug': True}  # child - 지역 설정
global_settings = {'debug': False, 'log_level': 'INFO'}  # parent의 일부 - 전역 설정
default_settings = {'debug': False, 'log_level': 'WARNING', 'theme': 'default'}  # parent의 일부 - 기본 설정

# ChainMap 생성 (local_settings가 child, 나머지는 parent)
settings = ChainMap(local_settings, global_settings, default_settings)

# child 접근 및 수정
print(settings.maps[0])  # {'debug': True} - child 맵
settings['log_level'] = 'DEBUG'  # child 맵에 새 키-값 추가
print(settings['log_level'])  # 'DEBUG'

# parent 접근
parent_settings = settings.parents  # 첫 번째 맵 제외한 ChainMap
print(parent_settings['log_level'])  # 'INFO' - global_settings에서 참조
print(parent_settings['theme'])  # 'default' - default_settings에서 참조

# 새로운 child 맵 추가
temporary_settings = {}
new_settings = settings.new_child(temporary_settings)  # temporary_settings가 새로운 child
print(new_settings.maps)  # [{}(새 child), {'debug': True, 'log_level': 'DEBUG'}, ...]

ChainMap의 child-parent 구조는 범위(스코프), 설정 계층, 오버라이드 메커니즘 등 여러 개념적 모델을 표현하는데 강력한 도구임.

from collections import ChainMap

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
chain = ChainMap(dict1, dict2)

# 1. 새로운 빈 맵을 앞에 추가 (new_child 메서드)
new_chain = chain.new_child()
print(new_chain.maps)  # [{}, {'a': 1, 'b': 2}, {'b': 3, 'c': 4}]

# 2. 특정 맵을 앞에 추가
dict3 = {'a': 10, 'd': 5}
new_chain2 = chain.new_child(dict3)
print(new_chain2['a'])  # 10 (dict3의 값이 우선)
print(new_chain2.maps)  # [{'a': 10, 'd': 5}, {'a': 1, 'b': 2}, {'b': 3, 'c': 4}]

# 3. 부모 체인 얻기 (첫 번째 맵 제외)
parents = chain.parents
print(parents.maps)  # [{'b': 3, 'c': 4}]

# 4. 새로운 ChainMap 직접 생성
new_chain3 = ChainMap({}, dict1, dict2)
print(new_chain3.maps)  # [{}, {'a': 1, 'b': 2}, {'b': 3, 'c': 4}]

4. ChainMap 주요 속성 및 메서드

4.1. 주요 속성

from collections import ChainMap

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
chain = ChainMap(dict1, dict2)

# 1. maps 속성 - 매핑 목록 확인
print(chain.maps)  # [{'a': 1, 'b': 2}, {'b': 3, 'c': 4}]

# 2. parents 속성 - 첫 번째 맵을 제외한 ChainMap 반환
print(chain.parents)  # ChainMap({'b': 3, 'c': 4})
print(chain.parents.maps)  # [{'b': 3, 'c': 4}]

4.2. 주요 메서드

ChainMap의 주요 메서드와 각 기능.

  • new_child(m=None): 새 맵을 체인 앞쪽에 추가한 새 ChainMap 반환. 인자 없이 호출 시 빈 딕셔너리 추가.
  • keys(), items(), values(): 중복이 제거된 모든 키/항목/값의 뷰 객체 반환. 첫 번째 발견된 값만 포함.
  • get(key, default=None): 키가 존재하면 해당 값 반환, 없으면 지정된 기본값 반환.
  • pop(key): 첫 번째 맵에서 키 제거 후 해당 값 반환. 첫 번째 맵에 키가 없으면 KeyError 발생.
  • popitem(): 첫 번째 맵에서 임의의 키-값 쌍 제거 후 반환. 첫 번째 맵이 비어있으면 KeyError 발생.
  • clear(): 첫 번째 맵의 모든 항목 제거. 다른 맵들은 영향 없음.

5. ChainMap 실제 사용 사례

5.1. 계층적 구성 관리

애플리케이션 설정을 여러 계층으로 관리하는 방법.

  • 기본 설정: 애플리케이션의 기본값 제공. 가장 낮은 우선순위.
  • 환경 변수: 시스템 환경에 따른 설정 제공. 중간 우선순위.
  • 명령줄 인수: 사용자가 직접 지정한 설정. 가장 높은 우선순위.
  • ChainMap으로 세 계층을 결합하여 통합된 설정 뷰 제공.
  • 각 설정 소스의 무결성 유지하면서 우선순위에 따른 값 탐색 가능.
from collections import ChainMap
import os
import argparse

# 1. 명령줄 인수 파싱
def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('--debug', dest='debug', action='store_true', default=False)
    parser.add_argument('--verbose', dest='verbose', action='store_true', default=False)
    return parser.parse_args()

# 2. 설정 계층 구성
def get_config():
    # 기본 설정
    defaults = {
        'debug': False, 
        'verbose': False,
        'host': 'localhost',
        'port': 8000,
        'timeout': 30
    }

    # 환경 변수에서 설정 (환경 변수 이름을 대문자로 변환)
    env = {k.lower(): v for k, v in os.environ.items() 
           if k.lower() in defaults}

    # 명령줄 인수에서 설정
    args = vars(parse_args())
    args = {k: v for k, v in args.items() if v is not None}

    # ChainMap으로 통합 (우선순위: 명령줄 > 환경 변수 > 기본값)
    return ChainMap(args, env, defaults)

# 3. 사용 예시
# config = get_config()
# print(f"Debug mode: {config['debug']}")
# print(f"Server running at {config['host']}:{config['port']} with {config['timeout']}s timeout")

5.2. 중첩된 스코프 구현

프로그래밍 언어의 스코프 체인 구현 방법.

  • 전역 스코프: 모든 코드에서 접근 가능한 변수 저장. 가장 바깥쪽 스코프.
  • 외부 함수 스코프: 외부 함수 내에서 정의된 변수 저장. 중간 스코프.
  • 내부 함수 스코프: 가장 안쪽 함수에서 정의된 변수 저장. 가장 안쪽 스코프.

ChainMap으로 이러한 스코프를 계층적으로 연결하여 변수 탐색 구현.

실제 프로그래밍 언어의 변수 검색 메커니즘과 유사한 방식으로 작동.

from collections import ChainMap

# 간단한 심볼 테이블 스코프 구현
def create_scope_chain():
    # 전역 스코프 정의
    global_scope = {'x': 10, 'y': 20, 'print': print}

    # 외부 함수 스코프
    outer_scope = {'x': 30, 'z': 40}

    # 내부 함수 스코프
    inner_scope = {'x': 50, 'w': 60}

    # 스코프 체인 생성 (안쪽 스코프가 우선순위 높음)
    return ChainMap(inner_scope, outer_scope, global_scope)

# 변수 조회 예시
scope = create_scope_chain()
print(f"x: {scope['x']}")  # 50 (inner_scope)
print(f"y: {scope['y']}")  # 20 (global_scope)
print(f"z: {scope['z']}")  # 40 (outer_scope)
print(f"w: {scope['w']}")  # 60 (inner_scope)

# 스코프 체인에서 심볼 찾기
def find_symbol(symbol, scope_chain):
    if symbol in scope_chain:
        value = scope_chain[symbol]
        # 어느 스코프에서 찾았는지 확인
        for i, scope in enumerate(scope_chain.maps):
            if symbol in scope:
                scope_name = ['inner', 'outer', 'global'][i]
                return f"'{symbol}' found in {scope_name} scope with value {value}"
    return f"'{symbol}' not found in any scope"

print(find_symbol('x', scope))  # 'x' found in inner scope with value 50
print(find_symbol('y', scope))  # 'y' found in global scope with value 20

5.3. 컨텍스트 관리

임시적인 변경을 격리하는 컨텍스트 관리 패턴 구현.

  • 기본 설정: 애플리케이션의 기본 상태 저장.
  • 임시 설정: 특정 컨텍스트에서만 적용되는 임시 변경사항 저장.
  • ChainMap의 new_child()를 활용하여 임시 컨텍스트 생성.
  • 컨텍스트 종료 시 이전 상태로 복원하는 메커니즘 구현.
  • 격리된 환경에서의 안전한 변경 및 원래 상태 보존 가능.
from collections import ChainMap
import contextlib

class SettingsContext:
    def __init__(self):
        self.settings = ChainMap({})  # 기본 빈 맵으로 시작

    @contextlib.contextmanager
    def temp_settings(self, **kwargs):
        # 새로운 설정 맵을 체인의 앞에 추가
        new_settings = self.settings.new_child(kwargs)
        old_settings = self.settings
        self.settings = new_settings
        try:
            yield self.settings
        finally:
            # 컨텍스트 종료 시 이전 설정으로 복원
            self.settings = old_settings

    def get(self, key, default=None):
        return self.settings.get(key, default)

    def __getitem__(self, key):
        return self.settings[key]

    def __setitem__(self, key, value):
        self.settings.maps[0][key] = value

# 사용 예시
context = SettingsContext()
context['theme'] = 'light'
context['font_size'] = 12

print(f"기본 설정 - 테마: {context['theme']}, 폰트 크기: {context['font_size']}")

# 임시 설정 변경
with context.temp_settings(theme='dark', debug=True):
    print(f"임시 설정 - 테마: {context['theme']}, 폰트 크기: {context['font_size']}, 디버그: {context.get('debug')}")

# 원래 설정으로 복원
print(f"복원된 설정 - 테마: {context['theme']}, 폰트 크기: {context['font_size']}")
print(f"디버그 설정 존재?: {'debug' in context.settings}")

6. ChainMap vs 다른 자료구조 비교

6.1. ChainMap vs 딕셔너리 병합

ChainMap과 딕셔너리 병합 방식의 비교.

  • 병합 방식: 딕셔너리 병합은 모든 키-값 쌍을 새 딕셔너리로 복사하는 과정.
  • 생성 속도: ChainMap은 참조만 저장하므로 병합보다 빠른 생성 속도.
  • 메모리 사용: ChainMap은 원본 딕셔너리 참조만 유지하여 메모리 효율성 우수.
  • 동적 업데이트: 원본 딕셔너리 변경 시 ChainMap에 자동 반영, 병합은 재병합 필요.
  • 키 조회 성능: 병합된 딕셔너리는 단일 해시 테이블 조회로 더 빠른 키 접근 가능.
  • 대규모 데이터 처리 시 ChainMap의 생성 속도가 병합보다 수십에서 수백 배 빠른 성능 차이.

6.2. ChainMap vs 중첩 사전(Nested Dictionary)

ChainMap과 중첩 딕셔너리 구조의 비교.

  • 데이터 구조: 중첩 딕셔너리는 계층적 트리 구조, ChainMap은 평면적 맵의 연결 구조.
  • 키 접근 방식: 중첩 딕셔너리는 다중 레벨 접근(dict[section][key]), ChainMap은 단일 레벨 접근(chain[key]).
  • 우선순위 처리: 중첩 딕셔너리는 명시적 우선순위 함수 필요, ChainMap은 자동 우선순위 처리.
  • 동적 업데이트: 두 방식 모두 원본 딕셔너리 변경 시 반영 가능.
  • 코드 간결성: ChainMap이 보다 깔끔하고 직관적인 코드 작성 가능.
  • 유지보수성: ChainMap 방식이 로직 분리와 명확한 우선순위 체계로 더 나은 유지보수성 제공.

7. 정리: ChainMap의 장점과 한계

7.1. 장점:

  • 여러 딕셔너리의 통합된 뷰 제공
  • 원본 딕셔너리 변경 시 자동 반영
  • 딕셔너리 병합보다 빠른 생성 속도
  • 계층적 데이터 관리에 이상적
  • 메모리 효율성 (데이터 복사 없음)

7.2. 한계:

  • 키 중복 시 첫 번째 맵의 값만 사용 가능
  • 직접 수정 시 첫 번째 맵에만 적용
  • 딕셔너리 병합보다 키 조회가 약간 느릴 수 있음
  • 딕셔너리만큼 익숙하지 않은 API

8. 마무리

  • ChainMap은 여러 딕셔너리를 논리적으로 결합하여 단일 매핑처럼 사용할 수 있게 해주는 유용한 자료구조.
  • 특히 설정 관리, 스코프 체인 구현, 계층적 데이터 작업에 매우 적합.
  • 원본 매핑의 무결성을 유지하면서 동적으로 변화하는 다중 계층 데이터를 효율적으로 처리하는데 큰 장점 보유.

단순히 딕셔너리를 병합하는 경우에는 일반 딕셔너리 연산이 더 직관적일 수 있으나,
동적이고 계층적인 데이터 구조를 다룰 때는 ChainMap이 보다 나을 수 있음.


같이보면 좋은 자료들

2025.04.04 - [Python] - [Py] collections 모듈 (summary) - 작성중

 

[Py] collections 모듈 (summary) - 작성중

Python의 collections 모듈은 파이썬의 built-in 자료구조를 확장한 special container 클래스들을 제공함.1. Counter요소의 개수를 세는 dictionary의 subclass.해시 가능한 객체의 카운트를 저장함.from collections impor

ds31x.tistory.com