2.26. 오류 발생시키기#

함수는 예외를 발생(raise) 시켜 호출자에게 문제를 알릴 수 있습니다. 키워드는 raise 입니다:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

square_root(-1) 을 호출하면 raise 줄에서 멈추고, 함수 밖으로 점프하여 호출자에서 일치하는 except 를 찾습니다. 어떤 호출자도 그것을 잡지 않으면 스크립트는 트레이스백과 함께 종료됩니다.

2.26.1. 센티넬을 반환하는 대신 발생시키는 이유#

“잘못된 입력”을 알리는 두 가지 방법:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

보통은 예외 형태가 더 낫습니다:

  • 호출자는 오류 경우를 의도적으로 처리해야 합니다 – try 를 쓰거나, 예외가 전파되도록 두는 것입니다. 센티넬은 잊어버리기 쉽고 정상적인 결과로 착각하기 쉽습니다.

  • 오류 메시지는 예외와 함께 전달됩니다. 센티넬 방식은 그 진단 정보를 다른 어딘가에 따로 붙여야 합니다.

  • 처리되지 않은 예외에 대한 기본 동작은 문제가 된 호출을 가리키는 트레이스백과 함께 요란하게 크래시하는 것입니다. 조용한 None 반환은 나중에 미묘한 버그가 됩니다.

“찾지 못함”이 일상적이고 예외적이지 않은 결과일 때만 센티넬을 활용하십시오 – dict.get() 이 없는 키에 대해 None 을 반환하는 것은 바로 조회가 때때로 실패할 것으로 예상되기 때문입니다.

2.26.2. 사용자 정의 예외 클래스#

호출자가 내장 오류와 구별하고 싶어 할 수 있는 문제를 발생시키려면 Exception 의 서브클래스를 정의하십시오:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

class 본문도 괜찮습니다 – 호출자가 클래스로 잡기 때문에 중요한 것은 이름 자체입니다. 호출자가 한 블록에서 전체 부류를 잡고 싶어 할 수 있다면 관련 오류들을 공통 기반 아래 묶으십시오.

2.26.2.1. 다시 발생시키기#

except 블록 안의 빈 raise 는 현재 예외를 다시 발생시켜 다음 처리기로 전파되도록 합니다:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

이것은 함수가 오류를 실제로 처리하지 않으면서 관찰 하고 싶을 때(로그를 남기거나, 횟수를 세거나, 부분적인 변경을 되돌릴 때) 알맞은 형태입니다.

2.26.3. 언제 잡고 언제 전파할 것인가#

유용한 경험칙:

  • 의미 있게 복구할 수 있는 수준에서 예외를 잡으십시오 – 기본값으로 대체하거나, 재시도하거나, 잘못된 입력을 건너뜁니다.

  • 크래시 말고는 할 수 있는 유용한 일이 없을 때, 또는 복구하는 방법을 아는 것이 위쪽 계층일 때는 예외가 전파되도록 두십시오.

호출 스택 중간에 있는 함수가 모든 오류를 삼키고 조용히 반환하면 실패를 추적할 수 없게 됩니다. 예외가 정말로 대처 방안을 가진 코드에 도달할 때까지 전달되도록 두는 것을 선호하십시오.