7.1 함수 인자의 기본값으로 표현식의 잘못된 사용
많은 것을 배웠으니, 신입생들에게 가장 흔한 실수를 살펴보자 – 다른 사람의 실수를 통해 배우는 것이 자신의 실수를 통해 배우는 것보다 낫지, 안 그래?
Python은 함수에 선택적 인자가 있을 수 있음을 기본값으로 설정하여 지정할 수 있게 해줘. 물론, 이것은 아주 편리한 기능이지만, 이러한 값의 타입이 변경 가능한 경우 좋지 않은 결과를 초래할 수 있어. 예를 들어, 다음과 같은 함수 정의를 보자:
def foo(bar=[]): # bar는 선택적 인자로 기본값은 빈 리스트야.
bar.append("baz") # 이 줄이 문제가 될 수 있어...
return bar
여기서 흔히 하는 오해는 선택적 인자의 값이 함수가 이 인자에 대한 값 없이 호출될 때마다 기본값으로 설정된다고 생각하는 거야.
위의 코드에서, 예를 들어, foo() 함수를 반복해서 호출할 때 (즉, 인자 bar의 값을 지정하지 않을 때) 항상 ["baz"]를 반환할 것이라 생각할 수 있어. 왜냐하면 foo()가 호출될 때마다 (인자 bar를 지정하지 않을 때) bar가 []로 (즉, 새로운 빈 리스트) 설정된다고 가정하기 때문이야.
하지만 실제로 어떤 일이 일어나는지 보자:
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz", "baz"]
result = foo()
print(result) # ["baz", "baz", "baz"]
왜 foo()를 호출할 때마다 기존 리스트에 baz 값을 계속 추가하느냐고? 새로운 리스트를 생성하지 않고?
이 질문에 대한 답은 Python의 '내부'를 좀 더 깊이 이해하는 데 있어. 바로, 함수의 기본값은 한 번만 초기화된다, 즉 함수가 정의될 때 단 한 번!
따라서, 인자 bar는 기본값 (즉, 빈 리스트)으로 단 한 번, foo()가 처음 정의될 때 초기화되고, 이후에 foo()를 호출할 때 (즉, 인자 bar를 지정하지 않을 때) 항상 초기 정의 시 생성된 동일한 리스트를 사용하게 되는 거야.
이 실수에 대한 일반적인 "해결책"은 다음과 같은 정의야:
def foo(bar=None):
if bar is None:
bar = []
bar.append("baz")
return bar
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
7.2 클래스 변수의 잘못된 사용
다음 예제를 보자:
class A(object):
x = 1
class B(A):
pass
class C(A):
pass
print(A.x, B.x, C.x)
# 1 1 1
모두 잘 되네.
이제 B 클래스의 x 필드에 값을 할당해보자:
B.x = 2
print(A.x, B.x, C.x)
# 1 2 1
예상대로 잘 되었어.
이제 A 클래스의 변수를 변경해보자:
A.x = 3
print(A.x, B.x, C.x)
# 3 2 3
이상하네, 우리는 A.x만 변경했는데, 왜 C.x도 변경됐지?
Python에서는 클래스 변수가 딕셔너리처럼 처리되고, 메서드 해상도 순서(MRO)라고 불리는 규칙을 따르기 때문이야. 위 코드에서, 속성 x가 클래스 C 안에서는 발견되지 않기 때문에, 그것은 기본 클래스들에서 (위 예제에서는 A만 있지만, Python은 다중 상속을 지원하지) 찾아지게 돼.
다른 말로, C는 A와 독립적인 x 속성을 가지고 있지 않아. 따라서, C.x에 대한 참조는 사실 A.x에 대한 참조인 거야. 이러한 경우를 적절히 고려하지 않으면 문제가 될 수 있어. Python을 배우면서 클래스 속성과의 작업에 특별한 주의를 기울이자.
7.3 예외 처리 블록의 잘못된 매개변수 지정
다음 코드 조각을 보자:
try:
l = ["a", "b"]
int(l[2])
except ValueError, IndexError: # 두 예외를 모두 잡으려고 했지?
pass
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
여기서 문제는 except 표현식이 그렇게 지정된 예외 목록을 수락하지 않는다는 거야. 결과적으로 위 코드에서는 IndexError 예외가 except 표현식에 의해 잡히지 않고, 대신 IndexError라는 이름의 매개변수에 바인딩돼.
중요! 인터넷 예제에서 이런 코드를 볼 수 있는데, Python 2.x에서 사용되던 방식이야.
except 표현식을 사용하여 여러 예외를 잡는 올바른 방법은 첫 번째 매개변수를 잡고자 하는 모든 예외를 포함하는 튜플로 지정하는 것이야. 또한, 더 읽기 쉽도록 as 키워드를 사용할 수도 있어, 예를 들어 이렇게:
try:
l = ["a", "b"]
int(l[2])
except (ValueError, IndexError) as e:
pass
GO TO FULL VERSION