Python은 배우기 쉽고 생산성이 높은 언어이지만, 그만큼 초보자부터 숙련자까지 누구나 한 번쯤 겪게 되는 고질적인 실수와 함정(gotcha)들이 존재합니다. 이 글에서는 Python 프로그래밍에서 가장 흔하게 발생하는 오류들을 살펴보고, 각각의 원인과 주의점을 정리해 드립니다.
1. 스코프 이름 탐색(LEGB 규칙) 문제
Python은 변수를 탐색할 때 LEGB(Local → Enclosing → Global → Built-in) 순서로 스코프 규칙을 따릅니다. 그런데 Python에는 엄격한 타입 바인딩이 없기 때문에, 외부 스코프의 변수를 다른 값으로 재할당해 버리면 나중에 외부 스코프에서 해당 변수를 사용할 때 의도하지 않은 값으로 대체되어 버리는 문제가 발생합니다.
2. is와 == 연산자의 혼동
is 연산자는 두 객체가 동일한 메모리 주소를 참조하는지를 확인합니다. 반면 == 연산자는 클래스에 정의된 __eq__ 메서드를 실행하며, 클래스마다 '같음'을 판단하는 기준이 다를 수 있습니다. 값 비교가 필요한 상황에서 is를 사용하면 예상치 못한 결과를 얻게 되므로 두 연산자의 차이를 명확히 이해해야 합니다.
3. 리스트를 순회하면서 수정하기
리스트를 반복(iteration)하는 도중에 요소를 삭제하면 IndexError가 발생할 수 있습니다. 이는 리스트의 크기가 줄어드는데도 인덱스가 이를 따라가지 못하기 때문입니다. 안전하게 요소를 제거하려면 새로운 리스트를 만들거나, 역순으로 순회하거나, 리스트 컴프리헨션을 활용하는 것이 좋습니다.
4. 클로저(Closure) 바인딩의 함정
다음 코드를 살펴보겠습니다.
예제 코드
listLambdas = [lambda x : i + x for i in range(5)]
for lam in listLambdas:
print(lam(10))
실행 결과
14 14 14 14 14
0, 11, 12, 13, 14가 출력될 것이라고 예상했다면 놀라실 수 있습니다. 결과가 모두 14인 이유는 클로저의 늦은 바인딩(late binding) 때문입니다. 리스트 안의 모든 람다는 변수 i 자체를 '참조'하고 있을 뿐, 생성 시점의 값을 복사해 두는 것이 아닙니다. 따라서 반복문이 끝난 후 i의 최종 값(4)을 기준으로 계산되어 모두 14가 출력되는 것입니다.
5. 내장 함수·내장 이름과의 충돌
sum, list, type 같은 이름으로 변수를 만든 경험이 있으신가요? 이렇게 하면 해당 이름의 내장 함수에 대한 참조를 덮어쓰게(reassign) 됩니다. 단순한 스크립트에서는 사소해 보이지만, 패키지나 모듈 이름을 이런 식으로 지으면 심각한 문제가 생길 수 있습니다. 다른 패키지가 표준 모듈 대신 여러분이 만든 클래스나 모듈을 잘못 임포트해 버리는 상황이 발생할 수 있기 때문입니다.
6. 직관적이지 않은 연산자 오버로딩
Python은 클래스에 대해 연산자 함수(__add__, __eq__ 등)를 오버로딩할 수 있는 강력한 기능을 제공합니다. 하지만 많은 개발자가 통상적이지 않은 방식으로 이러한 연산자를 구현하다 보니, 사용하기 어렵고 직관적이지 않은 복잡한 API가 만들어지곤 합니다. 연산자 오버로딩은 반드시 해당 기호가 일반적으로 의미하는 동작과 일치하도록 구현하는 것이 좋습니다.
마무리
위에서 소개한 실수들은 Python의 유연함이 만들어내는 전형적인 함정들입니다. LEGB 스코프 규칙, is와 ==의 차이, 클로저의 늦은 바인딩 등 핵심 개념을 정확히 이해하고 있다면 이런 문제들을 미리 예방할 수 있습니다. 코드를 작성하기 전에 이러한 함정들을 기억해 두면 디버깅 시간을 크게 줄일 수 있습니다.