본문 바로가기

진리는어디에/Python

[Python] 예외 처리(Exception Handling)

파이썬에서 에러를 처리하는 방법은 다양하다. 

간단한 처리 방법으로는 함수의 리턴 값을 이용하는 방법이 있다. 하지만 이는 호출자가 에러를 처리하도록 강제할 수 있는 방법이 없는 단점이 있다. 그래서 에러 상황에 대한 처리가 중요한 부분에서는 exception을 사용하여 에러 처리를 강제 할 수 있다.

이번 포스트에서는 파이썬의 예외 처리(exception handling)에 대해 자세히 살펴 보도록 하겠다.

None이나 False 리턴

함수 실패 시 None이나 False 또는 다른 미리 약속된 값을 반환하여 함수의 실패를 알린다. 호출자가 반드시 실패에 대한 처리를 할 필요는 없다. 주로 심각하지 않고 무시해도 되는 사소한 에러 처리를 할 때 이런 방식을 사용하면 좋다.

def foo(flag) :
    if 0 == flag :
        return False
    else if 2 == flag :
        return None
    return True
    
foo()     # 호출자가 반드시 실패를 처리 할 필요는 없다

if(None == foo()) :
    print('error')

NotImplemented 리턴

NotImplemented를 리턴하는 것은 주로 연산자 재정의(operator overloading)에서 사용되는 특수한 방법이다. 아래 예제의 15라인에서처럼 클래스 A에 '+' 연산, 즉, __add__ 스페셜 메소드가 호출 되는 연산을 호출 할 때, 스페셜 메소드에서 NotImplemented를 리턴하게 되면, 파이썬 인터프리터가 자동으로 리버스 스페셜 메소드 __rXXX__를 호출해준다.

일반적으로 자주 사용 되지는 않지만 연산자 오버로딩에서 요긴하게 쓰이므로 숙지해 두도록 하자.

class A :
    def __add__(self, other) :
        if not isinstance(other, A) :
            return NotImplemented
        print('A __add__')
        
class B :
    def __radd__(self, other) :
        print('B __radd')
        
a1 = A()
a2 = A()

a1 + a2  # 'A __add__'
a1 + b1  # 'B __radd_'

예외(Exception)

가장 강력한 에러 처리 방식으로써 호출자가 발생된 예외를 처리하지 않으면 프로그램이 종료 된다. 이번 포스트의 핵심 내용으로써 다음 섹션에서 예외 발생과 그 처리 방법에 대해 자세히 살펴 보겠다.

예외 발생 시키기 - raise

코드를 작성하다 예외를 발생 시키고 싶으면 raise 키워드에 예외 객체를 생성해 넘겨 주면 된다.

def foo() :
    raise NameError('Unknown Name')
    
foo()    # 호출자가 예외를 처리하지 않으면 에러를 출력하며 프로그램이 종료 된다.

# OUTPUT :
# Traceback (most recent call last):
#   File "D:\main.py", line 4, in <module>
#     foo()
#   File "D:\main.py", line 2, in foo
#     raise NameError('Unknown Name')
# NameError: Unknown Name

위 코드를 실행하면 NameError라는 예외(exception)이 발생함을 확인 할 수 있다. NameError 객체를 만들 때 인자로 넘겨준 문자열이 에러 로그에 같이 출력 된다.

예외 객체는 파이썬에서 미리 정의 되어 있는 예외 클래스들을 사용 할 수도 있고, 사용자가 정의한 예외 클래스를 사용 할 수도 있다. 이미 다양한 예외 상황에 대비해 파이썬에서 제공하는 예외 클래스들이 있으므로 어지간하면 파이썬의 표준 예외 클래스들을 사용 하는 것이 좋다. 파이썬에서 제공하는 예외 클래스들에 대해서는 아래 섹션에서 계속 알아 보도록 하겠다.

예외 처리 하기 - try ... except ...

앞에서 예외를 발생(raise) 시켜 프로그램이 종료되는 것을 확인 했다. 이번 섹션에서는 프로그램이 종료 되지 않게 하기 위해서 호출자가 예외를 처리(exception handling)하는 방법에 대해 알아보도록 하겠다.

예외를 처리하기 위해서는 try 와 except 키워드를 이용한다. try 블록 내에서 예외가 발생하면 프로그램을 중단하고 except 블록에 정의된 예외 처리 로직을 실행 한다. 

모든 예외 처리하기

try :
    foo()
    print('this message would be printed if no exception')
except :   # 모든 예외를 처리한다
    print('catch all exception here')
    
# OUTPUT :
#  catch all exception here

foo() 함수는 이전 섹션에서 작성한 NameError예외를 발생 시키는 foo() 함수다. 위 예제의 try 블록 내에서 foo() 함수 실행 중 예외가 발생하면 3라인의 print문을 건너 뛰고 바로 except 블록으로 넘어가 except 블록의 print 문을 실행 한다.

except 키워드에 아무런 인자를 지정하지 않고 사용하는 것은 모든 예외를 다 처리하겠다는 것을 의미한다. 

특정 예외만 처리하기

try :
    foo()
    print('this message would be printed if no exception')
except NameError :   # NameError 예외만 처리한다.
    print('catch NameError exception only')
    
# OUTPUT :
#  catch NameError exception only

except 키워드 뒤에 특정 예외 타입을 명시하면 해당 예외만 처리하고 나머지는 모두 무시하게 된다.

만일 아래 예처럼 except 키워드에 지정되지 않은 예외가 발생하는 경우, except 블록은 예외를 처리하지 못하고 프로그램은 종료 되게 된다.

def bar() :
    raise TypeError('Unknown Type') # 다른 종류의 예외 발생
    
try :
    bar()
    print('this message would be printed if no exception')
except NameError :   # NameError 예외만 처리한다. 하지만 TypeError 예외가 발생 했다.
    print('catch NameError exception only')
    
# OUTPUT :
# Traceback (most recent call last):
#   File "D:\main.py", line 16, in <module>
#     bar()
#   File "D:\main.py", line 13, in bar
#     raise TypeError('Unknown Type') # 다른 종류의 예외 발생
# TypeError: Unknown Type

예외 객체 처리하기

앞에서 우리는 예외를 발생(raise) 시킬 때, 예외 객체에 문자열 정보를 넘겨 주었다. 이런 예외에 대한 정보는, 예외 객체에 담겨져 except 블록으로 전달 된다. except 블록에서 예외에 대한 정보들을 처리하기 위해서는 'as' 키워드를 이용하여 예외 '객체'를 넘겨 받아야 한다.

try :
    foo()
    print('this message would be printed if no exception')
except NameError as e :   # e에 예외 객체를 넘겨 받는다
    print(f'catch NameError exception only. {e.args}')
    
# OUTPUT :
#  catch NameError exception only. ('Unknown Name',)

여러 종류 예외 처리하기

두개 이상의 예외를 처리하기 위해서는 아래와 같이 except 블록을 각 처리하고자 하는 예외마다 만들어 주거나, 만일 예외 처리 로직이 같다면 튜플을 이용해 묶어 하나의 블록으로 처리 가능하다.

# 각 예외 마다 다른 로직 처리
try :
    foo()
    print('this message would be printed if no exception')
except NameError as e :   # NameError 예외를 처리한다
    print(f'catch NameError exception only. {e.args}')
except TypeError as e :   # TypeError 예외를 처리한다
    print(f'catch TypeError exception only. {e.args}') 
    
# 튜플로 묶어 같은 로직으로 처리
try :
    foo()
    print('this message would be printed if no exception')
except (NameError, TypeError) as e :   # NameError와 TypeError를 동시에 처리한다
    print(f'catch exception only. {e.args}')

finally

예외를 처리하는 키워드 중 try, catch 외에 finally라는 키워드가 있다. 이는 try 블록이 정상적으로 수행 되든, 예외가 발생해 except 블록을 실행하든 무조건 실행되는 블록이다.

try :
    foo()
    print('this message would be printed if no exception')
except NameError as e :   
    print(f'catch NameError exception only. {e.args}')
finally :
    print(f'execute final code block') # 예외가 발생하더라도 실행
    
# OUTPUT :
# catch NameError exception only. ('Unknown Name',)
# execute final code block

def con() :
    pass
    
try :
    con()
    print('this message would be printed if no exception')
except NameError as e :   
    print(f'catch NameError exception only. {e.args}')
finally :
    print(f'execute final code block') # 예외가 발생하지 않더라도 실행

# OUTPUT :
# this message would be printed if no exception
# execute final code block

어렵지 않은 코드고 지겹도록 자주 사용하게 될 코드들이니까 외울 필요 없이 필요할 때 마다 한번씩 보다 보면 나도 모르게 저절로 몸에 익게 된다. 지금은 한번 전체적으로 살펴 보고 이런 방식으로 예외를 처리한다는 것만 알아 두고 넘어가면 된다.

표준 예외

이번 섹션에서는 파이썬의 모든 표준 예외들을 열거해 보도록 하겠다. 이전 강의 모듈에서 파이썬 모든 표준 타입들은 __builtins__내장 객체에 저장되어 있다고 이야기 했었다.

표준 예외도 역시 __builtins__내장 객체에 저장되어 있는데 'Error'로 끝나는 것들이 파이썬의 표준 예외다. 아래 예제는 __builtins__에서 파이썬의 표준 예외들을 순회하며 출력하고 있다.

exceptions = [ e for e in dir(__builtins__)
                 if e.endswith('Error')
             ]

for exception in exceptions :
    print(exception)

# OUTPUT :
# ArithmeticError
# AssertionError
# AttributeError
# BlockingIOError
# BrokenPipeError
# BufferError
# ChildProcessError
# ...

사용자 정의 예외

파이썬은 Exception 클래스를 상속 받아 사용자가 정의한 예외 클래스를 직접 정의 할 수 있다. 하지만 대부분의 예외 상황에 대한 예외 클래스는 표준에서 충분히 제공하고 있으므로 어지간하면 사용자 정의 예외 클래스를 직접 만들어 쓰기 보다는 표준 예외 클래스를 사용 할 것을 추천한다.

class CustomError(Exception) :
    pass
    
def con() :
    raise CustomError()  # 사용자 정의 예외 발생
    
con()

# OUTPUT :
# Traceback (most recent call last):
#   File "D:\main.py", line 7, in <module>
#     con()
#   File "D:\main.py", line 5, in con
#     raise CustomError()  # 사용자 정의 예외 발생
# __main__.CustomError

마치며

이상 지금까지 예외 발생에서 부터, 처리, 사용자 정의 예외 타입만드는 부분까지 파이썬 예외 처리에 관한 모든 것을 살펴 보았다. 포스트는 길지만 내용 자체는 그렇게 복잡하지 않으니 일일이 기억할 필요 없이 일단 읽어 두기만 하자. 나중에 사용 할 때 생각이 잘 안나면 다시 들춰 보면 된다. 몇 번 반복하다 보면 자연스레 몸에 익게 된다.

여기 까지 파이썬의 기본적인 부분에 대해서는 모두 살펴 보았다. 다음 포스트 부터는 파이썬의 비동기 입출력(asyncio)에 대해서 살펴 보도록 하겠다.

강의의 마지막이 얼마 남지 않았다. 조금만 더 힘내서 끝까지 가보자.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!