유틸리티 함수 작성 시 반환 값 None에 특별한 의미를 부여하는 경향이 있어요.
예를들어 어떤 숫자를 다른 숫자로 나누는 헬퍼 함수를 생각해보죠. 0으로 나누는 경우에는 결과가 정의되어 있지 않기 때문에 None을 반환하는게 자연스러워요.
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
assert divide(4, 2) == 2
assert divide(0, 1) == 0
assert divide(3, 6) == 0.5
assert divide(1, 0) == None
이 함수를 사용하는 코드는 반환 값을 다음과 같이 해석해요
x, y = 1, 0
result = divide(x, y)
if result is None:
print('Invalid inputs')
else:
print('Result is %.1f' % result)
Invalid inputs
분자가 0이 되면 어떤 일이 일어날까? 반환 값도 0이 되어버려요.(분모가 0이 아닐 경우) 그러면 if 문과 같은 조건에서 결과를 평가 할 때 문제가 될 수 있어요. 오류인지 알아내려고 None대신 실수로 False에 해당하는 값을 검사 할 수도 있어요.
x, y = 0, 5
result = divide(x, y)
if not result: # result == 0
print('Invalid inputs') # This is wrong!
else:
assert False
Invalid inputs
이 예는 None에 특별한 의미가 있을 때 파이썬 코드에서 흔히 하는 실수다. 바로 이 점이 함수에서 None을 반환하면 오류가 일어나기 쉬운 이유에요. 오류가 일어나는 상황을 줄이는 방법은 2가지에요.
첫 번째 방법은 반환 값을 2개로 나눠서 튜플에 담는것이에요. 튜플의 첫 번째 부분은 작업이 성공했는지 실패했는지를 알려줘요. 두 번째 부분은 계산된 실제 결과에요.
def divide(a, b):
try:
return True, a / b
except ZeroDivisionError:
return False, None
x, y = 5, 0
success, result = divide(x, y)
if not success:
print('Invalid inputs')
Invalid inputs
이 함수를 호출하는 쪽에서는 튜플을 풀어야해요. 따라서 나눗셈의 결과만 얻을게 아니라 튜플에 들어 있는 상태 부분까지 고려하니깐요.
문제는 호출자가 튜플의 첫 번째 부분을 쉽게 무시할 수 있다는 점입니다. 얼핏 보기에는 이렇게 작성한 코드가 나빠 보지이 않아요. 하지만 결과는 그냥 None을 반환하는 것만큼이나 나빠요.
x, y = 5, 0
_, result = divide(x, y)
if not result:
print('Invalid inputs') # This is right
x, y = 0, 5
_, result = divide(x, y)
if not result:
print('Invalid inputs') # This is wrong
Invalid inputs
Invalid inputs
이런 오류를 줄이기에 더 좋은 두 번째 방법은 절대로 None을 반환하지 않는 것이에요 대신 호출하는 쪽에 예외를 일으켜서 호출하는 쪽에서 그 예외를 처리하게 하는 것입니다. 여기서는 호출하는 쪽에 입력값이 잘못됐음을 알리려고 ZeroDivision을 ValueError로 변경했어요.
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
Result is 2.5
이제 호출하는 쪽에서는 잘못된 입력에 대한 예외를 처리해야해요. 호출하는 쪽에서는 더는 함수의 반환 값을 조건식으로 검사할 필요가 없어요. 함수가 예외를 일으키지 않았다면 반환값은 문제가 없어요. 예외를 처리하는 코드도 깔끔해지고요.