Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 584 – 딕셔너리에 합집합 연산자 추가

Author:
Steven D’Aprano <steve at pearwood.info>, Brandt Bucher <brandt at python.org>
BDFL-Delegate:
Guido van Rossum <guido at python.org>
Status:
Final
Type:
Standards Track
Created:
01-Mar-2019
Python-Version:
3.9
Post-History:
01-Mar-2019, 16-Oct-2019, 02-Dec-2019, 04-Feb-2020, 17-Feb-2020
Resolution:
Python-Dev thread

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP에서는 내장 dict 클래스에 병합(|) 및 업데이트(|=) 연산자를 추가할 것을 제안합니다.

Note

이 PEP가 승인된 후, 몇 가지 다른 표준 라이브러리 매핑에도 새 연산자를 구현하기로 결정했습니다.

동기

현재 두 딕셔너리를 병합하는 방법에는 몇 가지 단점이 있습니다.

dict.update

d1.update(d2)d1을 제자리에서 수정합니다. e = d1.copy(); e.update(d2)는 표현식이 아니며 임시 변수가 필요합니다.

{**d1, **d2}

딕셔너리 언패킹은 보기 좋지 않으며 쉽게 알아보기 어렵습니다. 처음 보았을 때 이것이 무엇을 의미하는지 추측하거나, 두 딕셔너리를 병합하는 “명백한 방법”이라고 생각할 수 있는 사람은 많지 않을 것입니다.

귀도가 말했듯이:

PEP 448에 대해서는 유감스럽지만, 더 단순한 상황에서 **d를 알고 있더라도 일반적인 Python 사용자에게 두 딕셔너리를 새 딕셔너리로 결합하는 방법을 물어본다면 {**d1, **d2}를 떠올릴 사람은 많지 않을 것이라고 생각합니다. 이 스레드가 시작되었을 때는 저 자신도 그것을 잊고 있었다는 것을 알게 되었습니다!

{**d1, **d2}는 매핑의 타입을 무시하고 항상 dict를 반환합니다. type(d1)({**d1, **d2})는 호환되지 않는 __init__ 메서드를 가진 defaultdict 와 같은 dict 서브클래스에서 실패합니다.

collections.ChainMap

ChainMap은 안타깝게도 잘 알려져 있지 않으며 “명백한” 방법으로도 볼 수 없습니다. 또한 예상되는 순서와 반대 순서로 중복 키를 처리합니다(“마지막으로 발견된 키가 우선”이 아니라 “처음 발견된 키가 우선”). 딕셔너리 언패킹과 마찬가지로 원하는 서브클래스를 적용하게 만들기가 어렵습니다. 같은 이유로 type(d1)(ChainMap(d2, d1))는 dict의 일부 서브클래스에서 실패합니다.

또한 ChainMap은 기반이 되는 딕셔너리를 감싸므로, ChainMap에 쓰기 작업을 수행하면 원래 딕셔너리가 수정됩니다.:

>>> d1 = {'spam': 1}
>>> d2 = {'eggs': 2}
>>> merged = ChainMap(d2, d1)
>>> merged['eggs'] = 999
>>> d2
{'eggs': 999}

dict(d1, **d2)

이 “간단한 요령”은 잘 알려져 있지 않으며, d2가 전부 문자열을 키로 사용할 때만 작동합니다.:

>>> d1 = {"spam": 1}
>>> d2 = {3665: 2}
>>> dict(d1, **d2)
Traceback (most recent call last):
  ...
TypeError: keywords must be strings

근거

새 연산자는 리스트 연결(+) 및 확장(+=) 연산자가 list.extend와 맺는 관계와 동일한 관계를 dict.update 메서드와 맺게 됩니다. 이는 |/|=연산자가 set.update와 맺는 관계와는 다소 다르다는 점에 유의하십시오. 작성자들은 제자리 연산자가(list가 그러하듯이) 더 넓은 범위의 형식을 받아들이도록 허용하는 것이 더 유용한 설계이며, 이항 연산자의 피연산자 형식을 제한하는 것이(다시 list가 그러하듯이) 양쪽에서 발생하는 복잡한 암시적 형식 변환으로 인한 조용한 오류를 방지하는 데 도움이 된다고 판단했습니다.

키 충돌은 가장 오른쪽의 값을 유지하는 방식으로 해결됩니다. 이는 유사한 dict 연산의 기존 동작과 일치하며, 마지막으로 발견된 값이 항상 우선합니다.:

{'a': 1, 'a': 2}
{**d, **e}
d.update(e)
d[k] = v
{k: v for x in (d, e) for (k, v) in x.items()}

위의 모든 경우는 동일한 규칙을 따릅니다. 이 PEP는 이러한 동작이 단순하고 명백하며 일반적으로 우리가 원하는 동작이고, 딕셔너리의 기본 동작이어야 한다는 입장을 취합니다. 이는 딕셔너리 합집합이 교환법칙을 따르지 않는다는 의미입니다. 일반적으로 d | e != e | d입니다.

마찬가지로 딕셔너리의 키-값 쌍에 대한 반복 순서는 위 예시와 동일한 의미론을 따르며, 새로 추가된 각 키(및 그 값)는 현재 시퀀스의 뒤에 추가됩니다.

사양

딕셔너리 합집합은 왼쪽 피연산자와 오른쪽 피연산자를 병합한 새 dict를 반환하며, 두 피연산자는 각각 dict(또는 dict의 서브클래스 인스턴스)여야 합니다. 두 피연산자 모두에 키가 있으면 마지막으로 나타난 값(즉, 오른쪽 피연산자의 값)이 우선합니다:

>>> d = {'spam': 1, 'eggs': 2, 'cheese': 3}
>>> e = {'cheese': 'cheddar', 'aardvark': 'Ethel'}
>>> d | e
{'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}
>>> e | d
{'cheese': 3, 'aardvark': 'Ethel', 'spam': 1, 'eggs': 2}

증강 대입 버전은 제자리에서 동작합니다:

>>> d |= e
>>> d
{'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}

증강 대입은 단일 위치 인자와 함께 호출한 update 메서드와 동일하게 동작하므로, 매핑 프로토콜을 구현하는 모든 것(더 구체적으로는 keys__getitem__ 메서드를 가진 모든 것) 또는 키-값 쌍의 이터러블도 허용합니다. 이는 리스트뿐만 아니라 모든 이터러블을 허용하는 list +=list.extend와 유사합니다. 위에서 계속됨:

>>> d | [('spam', 999)]
Traceback (most recent call last):
  ...
TypeError: can only merge dict (not "list") to dict

>>> d |= [('spam', 999)]
>>> d
{'spam': 999, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}

새 키가 추가될 때 키의 순서는 해당 타입에 오른쪽 매핑이 존재하는 경우 그 매핑 내 순서와 일치합니다.

참조 구현

저자 중 한 명이 C 구현을 작성했습니다.

근사적인 순수 Python 구현은 다음과 같습니다:

def __or__(self, other):
    if not isinstance(other, dict):
        return NotImplemented
    new = dict(self)
    new.update(other)
    return new

def __ror__(self, other):
    if not isinstance(other, dict):
        return NotImplemented
    new = dict(other)
    new.update(self)
    return new

def __ior__(self, other):
    dict.update(self, other)
    return self

주요 반론

딕셔너리 합집합은 교환법칙을 따르지 않습니다

합집합은 교환법칙을 따르지만, 딕셔너리 합집합은 그렇지 않습니다(d | e != e | d).

답변

Python에는 교환법칙을 따르지 않는 합집합의 전례가 있습니다:

>>> {0} | {False}
{0}
>>> {False} | {0}
{False}

결과가 같을 수는 있지만, 둘은 분명히 다릅니다. 일반적으로 a | bb | a와 동일한 연산이 아닙니다.

딕셔너리 합집합은 비효율적입니다

매핑에 파이프 연산자를 제공하면 확장성이 좋지 않은 코드를 작성하게 될 수 있습니다. 딕셔너리 합집합을 반복해서 수행하는 것은 비효율적입니다. d | e | f | g | h는 세 개의 임시 매핑을 생성하고 제거합니다.

답변

동일한 주장은 시퀀스 연결에도 적용됩니다.

시퀀스 연결은 시퀀스의 전체 항목 수에 따라 증가하므로 O(N**2)(이차) 성능으로 이어집니다. 딕셔너리 합집합에는 중복 키가 포함될 가능성이 있으므로, 임시 매핑은 그만큼 빠르게 커지지 않습니다.

많은 수의 리스트나 튜플을 연결하는 일이 드문 것처럼, 이 PEP의 저자들은 많은 수의 딕셔너리를 병합하는 일도 드물 것이라고 생각합니다. collections.Counter는 많은 연산자를 지원하는 딕셔너리 서브클래스이며, 많은 Counter를 결합하여 성능 문제를 겪었다는 알려진 사례도 없습니다. 또한 저자들이 표준 라이브러리를 조사한 결과 두 개를 초과하는 딕셔너리를 병합하는 사례는 없었으므로, 실제로는 이것이 성능 문제가 될 가능성은 낮습니다… “N이 충분히 작으면 모든 것이 빠릅니다”.

성능이 문제가 되는 상황에서 많은 수의 딕셔너리를 병합할 것으로 예상된다면, 명시적 루프와 제자리 병합을 사용하는 편이 더 나을 수 있습니다:

new = {}
for d in many_dicts:
    new |= d

딕셔너리 합집합은 손실이 발생합니다

딕셔너리 합집합에서는 데이터가 손실될 수 있지만(값이 사라질 수 있음), 다른 어떤 형태의 합집합에서도 손실이 발생하지 않습니다.

답변

이 주장의 첫 번째 부분이 왜 문제인지 명확하지 않습니다. dict.update()는 값을 버릴 수 있지만 키는 버리지 않습니다. 이는 예상되는 동작이며, update()로 표기하든 |로 표기하든 계속 예상되는 동작입니다.

다른 유형의 합집합도 가역적이지 않다는 의미에서 정보가 손실됩니다. 합집합만 주어졌을 때 두 피연산자를 되돌릴 수 없습니다. a | b == 365ab는 무엇입니까?

이를 수행하는 유일한 방법

딕셔너리 합집합은 선의 ‘이를 수행하는 유일한 방법’이라는 공안을 위반합니다.

응답

그러한 공안은 없습니다. “이를 수행하는 유일한 방법”은 오래전 펄 커뮤니티에서 비롯된 파이썬에 대한 비방입니다.

이를 수행하는 방법은 하나 이상

좋습니다. 선에서는 이를 수행하는 방법이 유일해야 한다고 말하지 않습니다. 그러나 이를 수행하는 “방법이 하나 이상” 허용되는 것을 금지하기는 합니다.

응답

그러한 금지는 없습니다. “파이썬의 선”은 “유일하게 분명한 방법”에 대한 선호를 표현할 뿐입니다.:

There should be one-- and preferably only one --obvious way to do
it.

여기서 강조하는 점은 “그것”을 수행할 분명한 방법이 있어야 한다는 것입니다. 딕셔너리 업데이트 연산의 경우, 우리가 수행하고자 할 수 있는 서로 다른 연산은 적어도 두 가지입니다.

  • 딕셔너리를 제자리에서 업데이트: 분명한 방법은 update() 메서드를 사용하는 것입니다. 이 제안이 채택되면 |= 증강 대입 연산자도 작동하지만, 이는 증강 대입이 정의된 방식에 따른 부수 효과입니다. 어느 것을 선택할지는 취향의 문제입니다.
  • 기존의 두 딕셔너리를 합쳐 새로 생성된 세 번째 딕셔너리로 만들기: 이 PEP에서는 분명한 방법으로 | 병합 연산자를 사용할 것을 제안합니다.

실제로 파이썬에서는 “방법은 하나”라는 선호가 자주 위반됩니다. 예를 들어 모든 for 루프는 while 루프로 다시 작성할 수 있으며, 모든 if 블록은 if/ else 블록으로 작성할 수 있습니다. 리스트, 집합 및 딕셔너리 컴프리헨션은 모두 제너레이터 표현식으로 대체할 수 있습니다. 리스트는 연결을 구현하는 방법을 무려 다섯 가지나 제공합니다.

  • 연결 연산자: a + b
  • 제자리 연결 연산자: a += b
  • 슬라이스 대입: a[len(a):] = b
  • 시퀀스 언패킹: [*a, *b]
  • 확장 메서드: a.extend(b)

“방법은 하나”라는 원칙을 위반한다는 이유로 유용한 기능을 거부하는 데 지나치게 엄격해서는 안 됩니다.

딕셔너리 합집합은 코드를 이해하기 어렵게 만듭니다

딕셔너리 합집합은 코드가 무엇을 의미하는지 파악하기 어렵게 만듭니다. 특정인의 말을 그대로 인용하기보다는 이 반론을 바꾸어 표현하면 다음과 같습니다. “spam | eggs를 보더라도 spameggs가 무엇인지 알지 못하면 그것이 무엇을 하는지 알 수 없습니다.”

응답

이는 매우 타당합니다. 그러나 | 연산자의 사용이 다음 중 어느 것이든 의미할 수 있는 오늘날에도 이는 똑같이 타당합니다:

  • int/bool 비트 OR
  • set/frozenset 합집합
  • 그 밖의 오버로드된 연산

가능한 의미에 딕셔너리 합집합을 추가한다고 해서 코드를 이해하기가 더 어렵게 되는 것 같지는 않습니다. spameggs가 매핑인지 판단하는 데 필요한 작업은 이들이 집합인지 정수인지 판단하는 데 필요한 작업보다 많지 않습니다. 또한 적절한 명명 규칙이 도움이 됩니다.:

flags |= WRITEABLE  # Probably numeric bitwise-or.
DO_NOT_RUN = WEEKENDS | HOLIDAYS  # Probably set union.
settings = DEFAULT_SETTINGS | user_settings | workspace_settings  # Probably dict union.

전체 set API는 어떻습니까?

딕셔너리는 “집합과 유사”하므로 전체 집합 연산자 컬렉션인 |, &, ^, -를 지원해야 합니다.

응답

이 PEP에서는 딕셔너리가 전체 집합 연산자 컬렉션을 지원해야 하는지에 대한 입장을 취하지 않으며, 이를 이후의 PEP에 맡기는 편을 선호합니다(저자 중 한 명은 이러한 PEP를 작성하는 데 관심이 있습니다). 이후 PEP에 도움이 되도록 간단한 요약을 제시합니다.

집합 대칭 차집합(^)은 명확하고 자연스럽습니다. 예를 들어 두 딕셔너리가 주어졌다고 하면:

d1 = {"spam": 1, "eggs": 2}
d2 = {"ham": 3, "eggs": 4}

대칭 차집합 d1 ^ d2{"spam": 1, "ham": 3}이 됩니다.

집합 차집합(-)도 명확하고 자연스러우며, 이 PEP의 이전 버전에서는 이를 제안에 포함했습니다. 위의 딕셔너리가 주어지면 d1 - d2{"spam": 1}이 되고 d2 - d1{"ham": 3}이 됩니다.

집합 교집합(&)은 다소 문제가 있습니다. 두 딕셔너리에서 의 교집합을 구하는 것은 쉽지만, 을 어떻게 처리해야 하는지는 명확하지 않습니다. 위의 두 딕셔너리가 주어지면 d1 & d2의 유일한 키가 "eggs"여야 한다는 것은 분명합니다. 그러나 “마지막으로 확인된 값이 우선한다”는 다른 딕셔너리 연산 및 제안된 합집합 연산자와 일관된다는 장점이 있습니다.

MappingMutableMapping은 어떻습니까?

collections.abc.Mappingcollections.abc.MutableMapping||=를 정의해야 하므로, 서브클래스는 새 연산자를 직접 정의하는 대신 상속하기만 하면 됩니다.

응답

이러한 클래스에 새 연산자를 추가하는 것이 문제가 되는 주된 이유는 두 가지입니다:

  • 현재 둘 다 copy 메서드를 정의하지 않으며, |가 새 인스턴스를 만드는 데 이 메서드가 필요합니다.
  • MutableMapping|=를 추가하거나 Mappingcopy 메서드를 추가하면 가상 서브클래스에 대한 호환성 문제가 발생합니다.

거부된 아이디어

거부된 의미론

충돌하는 키를 처리하기 위해 제안된 다른 해결책이 적어도 네 가지 있었습니다. 이러한 대안은 딕셔너리의 서브클래스에 맡깁니다.

발생시키기

이 동작이 다양한 사용 사례를 가지거나 자주 유용할지는 분명하지 않지만, 딕셔너리 합집합 연산자를 사용할 때마다 try/except 절로 감싸야 하므로 성가실 가능성이 높습니다.

값 더하기 (Counter처럼 + 사용)

기본 동작으로 사용하기에는 지나치게 특수화되어 있습니다.

가장 왼쪽의 값(처음 발견된 값) 우선

이 동작이 다양한 사용 사례를 가지는지는 분명하지 않습니다. 사실 인자의 순서만 간단히 뒤집으면 됩니다.:

d2 | d1  # d1 merged with d2, keeping existing values in d1

리스트로 값 연결

이는 기본 동작으로 사용하기에는 지나치게 특수화되어 있을 가능성이 높습니다. 값이 이미 리스트인 경우 어떻게 처리해야 할지는 분명하지 않습니다.:

{'a': [1, 2]} | {'a': [3, 4]}

이 경우 {'a': [1, 2, 3, 4]}를 제공해야 합니까, 아니면 {'a': [[1, 2], [3, 4]]}를 제공해야 합니까?

거부된 대안

덧셈 연산자 사용

이 PEP는 원래 ++= 연산자를 사용하는 딕셔너리 덧셈 제안으로 시작했습니다. 이 선택은 극도로 논란이 많았으며, 많은 사람이 연산자 선택에 대해 심각한 이의를 제기했습니다. 자세한 내용은 PEP의 previous versions및 mailing list discussions를 참조하십시오.

왼쪽 시프트 연산자 사용

<< 연산자는 Python-Ideas에서 큰 지지를 얻지는 못한 것 같지만, 중대한 반대 의견도 없었습니다. 아마도 가장 강력한 반대 의견은 Chris Angelico의 다음과 같은 발언이었을 것입니다.

정보 흐름을 나타내기 위해 연산자를 남용하는 것의 “귀여움”은 C++가 그렇게 한 직후부터 식상해졌습니다.

새로운 왼쪽 화살표 연산자 사용

또 다른 제안은 새로운 연산자 <-를 만드는 것이었습니다. 안타깝게도 이는 모호할 수 있습니다. d <- ed merge e를 의미할 수도 있고 d less-than minus e를 의미할 수도 있습니다.

메서드 사용

dict.merged() 메서드를 사용하면 연산자가 전혀 필요하지 않습니다. 한 가지 미묘한 점은 이를 언바운드 메서드로 호출하는 경우와 바운드 메서드로 호출하는 경우에 구현이 약간 달라야 할 가능성이 높다는 것입니다.

언바운드 메서드로서의 동작은 다음과 유사할 수 있습니다.:

def merged(cls, *mappings, **kw):
    new = cls()  # Will this work for defaultdict?
    for m in mappings:
        new.update(m)
    new.update(kw)
    return new

바운드 메서드로서의 동작은 다음과 유사할 수 있습니다.:

def merged(self, *mappings, **kw):
    new = self.copy()
    for m in mappings:
        new.update(m)
    new.update(kw)
    return new
장점
  • 말하자면, 메서드는 연산자보다 더 쉽게 발견할 수 있습니다.
  • 이 메서드는 임의 개수의 위치 인자 및 키워드 인자를 받아 임시 딕셔너리를 만드는 비효율을 피할 수 있습니다.
  • update 메서드처럼 (key, value) 쌍의 시퀀스를 받아들입니다.
  • 메서드이므로 “첫 번째 값 우선”, “고유 키” 등과 같은 대체 동작이 필요할 때 서브클래스에서 쉽게 재정의할 수 있습니다.
단점
  • 일반 인스턴스 메서드와 classmethod의 동작을 결합한 새로운 종류의 메서드 데코레이터가 필요할 가능성이 높습니다. 메서드를 재정의해야 하는 사용자를 위해 공개되어야 하지만, 반드시 내장 함수일 필요는 없습니다. 개념 증명이 있습니다.
  • 연산자가 아닙니다. Guido는 연산자가 유용한 이유를 설명합니다. 다른 관점은 Alyssa Coghlan의 블로그 게시물을 참조하십시오.

함수를 사용하십시오

메서드 대신 새로운 내장 함수 merged()를 사용하십시오. 가능한 구현은 다음과 같은 형태일 수 있습니다.:

def merged(*mappings, **kw):
    if mappings and isinstance(mappings[0], dict):
        # If the first argument is a dict, use its type.
        new = mappings[0].copy()
        mappings = mappings[1:]
    else:
        # No positional arguments, or the first argument is a
        # sequence of (key, value) pairs.
        new = dict()
    for m in mappings:
        new.update(m)
    new.update(kw)
    return new

대체 방법으로는 임의의 키워드를 포기하고, 충돌 시 동작을 지정하는 단일 키워드 매개변수를 사용할 수도 있습니다.:

def merged(*mappings, on_collision=lambda k, v1, v2: v2):
    # implementation left as an exercise to the reader
장점
  • 위의 메서드 방식과 동일한 장점이 대부분 있습니다.
  • 충돌 시 대체 동작을 구현하기 위해 서브클래스가 필요하지 않고 함수만 있으면 됩니다.
단점
  • 내장 함수로 만들 만큼 중요하지 않을 수 있습니다.
  • 임의의 키워드 인자를 처리할 수 있는 기능을 잃지 않으면서 “첫 번째 항목이 우선하는” 것과 같은 동작을 재정의하기는 어렵습니다.

예제

이 PEP의 작성자들은 딕셔너리 병합을 위한 서드 파티 라이브러리 중 딕셔너리 합집합의 후보가 될 수 있는 라이브러리를 조사했습니다.

이는 작성자 중 한 명의 컴퓨터에 설치되어 있던 임의의 서드 파티 패키지 일부를 바탕으로 한 개략적인 목록이며, 현재 어떤 패키지의 상태도 반영하지 않을 수 있습니다. 또한 추가적인 관련 없는 리팩터링이 가능할 수 있지만, 다시 작성한 버전은 동등한 조건에서 비교할 수 있도록 새로운 연산자의 사용만 추가한다는 점에 유의하십시오. 또한 효율적인 경우 결과를 식으로 축약합니다.

IPython/zmq/ipkernel.py

이전:

aliases = dict(kernel_aliases)
aliases.update(shell_aliases)

이후:

aliases = kernel_aliases | shell_aliases

IPython/zmq/kernelapp.py

이전:

kernel_aliases = dict(base_aliases)
kernel_aliases.update({
    'ip' : 'KernelApp.ip',
    'hb' : 'KernelApp.hb_port',
    'shell' : 'KernelApp.shell_port',
    'iopub' : 'KernelApp.iopub_port',
    'stdin' : 'KernelApp.stdin_port',
    'parent': 'KernelApp.parent',
})
if sys.platform.startswith('win'):
    kernel_aliases['interrupt'] = 'KernelApp.interrupt'

kernel_flags = dict(base_flags)
kernel_flags.update({
    'no-stdout' : (
            {'KernelApp' : {'no_stdout' : True}},
            "redirect stdout to the null device"),
    'no-stderr' : (
            {'KernelApp' : {'no_stderr' : True}},
            "redirect stderr to the null device"),
})

이후:

kernel_aliases = base_aliases | {
    'ip' : 'KernelApp.ip',
    'hb' : 'KernelApp.hb_port',
    'shell' : 'KernelApp.shell_port',
    'iopub' : 'KernelApp.iopub_port',
    'stdin' : 'KernelApp.stdin_port',
    'parent': 'KernelApp.parent',
}
if sys.platform.startswith('win'):
    kernel_aliases['interrupt'] = 'KernelApp.interrupt'

kernel_flags = base_flags | {
    'no-stdout' : (
            {'KernelApp' : {'no_stdout' : True}},
            "redirect stdout to the null device"),
    'no-stderr' : (
            {'KernelApp' : {'no_stderr' : True}},
            "redirect stderr to the null device"),
}

matplotlib/backends/backend_svg.py

이전:

attrib = attrib.copy()
attrib.update(extra)
attrib = attrib.items()

이후:

attrib = (attrib | extra).items()

matplotlib/delaunay/triangulate.py

변경 전:

edges = {}
edges.update(dict(zip(self.triangle_nodes[border[:,0]][:,1],
             self.triangle_nodes[border[:,0]][:,2])))
edges.update(dict(zip(self.triangle_nodes[border[:,1]][:,2],
             self.triangle_nodes[border[:,1]][:,0])))
edges.update(dict(zip(self.triangle_nodes[border[:,2]][:,0],
             self.triangle_nodes[border[:,2]][:,1])))

다음과 같이 다시 작성합니다:

edges = {}
edges |= zip(self.triangle_nodes[border[:,0]][:,1],
             self.triangle_nodes[border[:,0]][:,2])
edges |= zip(self.triangle_nodes[border[:,1]][:,2],
             self.triangle_nodes[border[:,1]][:,0])
edges |= zip(self.triangle_nodes[border[:,2]][:,0],
             self.triangle_nodes[border[:,2]][:,1])

matplotlib/legend.py

변경 전:

hm = default_handler_map.copy()
hm.update(self._handler_map)
return hm

변경 후:

return default_handler_map | self._handler_map

numpy/ma/core.py

변경 전:

_optinfo = {}
_optinfo.update(getattr(obj, '_optinfo', {}))
_optinfo.update(getattr(obj, '_basedict', {}))
if not isinstance(obj, MaskedArray):
    _optinfo.update(getattr(obj, '__dict__', {}))

변경 후:

_optinfo = {}
_optinfo |= getattr(obj, '_optinfo', {})
_optinfo |= getattr(obj, '_basedict', {})
if not isinstance(obj, MaskedArray):
    _optinfo |= getattr(obj, '__dict__', {})

praw/internal.py

변경 전:

data = {'name': six.text_type(user), 'type': relationship}
data.update(kwargs)

변경 후:

data = {'name': six.text_type(user), 'type': relationship} | kwargs

pygments/lexer.py

변경 전:

kwargs.update(lexer.options)
lx = lexer.__class__(**kwargs)

변경 후:

lx = lexer.__class__(**(kwargs | lexer.options))

requests/sessions.py

변경 전:

merged_setting = dict_class(to_key_val_list(session_setting))
merged_setting.update(to_key_val_list(request_setting))

변경 후:

merged_setting = dict_class(to_key_val_list(session_setting)) | to_key_val_list(request_setting)

sphinx/domains/__init__.py

변경 전:

self.attrs = self.known_attrs.copy()
self.attrs.update(attrs)

변경 후:

self.attrs = self.known_attrs | attrs

sphinx/ext/doctest.py

변경 전:

new_opt = code[0].options.copy()
new_opt.update(example.options)
example.options = new_opt

변경 후:

example.options = code[0].options | example.options

sphinx/ext/inheritance_diagram.py

변경 전:

n_attrs = self.default_node_attrs.copy()
e_attrs = self.default_edge_attrs.copy()
g_attrs.update(graph_attrs)
n_attrs.update(node_attrs)
e_attrs.update(edge_attrs)

변경 후:

g_attrs |= graph_attrs
n_attrs = self.default_node_attrs | node_attrs
e_attrs = self.default_edge_attrs | edge_attrs

sphinx/highlighting.py

변경 전:

kwargs.update(self.formatter_args)
return self.formatter(**kwargs)

변경 후:

return self.formatter(**(kwargs | self.formatter_args))

sphinx/quickstart.py

변경 전:

d2 = DEFAULT_VALUE.copy()
d2.update(dict(("ext_"+ext, False) for ext in EXTENSIONS))
d2.update(d)
d = d2

변경 후:

d = DEFAULT_VALUE | dict(("ext_"+ext, False) for ext in EXTENSIONS) | d

sympy/abc.py

이전:

clash = {}
clash.update(clash1)
clash.update(clash2)
return clash1, clash2, clash

이후:

return clash1, clash2, clash1 | clash2

sympy/parsing/maxima.py

이전:

dct = MaximaHelpers.__dict__.copy()
dct.update(name_dict)
obj = sympify(str, locals=dct)

이후:

obj = sympify(str, locals=MaximaHelpers.__dict__|name_dict)

sympy/printing/ccode.py 및 sympy/printing/fcode.py

이전:

self.known_functions = dict(known_functions)
userfuncs = settings.get('user_functions', {})
self.known_functions.update(userfuncs)

이후:

self.known_functions = known_functions | settings.get('user_functions', {})

sympy/utilities/runtests.py

이전:

globs = globs.copy()
if extraglobs is not None:
    globs.update(extraglobs)

이후:

globs = globs | (extraglobs if extraglobs is not None else {})

위의 예제는 | 연산자가 때때로 가독성을 명확하게 향상하고, 코드 줄 수를 줄이며 명확성을 개선한다는 것을 보여 줍니다. 그러나 | 연산자를 사용하는 다른 예제에서는 길고 복잡한 단일 표현식이 만들어지며, PEP 8에서 정한 최대 줄 길이인 80열을 훨씬 초과할 수도 있습니다. 다른 언어 기능과 마찬가지로, |가 자신의 코드를 개선하는지 여부는 프로그래머가 스스로 판단하여 사용해야 합니다.

관련 논의

메일링 리스트 스레드(결코 완전한 목록은 아닙니다):

버그 추적기의 티켓

표현식에서 두 딕셔너리를 병합하는 기능은 자주 요청되는 기능입니다. 예를 들면 다음과 같습니다.

https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression

https://stackoverflow.com/questions/1781571/how-to-concatenate-two-dictionaries-to-create-a-new-one-in-python

https://stackoverflow.com/questions/6005066/adding-dictionaries-together-python

때때로 병합에 대한 대안적인 동작을 요청하는 사람들도 있습니다.

https://stackoverflow.com/questions/1031199/adding-dictionaries-in-python

https://stackoverflow.com/questions/877295/python-dict-add-by-valuedict-2

…딕셔너리를 키의 집합으로 취급하자는 제안 등이 포함됩니다.

Ian Lee의 proto-PEP와 2015년의 논의가 있었습니다. 추가 논의는 Python-Ideas에서 이루어졌습니다.

(주의 깊은 독자라면 이 PEP의 저자 중 한 명이 2015년에는 이 아이디어에 더 회의적이었다는 점을 알아차릴 것입니다.)

딕셔너리에 연산자 전체를 추가하는 방안입니다.

Y-Combinator에서의 논의입니다.

https://treyhunner.com/2016/02/how-to-merge-dictionaries-in-python/

https://code.tutsplus.com/tutorials/how-to-merge-two-python-dictionaries–cms-26230

이 PEP의 이전 초안에 대한 직접적인 응답으로, Serhiy Storchaka는 copy(); update() 관용구를 딕셔너리 언패킹으로 대체하자는 버그 트래커의 티켓을 제기했습니다.