Gorio Tech Blog search

파이썬 Error 처리

|

목차

1. Introduction

파이썬에서 에러를 처리하고 관리하는 데에는 다양한 이유가 있다. 실제 Applicaion 상에서 에러가 발생하지 않도록 개발과 테스트 단계에서 미리 에러를 식별하고 수정하는 것은, 어떤 프로그램을 만들 때 굉장히 중요한 과정이라고 할 수 있다.

기본적으로 파이썬에서는 BaseException이라는 class를 통해 에러를 관리하도록 도와준다. 이 class는 모든 내장 exception들의 base class이다. 만약 사용자가 직접 에러 class를 만들고 싶을 때는 이 에러를 사용하는 것이 아니라 Exception class를 사용해야 한다.

코딩을 하다보면 여러 종류의 에러를 보았을 것이다. 예를 들어 아래와 같은 에러가 대표적일 것이다.

ValueError
AssertionError
FileNotFoundError
SyntaxError

대체 이 에러들은 다 어떻게 만들어지고, 어떻게 구성되는 것일까? 사실 이 에러들은 앞서 설명한 BaseException class의 하위 class로 이루어진다. 그 전체 구조는 아래와 같다.

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

굉장히 많다. 이 에러와 경고(Warning)들을 다 외우고 있을 필요는 없을 것이다. 하지만 인지는 하고 있는 편이 좋다.


2. Exception 처리: try, except, finally

2.1. 일반적인 처리

try 블록을 수행하는 과정에서 에러가 발생하면 except 블록이 수행된다. 만약 에러가 발생하지 않았다면, except 블록은 수행되지 않는다. 만약 에러의 발생 유무와 상관없이 꼭 어떤 과정을 수행하고 싶다면 finally 블록에 이를 담으면 된다.

# 예시 1
try:
    import nothing
except ImportError as error:
    print(error)
finally:
    import numpy as np
    print(np.array([1, 2]))


No module named 'nothing'
[1 2]

# 예시 2
try:
    print(3/0)
except ZeroDivisionError:
    print("Error: You cannot divide integer by zero")

Error: You cannot divide integer by zero

참고로 assert 조건, "에러 메시지"assert 구문을 통해 에러를 관리할 수도 있다.

2.2. 특별한 요청

아래에는 위와는 다르게 조금은 특별한(?) 요청을 하고 싶을 때 사용할 수 있는 기능들이다.

  • 만약 에러를 그냥 회피하고 싶다면 except 블록에 pass를 입력하면 된다.
  • Exception이 발생하였을 때 프로그램을 중단하고 싶으면 raise SystemExit을 except 블록에 입력하면 된다.
  • Exception을 일부러 발생하고 싶을 때에도 raise 구문을 사용하면 된다.

3번 째 경우에 대한 예시를 첨부하겠다. BaseBandit이라는 부모 class가 있고, 사용자는 이 부모 class를 상속받아 TalkativeBandit이라는 자식 class를 만들고 싶다고 하자.

그런데 이 때, 자식 class에 반드시 operate이란 메서드를 구현하도록 미리 설정을 해두고 싶다. 모니터 구석에 메모를 해두는 것 외에 방법이 없을까? 이 때 부모 class인 BaseBandit에 미리 아래와 같은 코드를 구현해 놓으면 원하는 바를 쟁취할 수 있을 것이다.

# 부모 class 구현
class BaseBandit:
    def operate(self):
        raise NotImplementedError

# 자식 class 구현
class TalkativeBandit(BaseBandit):
    def stay(self):
        print("Don't talk")

tb = TalkativeBandit()

# 자식 class에서는 operate 메서드를 구현하지 않았으므로
# 부모 class의 operate 메서드가 호출된다.
tb.operate()

# 에러가 발생한다.
Traceback (most recent call last):
  File "C:\Users\...\interactiveshell.py", line 2961, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-17-fdf0f46c74b7>", line 1, in <module>
    tb.operate()
  File "<ipython-input-12-af85936c9668>", line 3, in operate
    raise NotImplementedError
NotImplementedError

operate 메서드를 제대로 구현한다면, 별 문제 없이 코드를 진행할 수 있을 것이다.


3. Exception 추적

바로 위의 예시를 보자. Traceback (most recent call last)란 문구를 볼 수 있을 것이다. 이는 Exception을 역으로 추적한다는 뜻이다.

사용자가 직접 추적 과정을 만들고 싶을 때 stack trace를 표시하고 출력하는 traceback 모듈과 로그 기록을 관리하는 logging 모듈을 사용하면 편리하다.

가장 기초적인 추적 방법은 아래와 같다.

import traceback

try:
    tuple()[0]
except IndexError:
    print("--- Exception Occured ---")
    traceback.print_exc(limit=1)

# 출력 결과
--- Exception Occured ---
Traceback (most recent call last):
  File "<ipython-input-19-0acccd16d042>", line 2, in <module>
    tuple()[0]
IndexError: tuple index out of range    

빈 튜플에 indexing을 시도했으므로 에러가 발생하는 것은 당연하다.
그 에러는 IndexError 인데, 우리는 traceback.print_exc 메서드를 통해 stack trace 정보를 출력할 수 있다.

limit=None이 기본이며 이 때는 제한 없이 stack trace를 출력한다. 위 예시와 같이 1을 입력하면 단 한 개의 stack trace 정보를 출력한다는 뜻이다. file, chain argument 설정을 통해 파일 출력 위치를 설정하거나 연쇄적인 Exception 출력 설정을 관리할 수 있다.

왜 이런 과정을 거쳐야 할까? 만약 이와 같이 try-except를 통해 Exception을 관리해주지 않는다면, 우리는 모든 에러를 잡기 전까지 프로그램 전체를 돌릴 수 없을 것이다.

이번에는 logging 모듈과 합작하여 Exception을 추적해보자.

import traceback
import logging

logging.basicConfig(filename="example.log", format="%(asctime)s %(levelname)s %(message)s")

try:
    tuple()[0]
except IndexError:
    logging.error(traceback.format_exc())
    raise

# 출력 결과
Traceback (most recent call last):
  File "C:\Users\...\interactiveshell.py", line 2961, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-18-16da8da0daa5>", line 6, in <module>
    tuple()[0]
IndexError: tuple index out of range

logging 모듈을 통해 우리는 example.log라는 파일에 에러에 관한 기록을 해둘 수 있었다.
이 파일에는 다음과 같은 로그 기록이 남아있다.

2020-01-12 18:38:50,633 ERROR Traceback (most recent call last):
  File "<ipython-input-18-16da8da0daa5>", line 6, in <module>
    tuple()[0]
IndexError: tuple index out of range

4. Exception 만들기

Exception class 상속을 통해 Exception을 직접 만들 수 있다.

import numpy as np

class SizeError(Exception):
    # 에러 메시지를 출력하고 싶으면 아래와 같은 특별 메서드를 구현해야 한다.
    def __str__(self):
        return "Size does not fit"
    
# 기준이 되는 base
base = np.eye(3)

# 비교대상인 data
data1 = np.array([[1,2], [3,4]])
data2 = np.ones((3, 3))

# np.array의 shape을 비교하는 함수이다.
def compare(base ,data):
    if base.shape != data.shape:
        raise SizeError()
    else:
        print("All Clear")

# 첫 번째 테스트
compare(base=base, data=data1)

# 첫 번째 결과
Traceback (most recent call last):
  File "C:\Users\...\interactiveshell.py", line 2961, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-36-c1718418c4b8>", line 1, in <module>
    compare(base=base, data=data1)
  File "<ipython-input-35-8ec7197ddfb7>", line 3, in compare
    raise SizeError()
SizeError: Size does not fit

# 두 번째 테스트
compare(base=base, data=data2)

# 두 번째 결과
All Clear

Reference

파이썬 공식문서
참고 블로그1 참고 블로그2