WTF Python? 많이 당하는 코딩 파이썬 함정들

김현수·2025년 5월 29일

인간은 그렇게 논리적이지 않다

간단한 문제를 먼저 풀어봅시다.

4장의 카드가 있고, 각 카드는 한 면에 숫자, 다른 면에 색깔이 있습니다.

카드에 짝수가 적혀져있다면, 뒷면은 파란색이다.

이 규칙이 참인지 거짓인지 확인하려면 어떤 카드들을 뒤집어서 확인해야 할까요?

3, 8, 파랑, 빨강

정답은 8, 빨강입니다.

Wason Selection Task란?

  • 1966년 Peter Wason이 개발한 논리 추론 실험
  • 조건문 검증 능력을 테스트 ("If P then Q" 형태)
  • 반증 가능성을 찾는 능력 측정 (과학적 사고의 핵심)
  • 인지심리학에서 가장 많이 연구된 과제 중 하나

정답률

놀라운 결과는 대학생의 90% 이상이 틀리고, 논리학 교수들도 처음엔 헷갈립니다.
하지만 같은 논리를 이렇게 바꾸면 정답률이 금방 오릅니다.

"술집에서 음주자는 21세 이상이어야 한다" 맥주,콜라, ,25세, 16세

바로 반증을 찾을 겁니다. 맥주를 먹는 사람의 나이를 볼 것이고, 16세가 마시는 게 뭔지 체크할 겁니다.
콜라는 뭐 누구나 먹을 수 있고, 25세가 맥주를 먹는 것이 그렇게 궁금하진 않죠. 우리는 사회적 맥락안에서는 더욱 잘 해결합니다.

Daniel Kahneman의 연구에 따르면 인간이 직관적이고 휴리스틱 기반의 추론(System 1)을 선호하고, 논리적-분석적 추론(System 2)은 잘 사용하지 않는다고 합니다. 이는 논리적-분석적 추론의 System2는 느리고, 의식해야하며, 더 많은 에너지를 소모하고, 그리고 실제로 노력하는 것을 싫어한다고 합니다.

그렇다면 우리는 System2를 활성화하는 코딩 습관을 들여 System1의 영역으로 이전해야합니다. 많은 사람들이 파이썬을 사용하는 만큼 파이썬의 WTF moment를 몇가지 소개해보겠습니다.

Warlus Operator

먼저 흐름을 따라가면서 이해해보죠.

# Python version 3.8+
>>> a = "wtf_walrus"
>>> a
'wtf_walrus'

>>> a := "wtf_walrus"
File "<stdin>", line 1
    a := "wtf_walrus"
      ^
SyntaxError: invalid syntax

>>> (a := "wtf_walrus") # This works though
'wtf_walrus'
>>> a
'wtf_walrus'
# Python version 3.8+

>>> a = 6, 9
>>> a
(6, 9)

>>> (a := 6, 9)
(6, 9)
>>> a
6

>>> a, b = 6, 9 # Typical unpacking
>>> a, b
(6, 9)
>>> (a, b = 16, 19) # Oops
  File "<stdin>", line 1
    (a, b = 16, 19)
          ^
SyntaxError: invalid syntax

>>> (a, b := 16, 19) # This prints out a weird 3-tuple
(6, 16, 19)

>>> a # a is still unchanged?
6

>>> b
16
def some_func():
        # Assume some expensive computation here
        # time.sleep(1000)
        return 5

# So instead of,
if some_func():
        print(some_func()) # Which is bad practice since computation is happening twice

# or
a = some_func()
if a:
    print(a)

# Now you can concisely write
if a := some_func():
        print(a)

일반 할당연산자랑 바다코끼리 연산자는 뭐가 다를까요?
일반 할당연산자는 문(statement)이고 값을 반환하지 않고 다른 표현식 안에 포함될 수 없습니다.

바다코끼리 연산자는 표현식(expression)이고, 할당한 값을 반환하고, 다른 표현식 안에 포함될 수 있습니다. 그래서 엄밀히는 할당 표현식 연산자라고 하는게 맞습니다.

괄호 없는 warlus 연산자는 허용하지 않습니다.
warlus 연산자의 문법구조는 NAME := expr 형식이고 NAME은 유효한 식별자이고, expr은 유효한 표현식이고, 이 연산자가 iterable 패킹과 언패킹을 지원하지 않습니다.

>>> (a := 6, 9)
>>> ((a :=6),9) # 이거랑 동일 

>>> (a, b := 16, 19)
>>> (a, (b := 16), 19) # 이거랑 동일

틱택토 X의 승리

다음 코드를 보고 무엇이 출력될지 예상해보세요.

python

# Let's initialize a row
row = [""] * 3 #row i['', '', '']
# Let's make a board
board = [row] * 3
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

대부분의 사람들은 첫 번째 행의 첫 번째 칸에만 'X'가 들어갈 것이라고 예상합니다. 하지만 실제 결과는 모든 행의 첫 번째 칸에 'X'가 들어갑니다.

초기 보드:
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]

첫 번째 수: X가 (0,0)에 놓는다
['X', 0, 0]
['X', 0, 0]
['X', 0, 0]

이는 [[0] * 3] * 3가 같은 리스트 객체를 3번 참조하기 때문입니다. 즉, 세 개의 행이 모두 같은 메모리 위치의 리스트를 가리키고 있어서, 하나를 수정하면 모든 행이 함께 바뀝니다. 이를 얕은 복사(shallow copy) 문제라고 합니다.

올바른 방법은 다음과 같습니다

# 방법 1: 리스트 컴프리헨션
board = [[0] * 3 for _ in range(3)]

# 방법 2: 깊은 복사 사용
import copy
board = copy.deepcopy([[0] * 3] * 3)

가변 기본 인수(Mutable Default Arguments)

파이썬에서 가장 유명한 함정 중 하나입니다.

def add_item(item, lst=[]):
    lst.append(item)
    return lst

>>> print(add_item('사과'))
['사과']
>>> print(add_item('바나나'))
['사과','바나나']
>>> print(add_item('오렌지'))
['사과','바나나','오렌지']

이는 기본 인수가 함수 정의 시점에 한 번만 생성되어 모든 호출에서 같은 객체를 재사용하기 때문입니다.

올바른 방법은 이렇게 구현하는 것입니다.

def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

이런 가변 기본 인수 문제는 dataclass에서도 발생할 수 있습니다. dataclass는 이 문제를 해결하기 위해 default_factory라는 특별한 옵션을 제공합니다.

# 잘못된 방법 - 같은 함정에 빠집니다 
@dataclass 
class Student:
	name: str
	grades: List[int] = [] # 모든 학생이 같은 리스트를 공유!

# 올바른 방법 - default_factory 사용
@dataclass
class Student:
	name: str
	grades: List[int] = field(default_factory=list)

문자열 Identity

다음 코드의 결과를 예상해보세요

>>> a = "hello"
>>> b = "hello"
>>> print(a is b)
True

>>> x = "hello world"
>>> y = "hello world"
>>> print(x is y)
False

>>> c = "hello!"
>>> d = "hello!"
>>> print(c is d)
False

>>> # 하지만 한 줄에 쓰면?
>>> m, n = "hello!", "hello!"
>>> print(m is n)
True

>>> # 세미콜론으로 구분해도
>>> p = "hello!"; q = "hello!"
>>> print(p is q)
False

이 결과는 CPython의 문자열 인터닝(string interning) 최적화 때문입니다.

인터닝 규칙

  • 길이 0, 1인 모든 문자열은 자동으로 인터닝됩니다
  • 컴파일 타임에 결정됩니다 ('hello'는 인터닝되지만 ''.join(['h', 'e', 'l', 'l', 'o'])는 안됨)
  • ASCII 문자, 숫자, 언더스코어만 포함된 문자열만 인터닝됩니다 ('hello!'에서 ! 때문에 인터닝 안됨)

컴파일 단위의 차이

  • m, n = "hello!", "hello!": 하나의 statement → 같은 객체 생성
  • p = "hello!"; q = "hello!": 두 개의 statement → 각각 다른 객체

이는 대화형 환경에서 각 statement가 별도의 컴파일 단위로 취급되기 때문입니다.

숫자 캐싱

비슷한 현상이 숫자에서도 발생합니다:

>>> a = 256
>>> b = 256
>>> print(a is b)
True

>>> c = 257
>>> d = 257
>>> print(c is d) 
False  # 각각 다른 줄에서는 False!

>>> # 하지만 한 줄에 세미콜론으로 쓰면?
>>> x = 257; y = 257
>>> print(x is y)
True  # 같은 객체!

>>> # 반면 -5~256 범위는 어떻게 써도 항상 같습니다
>>> m = 100; n = 100  
>>> print(m is n)
True
>>> p = 100
>>> q = 100
>>> print(p is q)
True

파이썬은 -5부터 256까지의 정수를 미리 생성해두고 재사용합니다. 하지만 257 이상의 숫자는 코드가 어떻게 구성되느냐에 따라 다르게 동작합니다:

  • 각각 다른 줄: 별도의 statement로 파싱되어 다른 객체 생성
  • 한 줄에 세미콜론: 같은 코드 라인 내에서 컴파일 타임 최적화 적용

마무리

원작자의 repo를 확인하면, 더 많은 예시가 있습니다. 파이썬 중급서(fluent python, 파이썬 코딩의 기술) 이런 책을 읽어도 논리에 관한 부분은 실수를 하기가 참 쉽습니다만, 꾸준히 논리에 대한 System 2 사고를 활성화하여 코드의 예상치 못한 동작을 미리 예측하고 파이썬의 내부 동작 원리 이해하고 버그를 사전에 방지하는 습관을 형성합시다.

Reference

satwikkansal/wtfpython
Python의 조건식 평가에 대한 고찰
파이썬에서의 Truthy Falsy

profile
숭실대학교 소프트웨어학부생

0개의 댓글