23. 예외처리




1 파이썬 예외의 종류


  • 구문 에러 (Syntax Error)

    • 문법적 에러
    • 이클립스 등의 통합개발환경 도구에서는 자동으로 실행 전에 구문 에러를 체크 해 줌
    • 파이썬은 상대적으로 언어적 문법이 간단하기 때문에 구문 자체의 에러 발생 비율이 낮으며, 통합개발환경 도구를 사용하여 완벽하게 제거할 수 있음
  • 논리 에러 (Logical Error)

    • 논리적 에러
    • 문법적 에러가 없으므로 프로그램이 실행이 되며, 비정상적으로 종료되지 않는 버그
    • 논리 오류는 즉시 인식되지는 않지만 의도치 않은 또는 바라지 않은 결과나 다른 행동을 유발
  • 예외 (Exception)

    • 문법적 에러는 없으나 프로그램 실행 중 더 이상 진행 할 수 없는 상황
    • 즉, 논리 에러에 해당함
    • 프로그램 실력은 이러한 예외 처리를 얼마나 꼼꼼하게 잘 하는가에 달려 있음
  • 파이썬에서의 예외 (Exception) 종류
    • 에러 (Error)
      • 대부분의 예외
      • 대부분의 예외들에 해당하는 클래스 이름은 Error 로 끝남
    • 경고 (Warning)
      • 논리적인 오류로 판단하기에 애매한 상황
      • 일단은 무시하여도 좋음
      • 하지만, 미래에는 다른 판단을 하여 에러로 변경될 수 있음

1-1 예외 발생 예제 보기

  • 예외 발생 예제 1: 정의되지 않은 변수 사용하기
    • NameError
In [1]:
4 + spam*3
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-c98bb92cdcac> in <module>()
----> 1 4 + spam*3

NameError: name 'spam' is not defined
  • 예외 발생 예제 2: 0으로 숫자 나누기
    • ZeroDivisionError
In [2]:
a = 10
b = 0 
c = a / b

# python2.x에서의 결과
# eroDivisionError: integer division or modulo by zero
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-2-41671660f656> in <module>()
      1 a = 10
      2 b = 0
----> 3 c = a / b

ZeroDivisionError: division by zero
  • [note] 예외가 발생하면 프로그램은 바로 종료된다.
In [3]:
def division():
    for n in range(0, 5):
        print(10.0 / n)

division()   
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-3-0a52054c4303> in <module>()
      3         print(10.0 / n)
      4 
----> 5 division()

<ipython-input-3-0a52054c4303> in division()
      1 def division():
      2     for n in range(0, 5):
----> 3         print(10.0 / n)
      4 
      5 division()

ZeroDivisionError: float division by zero
  • 예외 발생 예제 3: 문자열과 숫자 더하기
    • TypeError
In [5]:
'2' + 2

# python2.x에서의 결과
# TypeError: cannot concatenate 'str' and 'int' objects
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-687d63a8d4ed> in <module>()
----> 1 '2' + 2
      2 
      3 # python2.x에서의 결과
      4 # TypeError: cannot concatenate 'str' and 'int' objects

TypeError: must be str, not int
  • 예외 발생 예제 4: 참조 범위를 넘어서 인덱스 사용
    • IndexError
In [6]:
l = [1, 2]
print(l[2])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-6-08b5b9cc3f1f> in <module>()
      1 l = [1, 2]
----> 2 print(l[2])

IndexError: list index out of range
  • 예외 발생 예제 5: 등록되지 않은 키로 사전 검색
    • KeyError
In [7]:
d = {"a": 1, "b": 2}
print(d['c'])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-7-e0f66c508f6c> in <module>()
      1 d = {"a": 1, "b": 2}
----> 2 print(d['c'])

KeyError: 'c'
  • 예외 발생 예제 6: 있지도 않은 파일을 열려고 할 때
    • FileNotFoundError (Python3)
    • IOError (Python2)
In [1]:
a = open('aaa.txt')

# python2.x에서의 결과
# IOError: [Errno 2] No such file or directory: 'aaa.txt'
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-1-f82bc5689930> in <module>()
----> 1 a = open('aaa.txt')
      2 
      3 # python2.x에서의 결과
      4 # IOError: [Errno 2] No such file or directory: 'aaa.txt'

FileNotFoundError: [Errno 2] No such file or directory: 'aaa.txt'

1-2 내장 예외의 종류

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

2 예외 처리 방법


1-1 try/except/else/finally 절 사용하기

  • 예외가 발생할 수 있는 상황을 예상하여 예외 발생 상황을 전체 코드 흐름을 함께 제어할 수 있다.
  • try/except/else/finally 절
    • 구문

      try:
          (예외 발생 가능한) 일반적인 수행문들
      except Exception:
          예외가 발생하였을 때 수행되는 문들
      else:
          예외가 발생하지 않았을 때 수행되는 문들
      finally:
          예외 발생 유무와 관계없이 무조건 수행되는 문들

In [2]:
try:
    print(1.0 / 0.0)
except ZeroDivisionError:
    print('zero division error!!!')
zero division error!!!
  • 예외 처리를 하면 예외 발생시 프로그램 종료가 되지 않는다.
  • as 라는 구문을 통하여 예외 객체를 잡아서 처리할 수 있음.
In [23]:
#python3.x
def division():
    for n in range(0, 5):
        try:
            print(10.0 / n)
        except ZeroDivisionError as e:
            print("ZeroDivisionError 발생")
            print("-----" + str(e))
            print("-----" + e.__str__())
            print("-----" + str(e.__class__))

division()
ZeroDivisionError 발생
-----float division by zero
-----float division by zero
-----<class 'ZeroDivisionError'>
10.0
5.0
3.3333333333333335
2.5
In [24]:
#python2.x
def division():
    for n in range(0, 5):
        try:
            print(10.0 / n)
        except ZeroDivisionError, msg:
            print msg

division()
  File "<ipython-input-24-56454c2dfe89>", line 6
    except ZeroDivisionError, msg:
                            ^
SyntaxError: invalid syntax
  • else: 구문은 except: 구문없이 사용 못한다.
In [11]:
def division():
    for n in range(0, 5):
        try:
            print(10.0 / n)
        else:
            print("Success")

division() 
  File "<ipython-input-11-b3afe5cad960>", line 5
    else:
       ^
SyntaxError: invalid syntax
  • 상황에 따라서는 에러와 함께 따라오는 정보를 함께 받을 수도 있다.
    • 그러한 정보를 얻으려면 객체 자체를 print()에 넣어 출력하면 됨
In [6]:
#python3.x
try:
    spam()
except NameError as e:
    print('Error -', e)
Error - name 'spam' is not defined
In [25]:
#python2.7
try:
    spam()
except NameError, msg:
    print('Error -', msg)
  File "<ipython-input-25-0a2c38a0845a>", line 4
    except NameError, msg:
                    ^
SyntaxError: invalid syntax
  • try 절 안에서 간접적으로 호출한 함수의 내부 예외도 처리할 수 있다.
In [7]:
#python3.x
def zero_division():
    x = 1 / 0

try:
    zero_division()
except ZeroDivisionError as e:
    print('zero division error!!! -', e)
zero division error!!! - division by zero
In [18]:
#python2.x
def zero_division():
    x = 1 / 0

try:
    zero_division()
except ZeroDivisionError, msg:
    print 'zero division error!!! -', msg
zero division error!!! -  integer division or modulo by zero
In [15]:
def zero_division():
    x = 1 / 0

try:
    zero_division()
except ZeroDivisionError as msg:
    print('zero division error!!! -', msg)
    
# python2.x에서의 결과
# zero division error!!! - integer division or modulo by zero
zero division error!!! - division by zero
  • except 뒤에 아무런 예외도 기술하지 않으면 모든 예외에 대해 처리된다.
In [16]:
try:
    spam()
    print(1.0 / 0.0)
except:
    print('Error')
Error
  • 여러 예외들 각각에 대해 except 절을 다중으로 삽입할 수 있다.
In [17]:
b = 0.0
name = 'aaa.txt'
try:
    print(1.0 / b)
    spam()
    f = open(name, 'r')
    '2' + 2
except NameError:
    print('NameError !!!')
except ZeroDivisionError:
    print('ZeroDivisionError !!!')
except (TypeError, IOError):
    print('TypeError or IOError !!!')
else:
    print('No Exception !!!')
finally:
    print('Exit !!!')
ZeroDivisionError !!!
Exit !!!
  • 파일에서 숫자를 읽어와서 읽은 숫자로 나누기를 하는 예제
    • 꼼꼼한 예외 처리 예제
In [11]:
#python3.x
import os
print(os.getcwd())
filename = 't.txt'

try:
    f = open(filename, 'r')
except IOError as e:
    print(e)
else:
    a = float(f.readline())
    try:
        answer = 1.0 / a
    except ZeroDivisionError as e:
        print(e)
    else:
        print(answer)
    finally:
        print("Finally!!!")
        f.close()
/Users/yhhan/git/python-e-learning/python3.6
1.0
Finally!!!
In [9]:
#python3.x
import os
print(os.getcwd())
filename = 't.txt'

try:
    f = open(filename, 'r')
except IOError as e:
    print(e)
    f = open(default_filename, 'r')

a = float(f.readline())
try:
    answer = 1.0 / a
except ZeroDivisionError as e:
    print(e)
else:
    print(answer)
finally:
    print("Finally!!!")
    f.close()
/Users/yhhan/git/python-e-learning
1.0
Finally!!!

2-2 같은 부류의 예외 다 잡아내기

  • 예외 클래스들은 상속에 의한 계층 관계를 지니고 있기 때문에 이를 이용하면 여러 예외들을 한꺼번에 잡을 수 있다.
  • 예를 들어, ArithmeticError의 하위 클래스로서 FloatingPointError, OverflowError, ZeroDivisionError가 존재하기 때문에 이들 하위 클래스 예외가 발생하였을 경우 ArithmeticError로서 잡아낼 수 있다.
In [26]:
def dosomething():
    a = 1/0

try:
    dosomething()
except ArithmeticError:
    print("ArithmeticException occured")
ArithmeticException occured
  • 예외가 임의의 except에 의해 잡히면 다른 except에 의해서는 잡히지 않는다.
In [20]:
def dosomething():
    a = 1/0

try:
    dosomething()
except ZeroDivisionError:  # ZeroDivisionError는 이곳에서 잡힌다.
    print("ZeroDivisionError occured")
except ArithmeticError:    # FloatingPointError, OverflowError는 이곳에서 잡힌다.
    print("ArithmeticException occured")
ZeroDivisionError occured
In [20]:
def dosomething():
    a = 1/0

try:
    dosomething()
except ArithmeticError:
    print("ArithmeticException occured")
except ZeroDivisionError:  # 이곳에서 ZeroDivisionError는 잡히지 않는다. ==> 잘못된 코드
    print("ZeroDivisionError occured"  )  
ArithmeticException occured

3 예외 발생


3-1 raise로 예외 발생하기

  • 예외를 특정 상황 조건에서 raise 키워드를 통해 발생시킬 수 있다.
  • 아래 예는 시퀀스 형 클래스를 설계할 때 인덱싱을 구현하는 __getitem__ 메소드에서 인덱스가 범위를 넘을 때 IndexError를 발생시킨다.
In [27]:
class SquareSeq:
    def __init__(self, n):
        self.n = n
        
    def __getitem__(self, k):
        if k >= self.n or k < 0 :
            raise IndexError # 첨자 범위를 벗어나면 IndexError 예외를 발생시킴
        return k * k

    def __len__(self):
        return self.n
    
s = SquareSeq(10)
print(s[2], s[4])

for x in s:  # IndexError가 발생하는 시점까지 반복한다
    print(x,end=" ")

print(s[20])  # 첨자 범위가 넘었다
4 16
0 1 4 9 16 25 36 49 64 81 
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-27-3da74b713108> in <module>()
     17     print(x,end=" ")
     18 
---> 19 print(s[20])  # 첨자 범위가 넘었다

<ipython-input-27-3da74b713108> in __getitem__(self, k)
      5     def __getitem__(self, k):
      6         if k >= self.n or k < 0 :
----> 7             raise IndexError # 첨자 범위를 벗어나면 IndexError 예외를 발생시킴
      8         return k * k
      9 

IndexError: 

3-2 사용자 클래스 예외 정의 및 발생시키기

  • 사용자 정의 예외 클래스를 구현하는 일반적인 방법은 Exception 클래스를 상속 받아 구현한다.
    • Exception 클래스의 서브 클래스 중 하나를 상속 받아도 된다.
  • 사용자 정의 예외 발생 방법
    • 내장 예외 발생 방법과 동일하게 raise [클래스의 인스턴스] 와 같이 해당 예외 클래스의 인스턴스를 던진다.
  • 사용자 정의 예외를 잡는 방법
    • except [클래스 이름] 과 같이 해당 예외 클래스 이름을 사용한다.
  • 아래 예에서 except Big이 잡는 예외는 Big과 Small 이다.
    • 이유: Small은 Big의 하위 클래스이기 때문
In [29]:
class Big(Exception):
    pass

class Small(Big):
    pass
In [30]:
def dosomething1():
    x = Big()
    raise x
    
def dosomething2():
    raise Small()
    
for f in (dosomething1, dosomething2):
    try:
        f()
    except Big:
        print("Big Exception occurs!")
Big Exception occurs!
Big Exception occurs!

3-3 예외값 전달하기

  • raise 키워드 뒤에 예외와 함께, 추가 메시지를 함께 던질 수 있다.
    • python3에서는 지원하지 않음
  • python3에서는 예외의 생성자내에 메시지를 넣어서 예외 객체를 생성하여 던진다.
In [31]:
#python3.x
def f():
    raise Exception('message!!!')
    
try:
    f()
except Exception as e:
    print(e)
message!!!
In [13]:
#python2.x
def f():
    raise Exception, 'message!!!'
    
try:
    f()
except Exception, a:
    print a
message!!!
  • 생성자 안에 넣어준 에러 메시지는 except 키워드 사용시에 두 번째 인자로 해당 메시지를 받을 수 있다.
    • python3에서는 지원하지 않음
  • python3에서는 예외 객체를 받아서..
    • 객체 자체를 print() 하거나
    • 해당 객체의 __str__()을 호출하여 메시지 문자열을 얻어서 print() 한다.
In [15]:
#python3.x
a = 10
b = 0
try:
    if b == 0:
        raise ArithmeticError('0으로 나누고 있습니다.')
    a / b
except ArithmeticError as e:
    print(e)
0으로 나누고 있습니다.
In [28]:
#python2.x
a = 10
b = 0
try:
    if b == 0:
        raise ArithmeticError('0으로 나누고 있습니다.')
    a / b
except ArithmeticError, v:
    print v
0으로 나누고 있습니다.

참고 문헌: 파이썬(열혈강의)(개정판 VER.2), 이강성, FreeLec, 2005년 8월 29일