본문 바로가기

Python study

딕셔너리(dictionary)와 집합(set)

일반적인 매핑형

매핑형(mapping types)은 키와 값을 연결시켜 데이터를 저장하는 파이썬의 데이터 구조 중 하나이다. 이런 형태의 자료형에서는 각 키가 고유해야 하며, 키를 통해 해당 값에 접근할 수 있다.

파이썬에서 가장 일반적인 매핑형은 dict (딕셔너리)입니다. 딕셔너리는 키와 값의 쌍으로 데이터를 저장하며, 중괄호 {}를 사용하여 표현한다.

person = {"name": "John", "age": 30}

print(person["name"])  # 출력: John
print(person["age"])   # 출력: 30

 

파이썬의 딕셔너리 주요 특징

  • 동적 변경 가능: 딕셔너리는 실행 중에 항목의 추가, 삭제, 수정이 자유롭다.
  • 순서 유지: 파이썬 3.7 이상에서는 딕셔너리가 항목을 추가한 순서를 유지한다.
  • 다양한 데이터 타입 지원: 딕셔너리의 키는 불변(immutable) 타입이어야 하지만, 값은 어떠한 데이터 타입도 가능하다.

딕셔너리 외에도 파이썬 3.8 이상에서는 collections.abc.Mapping을 상속받는 다른 매핑형도 사용할 수 있다. 이는 더 특화된 기능을 제공하는 매핑형을 만들고자 할 때 유용하다.

 

collections.abc.Mapping

파이썬의 표준 라이브러리인 collections.abc 모듈에 있는 추상 기본 클래스(ABC)이다. 이 클래스는 매핑형 데이터 구조를 만들 때 사용되는 추상화를 제공한다. 개발자는 이 추상 클래스를 상속받아 필요한 메소드를 구현함으로써 자신만의 맞춤형 매핑형을 생성할 수 있다.

Mapping 클래스는 dict와 같은 매핑형의 기본적인 인터페이스를 정의한다. 예를 들어, __getitem__, __iter__, __len__ 등의 메소드를 구현해야 하며, 이를 통해 키-값 쌍을 저장하고, 반복하고, 길이를 측정할 수 있다.

collections.abc.Mapping을 활용하면 매핑형 데이터 구조를 구현할 때 필요한 표준 동작을 보장할 수 있으므로, 코드의 일관성과 호환성을 유지할 수 있다. 이렇게 만들어진 매핑형은 파이썬의 다른 표준 매핑형과 같이 동작하며, 키를 통해 값에 접근하거나 매핑의 크기를 알아내는 등의 작업을 수행할 수 있다.

 

지능형 딕셔너리(Dictionary comprehension)

리스트, 세트, 제너레이터 컴프리헨션과 유사한 문법을 사용하여 딕셔너리를 간결하게 생성할 수 있는 파이썬의 기능이다. 이를 통해 기존 딕셔너리나 다른 시퀀스로부터 새로운 딕셔너리를 효율적으로 생성할 수 있으며, 코드를 더 간결하고 읽기 쉽게 만들 수 있다.

지능형 딕셔너리는 중괄호 {} 안에 키와 값의 쌍을 콜론 :으로 구분하여 쓰고, for 루프와 선택적으로 if 조건문을 사용한다.

# 기본 구조
# {키: 값 for 아이템 in 시퀀스 if 조건}

# 기본 사용법
numbers = [1, 2, 3, 4]
squared_cubed = {x: x**3 for x in numbers}
print(squared_cubed)

# 조건을 포함한 사용법
even_square = {x**2: x for x in numbers if x % 2 == 0}
print(even_square)

공통적인 매핑 메서드

파이썬의 딕셔너리와 같은 매핑형 자료구조에서 사용할 수 있는 공통적인 메서드들은 데이터를 조작하고 정보를 얻는 데에 유용하다. 아래는 파이썬의 매핑형에서 자주 사용되는 몇 가지 기본적인 메서드들을 설명한다:

1. get(key, default=None)

  • 지정된 키에 대응하는 값을 반환한다. 키가 딕셔너리에 없는 경우 default 값을 반환한다.
  • 예: value = dict.get('key', 'default_value')

2. setdefault(key, default=None)

  • key가 딕셔너리에 있으면, 해당 키의 값을 반환한다.
  • key가 없으면, key와 default 값을 딕셔너리에 삽입하고, default를 반환한다.
  • 예: value = dict.setdefault('new_key', 'default_value')

3. keys()

  • 딕셔너리의 키를 뷰 객체로 반환한다. 이 뷰는 딕셔너리의 키 컬렉션을 반영하며, 딕셔너리가 변경될 때 뷰도 함께 갱신된다.
  • 예: keys = dict.keys()

4. values()

  • 딕셔너리의 값들을 뷰 객체로 반환한다. 이 뷰는 딕셔너리의 값 컬렉션을 반영하며, 딕셔너리가 변경될 때 뷰도 함께 갱신된다.
  • 예: values = dict.values()

5. items()

  • 딕셔너리의 키-값 쌍을 (키, 값) 형태의 튜플로 포함하는 뷰 객체를 반환한다.
  • 예: items = dict.items()

6. pop(key, default)

  • 지정된 키에 해당하는 값을 제거하고 그 값을 반환한다. 키가 딕셔너리에 없으면 default를 반환합니다. default가 제공되지 않고 키가 없는 경우 KeyError가 발생합니다.
  • 예: value = dict.pop('key', 'default_value')

7. popitem()

  • 딕셔너리에서 하나의 키-값 쌍을 제거하고 그 쌍을 (키, 값) 형태로 반환한다. 이 메서드는 LIFO(Last In, First Out) 방식으로 작동한다.
  • 예: key, value = dict.popitem()

8. clear()

  • 딕셔너리의 모든 항목을 제거한다.
  • 예: dict.clear()

9. update([other])

  • 다른 딕셔너리나 키-값 쌍의 반복 가능한 객체로부터 키-값 쌍을 받아서 기존 딕셔너리를 업데이트한다.
  • 예: dict.update({'key': 'value'}) 또는 dict.update(new_key='new_value')

 

setdefault() 메서드

이 메서드는 딕셔너리에 키가 존재하는지 확인하고, 키가 존재하지 않을 경우 키와 함께 기본값을 딕셔너리에 추가한 후 그 값을 반환한다. 존재하는 키에 대해서는 해당 키의 값을 반환한다.

# 기본 사용법
# value = dictionary.setdefault(key, default_value)

# 사람의 이름과 이메일이 저장된 딕셔너리
emails = {
    "John Doe": "johndoe@example.com",
    "Jane Smith": "janesmith@example.com"
}

# "Alice Johnson"의 이메일을 조회하되, 존재하지 않는다면 기본 이메일을 설정
email = emails.setdefault("Alice Johnson", "default@example.com")
print(email)  # 출력: default@example.com

# 이제 emails 딕셔너리를 확인하면 "Alice Johnson"이 추가된다.
print(emails)
# 출력: {'John Doe': 'johndoe@example.com', 'Jane Smith': 'janesmith@example.com', 'Alice Johnson': 'default@example.com'}

 

defaultdic

defaultdict는 파이썬의 collections 모듈에서 제공하는 특수한 딕셔너리 유형이다. 이 자료구조는 딕셔너리에 없는 키를 조회할 때 자동으로 기본값을 생성하여 반환하는 기능을 제공한다. defaultdict는 일반적인 딕셔너리(dict)와 비슷하게 동작하지만, 미리 정의된 기본값 생성 함수를 이용해 존재하지 않는 키에 대해 자동으로 처리해준다. 이는 매핑에서 융통성 있는 키 처리를 가능하게 해 주며, 특히 여러 작업을 자동화하고 싶을 때 유용하다.

defaultdict의 사용법

from collections import defaultdict
# 리스트를 기본값으로 사용하는 defaultdict 생성
string_list_map = defaultdict(list)

# "fruits" 키에 리스트 값 추가
string_list_map['fruits'].append('apple')
string_list_map['fruits'].append('banana')

# "vegetables" 키가 없으므로 자동으로 빈 리스트가 생성되고, 그 뒤에 값이 추가됨
string_list_map['vegetables'].append('carrot')

print(string_list_map)  # 출력: defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})

기본값 생성 함수

기본값 생성 함수는 매우 다양하게 설정할 수 있다. 예를 들어, 기본값으로 0, 빈 리스트, 빈 문자열, 사용자 정의 객체 등이 있을 수 있다. 함수로는 내장 함수인 int, list, set 등을 사용할 수 있고, 람다나 다른 사용자 정의 함수도 가능하다.

defaultdict은 특히 다음과 같은 상황에서 유용하다:

  • 여러 항목을 그룹화하여 리스트에 저장할 때
  • 각 키에 대한 카운트를 자동으로 관리할 때 (기본값으로 int 사용)
  • 키에 대응하는 복잡한 초기화가 필요한 객체를 자동으로 생성할 때

defaultdict를 사용하면 코드를 더 간결하고 효율적으로 작성할 수 있으며, 예외 처리를 최소화할 수 있다.

 

mising() 메소드

__missing__() 메서드는 파이썬의 dict 클래스를 상속받은 서브클래스에서 정의할 수 있는 특별한 메서드이다. 이 메서드는 딕셔너리에서 키를 조회했을 때 키가 존재하지 않는 경우 자동으로 호출된다. defaultdict에서 보이는 자동 기본값 생성 기능과 유사하게 동작하도록 사용자 정의 딕셔너리에서 이 메서드를 구현할 수 있다.

__missing__() 메서드는 딕셔너리의 __getitem__() 메서드에 의해 호출되며, 딕셔너리에 존재하지 않는 키에 대한 처리를 커스터마이징하고자 할 때 유용하다. 기본적으로 dict 클래스에는 __missing__() 메서드가 구현되어 있지 않기 때문에, 이 메서드를 정의하려면 딕셔너리를 상속받은 서브클래스를 만들어야 한다.

class CustomDict(dict):
    def __missing__(self, key):
        # 존재하지 않는 키에 대한 처리
        return f"{key} is not in the dictionary"

# CustomDict 인스턴스 생성
my_dict = CustomDict({'a': 1, 'b': 2})

# 존재하는 키
print(my_dict['a'])  # 출력: 1

# 존재하지 않는 키
print(my_dict['c'])  # 출력: c is not in the dictionary

 

그 외 매핑형들

파이썬에서 dict 이외에도 여러 다양한 매핑형이 있으며, 이들은 특정 사용 사례에 최적화되어 있거나 추가적인 기능을 제공한다. 여기에는 collections 모듈의 몇 가지 유용한 매핑형과 기타 관련된 매핑형들이 포함된다.

1. collections.OrderedDict

  • OrderedDict는 항목이 추가된 순서를 기억하는 딕셔너리이다. Python 3.7 이상에서는 기본 dict도 삽입 순서를 유지하지만, OrderedDict는 이전 버전과의 호환성을 위해 여전히 유용하다. 또한, OrderedDict는 항목의 순서를 변경하는 메서드를 제공하여, 이를 통해 순서를 재조정할 수 있다.

2. collections.defaultdict

  • 이미 설명한 바와 같이, defaultdict는 존재하지 않는 키에 대한 기본값을 자동으로 할당하는 딕셔너리이다. 이는 주로 그룹핑, 카운팅 등에서 유용하게 사용된다.

3. collections.ChainMap

  • ChainMap은 여러 딕셔너리를 하나의 뷰로 결합한다. 이 매핑형은 각각의 딕셔너리를 체인으로 연결하여 하나처럼 보이게 하며, 삽입, 업데이트, 삭제 등의 작업이 첫 번째 매핑에 영향을 미친다. 이는 예를 들어, 명령줄 인수, 환경 변수 등과 같은 여러 소스의 설정을 오버레이 할 때 유용하다.

4. collections.Counter

  • Counter는 요소의 개수를 카운팅하는 특수한 딕셔너리이다. 주로 요소의 출현 빈도를 세는 데 사용되며, 각 요소를 키로, 그 요소의 카운트를 값으로 저장한다.

5. types.MappingProxyType

  • MappingProxyType을 사용하면 기존 딕셔너리에 대한 읽기 전용 프록시를 생성할 수 있다. 이는 딕셔너리의 데이터를 변경하지 않고 안전하게 공유할 때 유용하다. (아래 추가 설명 有)
from collections import OrderedDict, defaultdict, ChainMap, Counter
from types import MappingProxyType

# OrderedDict 예시
ordered = OrderedDict([('a', 1), ('b', 2)])
ordered.move_to_end('a')  # 키 'a'를 마지막으로 이동
print(ordered)

# defaultdict 예시
default = defaultdict(int)
default['a'] += 1
print(default)

# ChainMap 예시
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
chain = ChainMap(dict1, dict2)
print(chain['b'])  # 출력: 2 (첫 번째 매핑에서 'b'의 값)

# Counter 예시
counter = Counter('banana')
print(counter)  # 출력: Counter({'a': 3, 'b': 1, 'n': 2})

# MappingProxyType 예시
original = {'a': 1}
proxy = MappingProxyType(original)
print(proxy['a'])  # 출력: 1
# proxy['a'] = 2  # TypeError: 'mappingproxy' object does not support item assignment

 

UserDict 상속

UserDict는 파이썬의 collections 모듈에서 제공하는 클래스로, 사용자가 딕셔너리 유형의 데이터 구조를 쉽게 확장하고 사용자 정의할 수 있도록 설계된 래퍼 클래스이다. UserDict는 내부적으로 실제 데이터를 저장하기 위해 일반 딕셔너리를 사용한다. 이 클래스를 상속하여 사용자 정의 딕셔너리를 만드는 것은 dict 클래스를 직접 상속하는 것보다 더 간단하고 안전할 수 있다. UserDict는 dict의 모든 메소드를 상속하며, 추가로 메소드를 오버라이드하거나 새로운 메소드를 추가할 수 있다.

from collections import UserDict

class MyDict(UserDict):
    def __init__(self, initial_data={}):
        super().__init__(initial_data)
    
    # 예제: 값을 설정할 때 항상 문자열로 변환하는 기능 추가
    def __setitem__(self, key, value):
        super().__setitem__(key, str(value))
    
    # 키가 없는 경우에 대한 처리 추가
    def __missing__(self, key):
        return f"{key} not found!"

# MyDict 인스턴스 생성 및 사용
my_dict = MyDict()
my_dict['alpha'] = 123
print(my_dict['alpha'])  # 출력: '123'
print(my_dict['beta'])   # 출력: 'beta not found!'

 

UserDict 사용하는 이유

  • 상속 단순화: dict를 직접 상속받을 때는 특별한 메소드들이 기대하는 방식으로 동작하지 않을 수 있다 (예: dict.__setitem__ 호출이 일부 메소드에서 무시될 수 있음). UserDict를 사용하면 이런 문제를 피할 수 있다.
  • 사용자 정의 용이성: UserDict는 내부 딕셔너리(data)에 모든 데이터를 저장하므로, 사용자 정의 메소드를 추가하거나 변경할 때 이를 더 쉽게 조작할 수 있다.

주의사항

  • UserDict를 사용할 때는 항상 self.data에 접근하여 내부 딕셔너리에 저장되어 있는 실제 데이터를 조작하게 된다. 이를 잘 활용하면 매우 효율적으로 딕셔너리 기능을 확장할 수 있다.
  • UserDict는 파이썬의 표준 딕셔너리처럼 다양한 딕셔너리 메소드를 지원하므로, 일반적인 딕셔너리 사용 방법과 크게 다르지 않다.

 

Userdict 클레스의 mutablemapping 상속

UserDict 클래스가 collections.abc.MutableMapping을 상속받는다는 것은 UserDict가 MutableMapping 추상 기본 클래스(ABC)의 모든 메소드와 특성을 구현하고 있다는 의미이다. 이러한 상속 구조는 **UserDict**가 매핑형 인터페이스를 완벽하게 지원하며, 딕셔너리처럼 동작하도록 보장한다.

MutableMapping 이해하기

collections.abc 모듈에 있는 MutableMapping은 매핑형의 추상 기본 클래스로, 매핑형이 가져야 할 기본적인 동작과 메소드를 정의한다. 이 ABC는 매핑형 데이터 구조가 가져야 할 주요 메소드를 구현하도록 요구함으로써, 사용자 정의 매핑형이 일관된 인터페이스와 동작을 갖출 수 있도록 한다.

MutableMapping에서 요구하는 메소드는 다음과 같다:

  • __getitem__: 특정 키에 대한 값을 조회한다.
  • __setitem__: 특정 키에 값을 할당한다.
  • __delitem__: 특정 키와 그 값을 매핑에서 제거한다.
  • __iter__: 매핑의 키를 반복자로 반환한다.
  • __len__: 매핑에 저장된 키-값 쌍의 수를 반환한다.

이외에도 MutableMapping은 다양한 유틸리티 메소드를 제공하는데, 이는 사용자가 매핑형을 보다 편리하게 활용할 수 있도록 돕는다. 예를 들어, keys(), items(), values(), clear(), update(), pop(), popitem(), setdefault() 등이 자동으로 제공된다.

UserDict의 역할

UserDict는 이러한 MutableMapping의 요구사항을 충족시키면서, 또한 사용자가 dict와 유사한 방식으로 클래스를 쉽게 확장하고 사용자 정의할 수 있도록 추가적인 구현과 유연성을 제공한다. UserDict 내부에는 실제 데이터를 저장하는 self.data 딕셔너리가 있으며, 이를 통해 dict의 기능을 모방하고 확장한다.

UserDict를 사용하는 주된 이유 중 하나는 dict를 직접 상속받는 것이 일부 상황에서 예상치 못한 문제를 일으킬 수 있기 때문이다. 예를 들어, 직접 상속받은 클래스에서 특정 메소드를 오버라이드할 때 내부적으로 기대하는 메소드 호출이 무시될 수 있다. UserDict는 이런 문제를 해결하고, 보다 안정적이고 예측 가능한 방식으로 사용자 정의 딕셔너리를 구현할 수 있게 돕는다.

 

불변 매핑(immutable mapping)

불변 매핑(immutable mapping)은 한 번 생성된 후에는 수정이 불가능한 매핑형 자료구조를 의미한다. 파이썬에서는 dict와 같은 표준 매핑형 자료구조가 변경 가능하며(mutable), 새로운 항목을 추가하거나 기존 항목을 수정 또는 삭제할 수 있다. 하지만, 불변 매핑은 이러한 변경이 허용되지 않는다.

types.MappingProxyType

파이썬에서 불변 매핑을 구현할 때 사용할 수 있는 표준 방법 중 하나는 types 모듈의 MappingProxyType을 사용하는 것이다. MappingProxyType은 주어진 딕셔너리에 대한 읽기 전용의 프록시를 생성하여, 원본 딕셔너리의 내용을 변경할 수 없도록 한다.

from types import MappingProxyType

writable = {'one': 1, 'two': 2}
read_only = MappingProxyType(writable)

# 값을 읽는 것은 가능
print(read_only['one'])  # 출력: 1

# 값을 변경하려고 시도하면 오류 발생
# read_only['one'] = 11  # TypeError: 'mappingproxy' object does not support item assignment

# 새 키를 추가하려고 시도하면 오류 발생
# read_only['three'] = 3  # TypeError: 'mappingproxy' object does not support item assignment

MappingProxyType은 내부적으로 원본 딕셔너리를 참조하므로, 원본 딕셔너리가 변경되면 프록시를 통해 그 변경사항을 볼 수 있다. 하지만 프록시 객체 자체를 통해 변경을 시도할 수는 없다.

불변 매핑의 이점

  1. 스레드 안전성: 변경이 불가능하기 때문에, 여러 스레드가 동시에 접근하더라도 문제가 발생하지 않는다.
  2. 사이드 이펙트 방지: 함수나 메서드에 매핑을 전달할 때, 호출된 함수/메서드에서 그 매핑을 변경할 위험이 없어진다.
  3. 메모리 사용 최적화: 일부 상황에서 불변 객체는 메모리 사용을 최적화할 수 있으며, 동일한 객체를 여러 참조에서 안전하게 공유할 수 있다.

MappingProxyType을 사용하는 것은 기존 딕셔너리 기능을 유지하면서도 불변성을 보장하고자 할 때 매우 유용하다. 이를 통해 프로그램의 안정성과 예측 가능성을 높일 수 있다.

 

집합 이론(set & frozenset)

파이썬에서 set과 frozenset은 집합을 표현하는 내장 데이터 타입이다. 이들은 수학적인 집합 이론을 구현하며, 중복된 요소를 허용하지 않고, 요소들 사이의 순서도 유지하지 않는다. 이러한 특성 때문에 집합은 주로 멤버십 테스트, 중복 항목 제거, 합집합, 교집합, 차집합 등의 연산을 수행하는 데 유용하다.

set

set은 가변 집합으로, 생성 후에 요소를 추가하거나 제거할 수 있다. set은 중괄호 {} 또는 set() 함수를 사용하여 생성할 수 있으며, 중괄호를 사용할 때는 적어도 하나의 요소가 초기에 포함되어야 한다.

set의 주요 메서드와 연산

  • add(element): 집합에 요소를 추가한다.
  • remove(element): 집합에서 요소를 제거한다. 요소가 집합에 없으면 **KeyError**를 발생시킨다.
  • discard(element): 집합에서 요소를 제거한다. 요소가 집합에 없어도 오류가 발생하지 않는다.
  • clear(): 집합의 모든 요소를 제거합니다.
  • union(), intersection(), difference(), symmetric_difference(): 각각 합집합, 교집합, 차집합, 대칭 차집합을 계산한다.
a = {1, 2, 3}
b = {3, 4, 5}

# 합집합
print(a | b)  # {1, 2, 3, 4, 5}

# 교집합
print(a & b)  # {3}

# 차집합
print(a - b)  # {1, 2}

# 대칭 차집합
print(a ^ b)  # {1, 2, 4, 5}

frozenset

frozenset은 불변 집합으로, 한 번 생성되면 수정할 수 없다. frozenset은 frozenset() 함수를 사용하여 생성하며, 이 집합은 추가나 삭제와 같은 변형 연산을 지원하지 않는다.

fs1 = frozenset([1, 2, 3])
fs2 = frozenset([3, 4, 5])

# 합집합
print(fs1 | fs2)  # frozenset({1, 2, 3, 4, 5})

# 교집합
print(fs1 & fs2)  # frozenset({3})

# 차집합
print(fs1 - fs2)  # frozenset({1, 2})

# 대칭 차집합
print(fs1 ^ fs2)  # frozenset({1, 2, 4, 5})

set과 frozenset은 둘 다 집합 연산에 유용하지만, set은 동적인 작업에 적합하고 frozenset은 변경이 불가능하여 딕셔너리의 키나 다른 집합의 요소로 사용할 수 있는 이점이 있다.

 

집합 리터럴

집합 리터럴은 파이썬에서 집합을 직접 선언하는 방식이다. 집합 리터럴을 사용하려면 중괄호 {} 안에 쉼표로 구분된 요소들을 넣는다. 집합 리터럴은 set 객체를 생성하는 가장 간결하고 직관적인 방법 중 하나이다.

# 기본적인 사용법
my_set = {1, 2, 3, 4, 5}
print(my_set)  # 출력 예시: {1, 2, 3, 4, 5}

# 빈 집합 생성
empty_set = set()
print(empty_set)  # 출력: set()

# 집합 리터럴과 연산
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# 합집합
union_set = a | b
print(union_set)  # 출력: {1, 2, 3, 4, 5, 6}

# 교집합
intersection_set = a & b
print(intersection_set)  # 출력: {3, 4}

# 차집합
difference_set = a - b
print(difference_set)  # 출력: {1, 2}

# 대칭 차집합
symmetric_difference_set = a ^ b
print(symmetric_difference_set)  # 출력: {1, 2, 5, 6}

 

지능형 집합 (Set comprehension)

지능형 집합(Set comprehension)은 파이썬에서 집합을 생성하는 간결하고 강력한 방법이다. 이는 리스트 컴프리헨션(list comprehension)과 유사한 문법을 사용하며, 표현식과 조건문을 통해 집합의 요소를 정의한다. 지능형 집합을 사용하면 코드를 단순화하고, 읽기 쉬우며, 실행도 빠르다.

# 기본 사용법
# {표현식 for 변수 in 반복가능객체 if 조건문}

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
even_numbers = {x for x in numbers if x % 2 == 0}
print(even_numbers)  # 출력: {2, 4, 6, 8}

words = ["apple", "banana", "cherry", "date"]
lengths = {len(word) for word in words}
print(lengths)  # 출력: {5, 6}

squared_numbers = {x**2 for x in range(10) if x**2 > 20}
print(squared_numbers)  # 출력: {25, 36, 49, 64, 81}

지능형 집합의 장점

  • 간결성: 복잡한 로직을 한 줄의 코드로 간결하게 표현할 수 있다.
  • 성능: 내부적으로 최적화되어 있어, 일반적인 루프를 사용하는 것보다 빠를 수 있다.
  • 직관성: 필터링과 변환 작업을 명확하게 설명하는 데 도움이 된다.

 

집합 연산

파이썬의 집합(set) 자료형은 수학의 집합 연산을 지원하여 데이터를 처리할 때 매우 유용하다. 집합 연산을 사용하면 두 집합 간의 합집합, 교집합, 차집합, 대칭 차집합 등을 쉽게 구할 수 있다.

1. 합집합 (Union)

  • 두 집합의 모든 요소를 포함하는 새 집합을 반환합니다.
  • 연산자: |
  • 메서드: .union()

2. 교집합 (Intersection)

  • 두 집합의 공통 요소만 포함하는 새 집합을 반환합니다.
  • 연산자: &
  • 메서드: .intersection()

3. 차집합 (Difference)

  • 첫 번째 집합에는 있지만 두 번째 집합에는 없는 요소들로 구성된 새 집합을 반환합니다.
  • 연산자: ``
  • 메서드: .difference()

4. 대칭 차집합 (Symmetric Difference)

  • 두 집합 중 한쪽에만 속하는 요소들로 구성된 새 집합을 반환합니다.
  • 연산자: ^
  • 메서드: .symmetric_difference()

5. 부분 집합 (Subset)과 상위 집합 (Superset) 확인

  • 부분 집합 여부를 확인합니다 (<=), 상위 집합 여부를 확인합니다 (>=).
  • 메서드: .issubset(), .issuperset()

6. 집합의 요소 추가 및 제거

  • 요소 추가: .add()
  • 요소 제거: .remove() (요소가 없으면 KeyError), .discard() (요소가 없어도 오류 발생하지 않음), .pop() (임의의 요소 제거 및 반환)
# 합집함
a = {1, 2, 3}
b = {3, 4, 5}
print(a | b)  # {1, 2, 3, 4, 5}
print(a.union(b))  # {1, 2, 3, 4, 5}

# 교집합
print(a & b)  # {3}
print(a.intersection(b))  # {3}

# 차집합
print(a - b)  # {1, 2}
print(a.difference(b))  # {1, 2}

# 대칭 차집합
print(a ^ b)  # {1, 2, 4, 5}
print(a.symmetric_difference(b))  # {1, 2, 4, 5}

# 부분 집합, 상위 집합 확인
c = {1, 2}
print(c <= a)  # True
print(c.issubset(a))  # True
print(a >= c)  # True
print(a.issuperset(c))  # True

# 요소 추가 및 제거
a.add(6)
print(a)  # {1, 2, 3, 6}
a.discard(6)
print(a)  # {1, 2, 3}
a.pop()
print(a)  # {2, 3}

 

dict와 set의 내부구조

파이썬의 dict와 set은 고성능을 위해 내부적으로 해시 테이블을 사용한다. 이들의 효율적인 구현은 파이썬 프로그래밍에서 중요한 데이터 구조로 만들어주며, 빠른 조회, 삽입, 삭제 등의 연산을 가능하게 한다.

Dict (딕셔너리)

딕셔너리는 키-값 쌍을 저장하는 매핑형 컨테이너이다. 내부적으로 딕셔너리는 해시 테이블을 사용하여 구현된다. 각 키-값 쌍은 해시 테이블의 '버킷'이라는 곳에 저장되며, 키의 해시 값을 이용하여 해당 버킷의 위치를 결정한다.

작동 방식:

  1. 해싱: 딕셔너리에 요소를 추가할 때, 키의 hash() 함수를 호출하여 해시 값을 계산한다. 이 해시 값은 저장 위치를 결정하는 데 사용된다.
  2. 충돌 해결: 두 개 이상의 키가 동일한 해시 값을 가질 경우 '충돌'이 발생한다. 파이썬은 개방 주소법(open addressing)의 한 형태인 선형 조사(linear probing) 방식을 사용하여 이 문제를 해결한다. 충돌이 발생하면, 파이썬은 해시 테이블에서 다음 가능한 버킷을 찾을 때까지 버킷을 순차적으로 검사한다.
  3. 재해싱: 딕셔너리의 크기가 커지고 로드 팩터(저장된 항목 수 / 버킷 수)가 특정 임계값을 초과하면, 더 큰 크기의 새로운 해시 테이블로 항목들을 이동시키는 '재해싱' 또는 '리사이징'이 발생한다. 이 과정은 모든 키의 해시 값을 다시 계산하고, 새로운 버킷 위치에 항목들을 배치한다.

Set (집합)

집합은 고유한 요소들의 컬렉션으로, 내부적으로도 해시 테이블을 사용하여 구현된다. 집합의 각 요소는 해시 테이블의 키로 저장되며, 값은 사용되지 않는다.

작동 방식:

  1. 해싱과 충돌 해결: 집합의 요소를 추가할 때, 해당 요소의 hash() 함수가 호출되어 해시 값이 생성된다. 집합에서도 충돌 해결을 위해 선형 조사 방식을 사용한다.
  2. 중복 제거: 집합에 요소를 추가할 때, 이미 같은 해시 값과 동일한 요소가 존재한다면 (즉, __eq__ 메서드로 같다고 평가되면), 그 요소는 추가되지 않는다. 이를 통해 집합은 자동으로 중복을 제거한다.
  3. 리사이징: 집합의 크기가 커질 때마다, 내부 해시 테이블도 재해싱을 통해 크기가 조정된다.

효율성: dictset 모두 평균적으로 상수 시간(O(1))의 성능을 제공하는 요소 접근, 추가, 제거를 가능하게 한다. 그러나 최악의 경우(예를 들어, 해싱 충돌이 많을 경우)에는 성능이 선형 시간(O(n))에 도달할 수 있다. 이러한 경우는 드물게 발생하도록 파이썬의 해시 알고리즘이 설계되어 있다.

이렇게 dict와 set의 내부 구조와 작동 방식은 각자의 사용 사례에 맞게 최적화되어 있으며, 데이터를 효과적으로 관리하고 빠른 연산을 지원한다.

 

set과 dict은 성능적으로 빠르고 순서가 없는 이유

파이썬의 set과 dict이 성능적으로 빠르며 왜 순서가 없는지에 대한 질문은, 이 자료 구조들이 내부적으로 어떻게 구현되어 있는지를 이해하는 데서 출발한다. 이 자료 구조들의 핵심은 해시 테이블이라는 데이터 구조를 사용한다는 점이다. 해시 테이블의 특성 때문에 set과 dict은 빠른 성능을 제공하면서도 원소의 순서를 보장하지 않는다.

해시 테이블과 성능

해시 테이블은 키를 해시 함수를 통해 해시값으로 변환하고, 이 해시값을 기반으로 데이터가 저장되는 위치(인덱스)를 결정한다. 이 과정을 통해, 데이터의 추가, 삭제, 검색 등의 연산이 평균적으로 상수 시간(O(1)) 내에 이루어질 수 있습니다. 해시 테이블은 다음과 같은 성능적 이점을 가진다:

  1. 빠른 데이터 접근: 키에 대한 해시값 계산 후, 해당 키가 저장된 위치를 직접 찾아갈 수 있기 때문에 데이터 검색이 매우 빠르다.
  2. 효율적인 메모리 사용: 해시 테이블은 동적으로 크기가 조정되며, 메모리 사용을 최적화할 수 있다.

순서가 없는 이유

해시 테이블은 키의 해시값을 사용하여 데이터의 저장 위치를 결정한다. 이 해시값은 키의 내용에만 기반하기 때문에, 데이터는 메모리 상에서 해시값의 크기에 따라 불규칙하게 퍼진다. 따라서, 다음과 같은 이유로 순서가 없다:

  1. 해시 충돌: 서로 다른 키가 동일한 해시값을 가질 수 있으며, 이 경우 선형 조사와 같은 충돌 해결 방법을 통해 다른 위치에 저장된다. 이 과정에서 원래의 순서가 무시된다.
  2. 재해싱: dict 또는 set의 크기가 커지면, 성능을 유지하기 위해 내부의 해시 테이블 크기를 조정(재해싱)하게 된다. 재해싱 과정에서 모든 키의 위치가 새롭게 조정되며, 이는 원소의 순서를 더욱 불규칙하게 만든다.

순서 보장의 변화

파이썬 3.6부터는 dict가 내부적으로 삽입 순서를 유지하는 방식으로 구현 변경이 이루어졌고, 파이썬 3.7에서는 이 특성이 공식적으로 언어 사양의 일부가 되었다. 이는 해시 테이블과 별개로 삽입 순서를 추적하는 추가적인 데이터 구조를 사용함으로써 가능해졌다. 하지만 set은 여전히 순서를 유지하지 않는다.

이렇게 set과 dict의 내부 구조는 빠른 성능을 제공하면서도, 원래의 순서를 유지하지 않는 특성을 가진다. 순서가 필요한 경우 OrderedDict나 list와 같은 다른 자료 구조를 사용할 수 있다.

 

딕셔너리 안의 해시 테이블

파이썬의 딕셔너리(dict)가 해시 테이블을 사용하는 방식에 대해 좀 더 자세히 설명하겠다. 파이썬 딕셔너리의 내부 구현은 효율적인 키-값 쌍의 저장 및 검색을 위해 설계되었으며, 이는 대부분의 작업을 평균적으로 상수 시간(O(1))에 수행할 수 있게 한다.

해시 테이블이란?

해시 테이블은 키-값 쌍을 저장하는 데이터 구조로, 각 키에 대해 해시 함수를 사용하여 계산된 해시값을 기반으로 키-값 쌍을 저장할 위치(인덱스)를 결정한다. 이 해시 함수는 키를 테이블 내의 인덱스로 매핑한다.

파이썬 딕셔너리의 해시 테이블 작동 방식

  1. 해시 함수 사용: 딕셔너리에 새로운 키-값 쌍을 추가할 때, 먼저 키의 hash() 함수가 호출되어 해시값이 생성된다. 이 해시값은 저장 위치를 결정하는 데 사용된다.
  2. 해시 충돌 처리: 두 개 이상의 키가 동일한 해시값을 가질 수 있는데, 이를 해시 충돌이라고 합니다. 파이썬은 충돌을 해결하기 위해 개방 주소법 중 하나인 선형 조사(linear probing) 방법을 사용한다. 즉, 충돌이 발생하면 해시 테이블에서 다음 가능한 위치를 찾을 때까지 인덱스를 순차적으로 증가시킨다.
  3. 리사이징: 딕셔너리의 요소가 증가하여 로드 팩터(요소의 수 대비 테이블의 크기)가 특정 임계값을 초과하면, 성능 유지를 위해 해시 테이블의 크기를 증가시키고 모든 요소를 새 테이블로 재해싱한다.

해시 테이블의 장점

  • 빠른 접근 속도: 키를 통해 직접 값을 찾을 수 있으므로 검색, 삽입, 삭제 등이 빠르다.
  • 데이터 관리 효율성: 해시 테이블은 키에 따른 빠른 접근을 제공하여 대용량 데이터 관리에 적합하다.

해시 테이블의 단점

  • 메모리 소비: 해시 테이블은 빠른 접근 속도를 위해 추가적인 메모리 공간을 소비할 수 있다.
  • 최악의 경우 성능: 매우 나쁜 해시 함수를 사용하거나 특정 패턴의 키가 사용될 경우, 해시 충돌이 빈번하게 발생하여 성능이 선형적으로 저하될 수 있다.

파이썬 딕셔너리는 이러한 해시 테이블의 특성을 활용하여 매우 효율적인 키-값 저장 및 접근 방법을 제공한다. 이로 인해 파이썬에서 딕셔너리는 데이터를 조작하고 관리하는 데 매우 강력하고 유연한 도구가 된다.

 

해시와 동치성

해시와 동치성은 컴퓨터 프로그래밍과 데이터 구조에서 매우 중요한 개념이다. 특히 파이썬 같은 고급 프로그래밍 언어에서는 해시 함수와 동치성 비교가 데이터 구조의 효율적인 운용을 위해 필수적으로 사용된다. 이 두 개념을 이해하는 것은 집합, 딕셔너리 같은 자료 구조를 효과적으로 사용하고, 데이터를 효율적으로 관리하는 데 도움이 된다.

해시 (Hashing)

해시는 어떤 데이터(값, 객체 등)를 대표하는 정수 값을 생성하는 과정이다. 이 정수 값은 보통 데이터를 효율적으로 저장하고 검색하기 위한 목적으로 사용된다. 파이썬에서 모든 객체는 __hash__() 메소드를 구현함으로써 해시 가능(hashable)하게 만들어진다. 해시 가능하다는 것은 해당 객체가 변경 불가능하며, 생명주기 동안 동일한 해시 값을 유지한다는 의미이다. 예를 들어, 튜플은 해시 가능하지만, 리스트는 해시 불가능하다.

해시 함수의 요구 사항:

  1. 일관성: 동일한 입력에 대해 항상 동일한 해시 값을 반환해야 한다.
  2. 효율성: 해시 함수는 빠르게 계산되어야 한다.
  3. 균일성: 해시 함수는 해시 테이블 전체에 걸쳐 키를 균등하게 분포시켜야 한다.

동치성 (Equivalence)

동치성은 두 객체가 서로 "같다"고 간주될 수 있는지를 결정하는 속성이다. 파이썬에서는 __eq__() 메소드를 통해 객체 간의 동치성을 비교할 수 있다. 두 객체가 동일하다고 간주될 때, 그 두 객체는 동일한 행동을 보여야 하며, 같은 데이터를 담고 있어야 한다.

동치성 비교의 규칙:

  • 반사성: 객체는 자기 자신과 같아야 한다 (x == x).
  • 대칭성: 한 객체가 다른 객체와 같다면, 그 반대도 마찬가지여야 한다 (x == y 이면, y == x).
  • 추이성: 한 객체가 두 번째 객체와 같고, 그 두 번째 객체가 세 번째 객체와 같다면, 첫 번째 객체는 세 번째 객체와 같아야 한다 (x == yy == z 이면, x == z).

해시와 동치성의 관계

파이썬에서 해시 가능한 객체는 __hash__()__eq__() 메소드를 모두 정의해야 한다. 해시 테이블을 사용하는 자료 구조(예: dict, set)에서는 객체를 저장하거나 검색하기 위해 이 두 메소드가 필수적으로 사용된다. 객체의 해시 값이 동일하면, 그 객체들은 동치일 수도 있지만 반드시 그런 것은 아니다(해시 충돌). 하지만, 두 객체가 동치라면 그들의 해시 값도 반드시 동일해야 한다. 이는 해시 테이블에서 효율적인 검색과 저장을 보장하기 위한 필수 요건이다.

이렇게 해시와 동치성은 파이썬의 자료 구조에서 데이터의 효율적인 관리를 위해 상호 연관되어 동작한다. 해시를 사용함으로써 빠른 접근 시간을 제공하고, 동치성을 통해 데이터의 정확성을 보장한다.

 

해시 충돌

해시 충돌은 해시 테이블에서 특정한 키들이 같은 해시값을 가지게 되어, 동일한 위치에 데이터를 저장하려고 할 때 발생하는 문제이다. 해시 테이블은 키를 해시 함수를 통해 계산된 인덱스에 저장하여, 빠른 데이터 검색, 삽입, 삭제를 가능하게 하는 자료 구조이다. 그러나, 한정된 크기의 인덱스 내에서 무한한 키를 다루게 되면, 서로 다른 키들이 같은 인덱스에 매핑될 가능성이 있다. 이를 해시 충돌이라고 한다.

해시 충돌의 발생

해시 함수는 어떤 긴 정보를 고정된 크기의 짧은 정보로 매핑한다. 이 과정에서 다음과 같은 이유로 충돌이 발생할 수 있다:

  1. 서로 다른 키들이 동일한 해시값을 갖는 경우: 예를 들어, 해시 함수가 두 개의 서로 다른 키에 대해 같은 출력값을 주는 경우이다.
  2. 해시 테이블 크기의 제한: 모든 가능한 키에 대해 유일한 인덱스를 제공하는 것은 실제로 불가능하며, 테이블의 크기는 보통 키의 가능한 값보다 훨씬 작다.

해시 충돌 해결 기법

해시 충돌을 처리하는 주요 방법은 다음과 같다

체이닝(Chaining):

  • 각 해시 테이블의 인덱스에 연결 리스트를 사용하여 여러 개의 키-값 쌍을 저장한다.
  • 충돌이 발생하면, 해당 인덱스의 리스트에 새로운 요소를 추가한다.
  • 검색, 삽입, 삭제 작업은 해당 인덱스의 리스트를 순회하면서 이루어진다.
# 체이닝 예제
hashtable = [[] for _ in range(10)]  # 10개의 버킷을 가진 해시 테이블
def hash_function(key):
    return key % 10
def insert(hashtable, key, value):
    hash_key = hash_function(key)
    hashtable[hash_key].append((key, value))

오픈 어드레싱(Open Addressing):

  • 모든 키-값 쌍을 해시 테이블의 버킷 배열 내에 직접 저장한다.
  • 충돌이 발생하면, 다른 버킷을 찾아 데이터를 저장한다. 이를 위한 방법으로 선형 조사(Linear Probing), 이차 조사(Quadratic Probing), 이중 해싱(Double Hashing) 등이 있다.
  • 선형 조사는 충돌이 발생한 인덱스에서 고정된 간격으로 다음 인덱스를 검사하는 방식이다.
# 선형 조사 예제
def insert(hashtable, key, value):
    original_hash = hash_key = hash_function(key)
    while hashtable[hash_key] is not None:
        hash_key = (original_hash + 1) % len(hashtable)
        if hash_key == original_hash:
            raise Exception("Hashtable is full")
    hashtable[hash_key] = (key, value)

해시 충돌의 영향

해시 충돌을 효과적으로 관리하지 못하면, 해시 테이블의 성능이 크게 저하될 수 있다. 최악의 경우, 해시 테이블의 모든 연산이 선형 시간복잡도(O(n))를 가지게 되어, 해시 테이블의 이점이 사라질 수 있다. 따라서, 효율적인 해시 함수 선택과 충돌 해결 전략은 해시 테이블 기반 자료 구조의 성능에 매우 중요하다.

 

dict 작동 방식에 의한 영향

1. 키 객체는 반드시 해시 가능해야 한다

파이썬에서 딕셔너리의 키로 사용되기 위해서는 해당 객체가 변경 불가능하고, 일관된 해시값을 반환할 수 있어야 한다. 이는 객체의 __hash__() 메서드가 구현되어 있어야 함을 의미한다. 변경 가능한 객체(예: 리스트)는 해시 가능하지 않기 때문에 키로 사용될 수 없다. 이러한 요구 사항은 딕셔너리가 키를 통해 빠르게 값을 찾을 수 있도록 보장하기 위한 것이다.

2. dict의 메모리 오버헤드가 크다

해시 테이블을 사용하기 때문에, 파이썬의 딕셔너리는 비교적 메모리 사용량이 크다. 이는 각 키-값 쌍에 추가적인 메모리가 사용되며, 해시 충돌을 관리하기 위한 추가 구조가 필요하기 때문이다. 또한, 해시 테이블의 효율적인 작동을 위해 일반적으로 사용하는 데이터 크기보다 큰 메모리가 할당된다(로드 팩터 관리).

3. 키 검색이 아주 빠르다

딕셔너리의 키 검색은 해시 함수를 통해 직접적인 인덱스 접근을 가능하게 하므로, 대부분의 경우 상수 시간(O(1)) 내에 이루어진다. 이는 데이터의 크기와 관계없이 일관된 성능을 제공한다.

4. 키 순서는 삽입 순서에 따라 달라진다

파이썬 3.7 이상에서는 **dict**가 삽입 순서를 보장한다. 이전에는 해시 테이블의 구조상 키의 순서가 무작위로 보였으나, 개선된 딕셔너리 구현은 삽입된 순서를 유지하는 내부 목록을 관리하여 이를 가능하게 한다.

5. 딕셔너리에 항목을 추가하면 기존 키의 순서가 변경 될 수 있다

이전 버전의 파이썬에서는 딕셔너리의 크기가 재조정될 때(예: 새 항목 추가로 인해 내부 버킷 배열이 리사이징되는 경우) 키의 내부 순서가 변경될 수 있었다. 이는 해시 충돌을 다르게 해결하고, 새로운 버킷 배열에 키를 재분배하기 때문이다. 그러나 파이썬 3.7 이후부터는 삽입 순서가 유지되므로 이러한 현상은 더 이상 발생하지 않는다.

 

집합의 작동 방식

1. 고속 멤버십 테스트

  • 집합의 가장 큰 장점 중 하나는 멤버십 테스트의 효율성입니다. in 연산자를 사용하여 집합 내에 특정 요소가 존재하는지 확인할 수 있으며, 이 연산은 평균적으로 상수 시간(O(1))에 수행된다. 이는 리스트나 튜플에서 요소를 찾기 위해 필요한 선형 시간(O(n))보다 훨씬 빠르다.
  • 예: if element in my_set:

2. 중복 요소 자동 제거

  • 집합에 요소를 추가할 때, 파이썬은 먼저 요소의 해시값을 계산하여 이미 존재하는지 검사한다. 이 과정에서 중복된 요소는 자동으로 제거된다. 이 특성은 데이터에서 고유한 항목만을 추출하거나 중복을 제거할 때 매우 유용하다.
  • 예: unique_elements = set(my_list)

3. 데이터 정렬의 부재

  • 해시 테이블 기반 구현 때문에 집합은 요소의 순서를 유지하지 않는다. 즉, 요소는 삽입된 순서를 반영하지 않고 저장되며, 이는 집합을 반복할 때 마다 요소의 순서가 다를 수 있음을 의미한다.
  • 예: for element in my_set: print(element)

4. 빠른 집합 연산

  • 집합은 해시 테이블을 사용하기 때문에 합집합, 교집합, 차집합 등의 집합 연산도 매우 효율적으로 수행된다. 이 연산들은 내부적으로 해시 테이블의 특성을 활용하여 빠르게 결과를 산출할 수 있다.
  • 예: union_set = set_a | set_b, intersection_set = set_a & set_b

5. 메모리 사용량

  • 해시 테이블을 사용하기 때문에, 집합은 동일한 데이터를 리스트나 튜플로 관리하는 것보다 일반적으로 더 많은 메모리를 사용한다. 이는 고유한 요소 관리와 빠른 접근을 위한 공간-시간 트레이드오프의 결과이다.

6. 동적 크기 조정

  • 집합의 크기가 커지면 파이썬은 내부 해시 테이블의 크기를 자동으로 조정한다. 이는 성능을 일정하게 유지하기 위해 필요한 작업이지만, 크기 조정 작업 중에는 짧은 시간 동안 성능 저하가 발생할 수 있다.

 

모든 코드는 github에 저장되어 있습니다.

https://github.com/SeongUk18/python

728x90