Tech </> tech.log
· - ·
PythonEasy

[Python][하급] PEP 572: 바다코끼리 연산자

파이썬 역사상 가장 뜨거웠던 논쟁

"Now that PEP 572 is done, I don't ever want to have to fight so hard for a PEP and find that so many people despise my decisions."
— Guido van Rossum, 2018년 7월 12일


연산자 하나가 파이썬의 역사를 바꾸다

2018년 7월 12일, 전 세계 파이썬 개발자들은 아침에 메일함을 열고 경악했습니다.

29년간 파이썬을 이끌어온 Guido van Rossum이 "Transfer of Power"라는 제목의 메일을 python-committers 메일링 리스트에 올린 것입니다.

I would like to remove myself entirely from the decision process.
I'll still be there for a while as an ordinary core dev, and I'll
still be available to mentor people -- possibly more available.
But I'm basically giving myself a permanent vacation from being BDFL,
and you all will be on your own.

BDFL(Benevolent Dictator For Life, 자비로운 종신 독재자).

1995년부터 파이썬의 모든 중요한 결정을 내려온 사람이 갑자기 떠났습니다. 그리고 그 중심에는 PEP 572, 일명 "바다코끼리 연산자(Walrus Operator)"가 있었습니다.

도대체 이 작은 := 기호가 뭐길래, 파이썬의 29년 역사를 뒤흔들었을까요?


문제의 시작 — "표현식 안에서 할당하고 싶어요"

파이썬의 "불편한" 설계 철학

파이썬은 의도적으로 문(Statement)과 표현식(Expression)을 분리했습니다.

# C/C++에서는 이런 코드가 가능합니다
while ((line = file.readline()) != "") {
    process(line);
}

# 하지만 파이썬에서는... 안 됩니다!
# = 는 statement이지 expression이 아니거든요.

파이썬에서는 이렇게 써야 했습니다:

# 방법 1: 중복 호출
line = file.readline()
while line != "":
    process(line)
    line = file.readline()  # 같은 코드를 또 씁니다

# 방법 2: 무한 루프 패턴
while True:
    line = file.readline()
    if line == "":
        break
    process(line)

어느 쪽이든 뭔가 찜찜합니다. 같은 코드를 두 번 쓰거나, while True라는 거짓말을 해야 하니까요.

리스트 컴프리헨션의 고통

더 고통스러운 케이스는 리스트 컴프리헨션이었습니다:

# 비싼 함수를 두 번 호출해야 합니다
results = [expensive_func(x) for x in data if expensive_func(x) > 0]

# 효율을 위해 풀어쓰면? 컴프리헨션의 우아함이 사라집니다
results = []
for x in data:
    val = expensive_func(x)
    if val > 0:
        results.append(val)

Guido van Rossum이 Dropbox 코드베이스를 분석했을 때, 그는 흥미로운 패턴을 발견했습니다. 개발자들이 성능을 희생하면서까지 코드 줄 수를 줄이려는 경향이 있었던 것입니다:

# 실제 Dropbox 코드에서 발견된 패턴들
# 패턴 매칭을 두 번 시도하는 비효율적인 코드
if re.match(pattern, data):
    result = re.match(pattern, data).group(1)  # 같은 매칭을 두 번!

PEP 572의 탄생과 진화

Chris Angelico의 제안 (2018년 2월)

2018년 2월 28일, 호주의 개발자 Chris Angelico가 PEP 572를 처음 제출했습니다. 원래 제목은 "Syntax for Statement-Local Name Bindings"였습니다.

초기 문법은 지금과 많이 달랐습니다:

# 초기 제안: (expr as name)
stuff = [(y, x/y) where y = f(x) for x in range(5)]

하지만 여기서부터 논쟁이 시작됩니다.

문법 전쟁: 무려 6가지 대안들

python-ideas와 python-dev 메일링 리스트에서는 치열한 문법 논쟁이 벌어졌습니다

# 대안 1: where / let / given 키워드
stuff = [(y, x/y) where y = f(x) for x in range(5)]
stuff = [(y, x/y) let y = f(x) for x in range(5)]
stuff = [(y, x/y) given y = f(x) for x in range(5)]

# 대안 2: with ... = 구문
stuff = [(y, x/y) with y = f(x) for x in range(5)]

# 대안 3: EXPR as NAME (import, except, with와 동일)
if (match := pattern.search(data)) as m:
    ...

# 대안 4: NAME := EXPR (최종 선택!)
if (n := len(a)) > 10:
    print(f"Too long: {n}")

# 대안 5: NAME -> EXPR
if (n -> len(a)) > 10:
    ...

# 대안 6: 일반 = 사용 (C 스타일)
if (n = len(a)) > 10:
    ...

왜 := 가 선택되었나?

각 대안이 거부된 이유를 살펴보면 파이썬의 설계 철학이 보입니다:

대안 거부 사유
where, let, given 새 키워드 추가는 하위 호환성을 깨뜨림
EXPR as NAME import, except, with에서 이미 다른 의미로 사용 중
= (C 스타일) ==와 혼동 → C의 악명 높은 버그 패턴 재현
-> 타입 힌트에서 반환 타입 표시로 이미 사용 중

결국 :=가 선택된 이유:

  1. 기존 문법과 충돌하지 않음 (:=는 파이썬에서 사용되지 않던 조합)
  2. Pascal/Ada 전통 — 할당 연산자로서의 역사적 선례
  3. 시각적 구분 — =와 명확히 다름

이름의 유래: 바다코끼리 🦭

:=를 옆으로 눕히면... 바다코끼리의 눈(:)과 엄니(=)처럼 보입니다!

:=
  눈  엄니

   O  O
    \ |
     \|
   ___Y___
  /       \
 |  ° - °  |   ← 바다코끼리!
  \_______/

이 귀여운 별명은 논쟁 중에 자연스럽게 붙었고, 지금은 공식 문서에도 언급됩니다.


논쟁의 불길 — python-dev 메일링 리스트가 불탔다

4라운드의 격전

PEP 572는 python-ideas에서 4차례 대규모 논쟁을 거쳤습니다.

그리고 2018년 4월 중순, python-dev로 이동했을 때... 폭풍이 시작되었습니다.

LWN.net의 보도에 따르면:

"After four rounds in the boxing ring at python-ideas, Angelico posted the PEP to the more widely read python-dev mailing list in mid-April. That set off a firestorm of replies..."

논쟁의 핵심 쟁점들

⚡ 쟁점 1: 가독성 (The Zen of Python)

반대파:

PEP 20 (The Zen of Python)을 인용하며

"There should be one-- and preferably only one --obvious way to do it."
이미 할당하는 방법이 있는데 왜 또 만드나요?

같은 PEP 20을 인용하며

찬성파:

"Readability counts."
"Simple is better than complex."
중복 코드 제거가 더 readable하다!

⚡ 쟁점 2: 스코프 문제

초기 제안에서 가장 논쟁적이었던 부분은 "Statement-Local Name Bindings" 개념이었습니다.

# 초기 제안: 변수가 statement 범위에서만 존재
if (x := compute()) > 0:
    print(x)  # x 존재
print(x)  # x가 사라짐! (초기 제안)

# 최종 결정: 일반 변수처럼 동작
if (x := compute()) > 0:
    print(x)
print(x)  # x 여전히 존재 (최종)

이 변경은 단순함을 위한 것이었지만, 일부 개발자들은 **"namespace pollution"**을 우려했습니다.

⚡ 쟁점 3: 컴프리헨션에서의 스코프 누출

가장 기술적으로 복잡했던 논쟁:

# 컴프리헨션의 반복 변수는 외부에서 안 보임 (Python 3)
[x for x in range(5)]
print(x)  # NameError!

# 하지만 walrus로 할당하면 보임!
[y := x for x in range(5)]
print(y)  # 4 (마지막 값)

Nick Coghlan(Python 핵심 개발자)은 이것이 Python 2 시절의 동작으로 퇴보하는 것 아니냐며 강하게 반대했습니다.

메일링 리스트의 실제 모습

2018년 Python Language Summit에서 Guido가 직접 언급한 내용:

"Overall, the idea has been 'incredibly controversial'. What one person thinks is more readable, another person thinks is less readable. The benefits are moderate and everyone has their own favorite syntax."

논쟁은 너무 길어져서, 많은 개발자들이 그냥 읽기를 포기했습니다. 메일 스레드가 수백 개에 달했거든요.


BDFL의 결단과 퇴장

Guido의 최종 결정 (2018년 7월 9일)

수개월간의 논쟁 끝에, Guido는 PEP 572를 승인(Accept)했습니다.

하지만 그 직후, 트위터와 메일링 리스트에서 비난의 물결이 쏟아졌습니다. 심지어 일부 핵심 개발자들도 공개적으로 불만을 표출했습니다.

"Transfer of Power" (2018년 7월 12일)

PEP 572 승인 3일 후, Guido는 다음과 같이 썼습니다:

Now that PEP 572 is done, I don't ever want to have to fight so
hard for a PEP and find that so many people despise my decisions.

I would like to remove myself entirely from the decision process.
I'll still be there for a while as an ordinary core dev, and I'll
still be available to mentor people -- possibly more available.
But I'm basically giving myself a permanent vacation from being BDFL,
and you all will be on your own.

I'm tired, and need a very long break.

나중에 InfoWorld와의 인터뷰에서 Guido는 더 구체적으로 밝혔습니다:

"The straw that broke the camel's back was a very contentious Python enhancement proposal, where after I had accepted it, people went to social media like Twitter and said things that really hurt me personally. And some of the people who said hurtful things were actually core Python developers, so I felt that I didn't quite have the trust of the Python core developer team anymore."

파이썬 거버넌스의 재편

Guido의 퇴장 후, 파이썬은 새로운 거버넌스 모델을 만들어야 했습니다:

  1. BDFL 체제 종료 (1995-2018, 23년)
  2. Steering Council 체제 시작 (2019-)
  • 5명의 핵심 개발자로 구성
  • 선거를 통해 선출
  • 집단 의사결정

첫 번째 Steering Council 멤버:

  • Barry Warsaw
  • Brett Cannon
  • Carol Willing
  • Guido van Rossum (처음에는 포함, 나중에 사퇴)
  • Nick Coghlan

구현 — Emily Morehouse의 이야기

5.1 Guido의 선택

PEP 572 승인 직후, Guido는 구현을 맡길 개발자를 찾아야 했습니다. 그가 선택한 사람은 Emily Morehouse였습니다.

Emily는 당시 아직 핵심 개발자가 아니었습니다. 하지만 그녀는 몇 년간 CPython 내부 구조를 공부하고, Guido의 멘토링을 받아왔습니다.

그녀의 회고:

"And then, something incredibly divisive happened: PEP 572 was approved soon after PyCon 2018. Guess who Guido asked to implement it? Suddenly, all of the things I had been studying aimlessly had meaning in a very real way."

구현의 도전

PEP 572 구현은 생각보다 훨씬 복잡했습니다:

구현해야 할 것들:
├── 토큰화 (tokenizer.c)
│   └── := 토큰 추가
├── 문법 (Grammar/python.gram)
│   └── named_expression 규칙 추가
├── AST (Python/ast.c)
│   └── NamedExpr 노드 추가
├── 컴파일러 (Python/compile.c)
│   └── 스코프 처리 로직 (가장 복잡!)
└── 테스트 (Lib/test/)
    └── 수백 개의 테스트 케이스

특히 스코프 처리가 악몽이었습니다. 컴프리헨션 내부의 walrus 연산자가 외부 스코프에 변수를 생성해야 했거든요:

# 컴프리헨션은 내부적으로 함수입니다
# 이 코드는:
[y := x*2 for x in range(5)]

# 대략 이렇게 동작합니다:
def _comprehension():
    result = []
    for x in range(5):
        nonlocal y  # 이 부분을 컴파일러가 처리해야!
        y = x*2
        result.append(y)
    return result
y  # 외부에서 접근 가능

2018년 9월: 핵심 개발자 승격

2018년 9월 CPython 스프린트에서, Emily는 Guido와 함께 구현을 거의 완성했습니다. 그리고 같은 주에 핵심 개발자(Core Developer)로 승격되었습니다.

[python-committers] 메일링 리스트
제목: New core developers: Lisa Roach and Emily Morehouse-Valcarcel

"Since July, Emily has worked with Guido's guidance to implement
PEP 572, Assignment Expressions."

Emily는 나중에 Python Steering Council 멤버가 되었고, 지금도 활발히 활동 중입니다.


그래서 바다코끼리가 뭔데?

📌 패턴 1: while 루프에서 할당과 조건 결합

# Before — 같은 코드를 두 번 써야 했죠
chunk = file.read(8192)
while chunk:
    process(chunk)
    chunk = file.read(8192)

# After 🦭 — 한 줄로 끝
while (chunk := file.read(8192)):
    process(chunk)

📌 패턴 2: if 문에서 결과 캡처
정규표현식 매칭할 때 특히 유용합니다:

# Before — match를 두 번 언급
match = pattern.search(data)
if match:
    print(match.group(1))

# After 🦭 — 깔끔하게 한 번에
if (match := pattern.search(data)):
    print(match.group(1))

📌 패턴 3: 컴프리헨션에서 중복 계산 제거

# Before (expensive_func 두 번 호출)
results = [expensive_func(x) for x in data if expensive_func(x) > 0]

# After 🦭 (한 번만 호출)
results = [y for x in data if (y := expensive_func(x)) > 0]

📌 패턴 4: any/all과 결합

조건을 만족하는 값을 찾으면서 동시에 캡처가능

# 주석으로 시작하는 첫 번째 줄 찾기
if any((comment := line).startswith('#') for line in lines):
    print(f"Found comment: {comment}")

Tim Peters의 조언

PEP 572의 공동 저자 Tim Peters(Timsort의 창시자!)는 이런 에세이를 남겼습니다:

"I dislike 'busy' lines of code, and also dislike putting conceptually unrelated logic on a single line."

그의 가이드라인:

# 좋음 ✓: 할당과 사용이 밀접하게 연관됨
if (n := len(items)) > 10:
    print(f"Too many: {n}")

# 나쁨 ✗: 과도한 복잡성
# 이건 그냥 여러 줄로 쓰세요
result = [(x, y, z) for x in range(10) 
          if (y := f(x)) > 0 
          if (z := g(y)) > 0]

언제 쓰지 말아야 하는가

PEP 8 스타일 가이드의 권장사항:

1️⃣ 일반 할당문이 가능하면 그걸 쓰세요

# 굳이 이렇게 쓸 필요 없음
(y := f(x))  # ❌

# 이게 더 명확함
y = f(x)     # ✓

2️⃣ 실행 순서가 헷갈리면 분리하세요

# 무슨 순서로 실행되는지 헷갈림
d[y := f(x)] = g(y)  # ❌

# 명확하게 분리
y = f(x)
d[y] = g(y)         # ✓

3️⃣ 한 표현식에 여러 walrus 금지

# 이건 그냥 읽기 싫음
(a := b) + (c := d)  # ❌

예외 케이스와 함정들

7.1 괄호가 필요한 상황들

:=는 가장 낮은 우선순위를 가지므로, 많은 경우 괄호가 필요합니다:

# 이것들은 SyntaxError!
y := f(x)          # 최상위 표현식 문에서 금지
y0 = y1 := f(x)    # 할당문 우변에서 금지
foo(x = y := f(x)) # 키워드 인자에서 금지
def foo(x = y := 0): pass  # 기본값에서 금지

# 괄호로 감싸면 동작
(y := f(x))        # OK
y0 = (y1 := f(x))  # OK
foo(x=(y := f(x))) # OK

왜 이렇게 제한했을까요? 일반 할당문 =과의 혼동을 막기 위해서입니다. 두 연산자가 같은 맥락에서 사용 가능하면 버그의 온상이 되거든요.

7.2 f-string에서의 충돌

:=는 f-string의 포맷 지정자 :와 충돌합니다:

# 이건 x:=8 을 "x를 너비 8, =정렬"로 해석함
f'{x:=8}'     # x의 값이 정렬됨 (walrus 아님!)

# walrus를 쓰려면 괄호 필수
f'{(x := 8)}' # x에 8 할당하고 출력

7.3 컴프리헨션 스코프 누출

이건 설계된 동작이지만, 많은 개발자를 놀라게 합니다:

>>> [y := x*2 for x in range(5)]
[0, 2, 4, 6, 8]
>>> y  # 밖에서도 보임!
8

>>> x  # 반복 변수는 안 보임
NameError: name 'x' is not defined

표준 라이브러리에서의 실제 사용

PEP 572가 승인된 후, CPython 표준 라이브러리도 일부 수정되었습니다:

copy.py에서:

# Before
reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    # ...

# After
if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
# ...

sre_parse.py (정규표현식 파서)에서:

# Before
c = source.get()
while c:
    # process c
    c = source.get()

# After
while (c := source.get()):
    # process c

논쟁 이후 — 커뮤니티의 평가

PEP 498 (f-string)도 처음에는 "왜 네 번째 포매팅 방법이 필요해?"라는 반대에 부딪혔지만, 지금은 Python 3의 가장 사랑받는 기능 중 하나가 되었습니다.

PEP 572도 비슷한 궤적을 따르고 있습니다:

  • 2019년: 조심스러운 채택
  • 2020년: 점진적 확산
  • 2021년~: 많은 개발자들이 일상적으로 사용

하지만 여전히…

일부 개발자들은 여전히 반대합니다:

"This feels very Perl-y in that it requires that you know what yet another operator means to read code that uses it. Since Python is supposed to be 'executable pseudocode', this kind of new operator might increase the amount of learning that a beginner has to do."

하지만 대부분의 Python 생태계는 walrus를 받아들였고, 많은 린터(flake8, pylint 등)도 walrus 관련 규칙을 추가했습니다.


연산자 하나의 무게

PEP 572는 단순한 문법 추가가 아니었습니다. 이 논쟁은 오픈소스 리더십의 한계를 보여주었으며, Python 거버넌스 모델을 근본적으로 바꿨고, 커뮤니티 토론 문화에 대한 성찰을 촉발했습니다.

Guido의 퇴장은 안타까운 일이었지만, 결과적으로 Python은 더 건강한 거버넌스 구조를 갖추게 되었습니다.

한 사람에게 모든 결정의 부담을 지우는 대신, Steering Council이라는 집단 리더십이 생겼습니다.

그리고 바다코끼리 연산자?

# 여전히 선택 사항입니다
# 하지만 적재적소에 사용하면...
if (match := re.search(pattern, text)):
    return match.group(1)

코드가 조금 더 우아해집니다. 🦭


참고 자료