파이썬 예외처리 try

파이썬 예외처리

파이썬 try 구문

파이썬은 오류를 처리하기 위해 try구문을 사용합니다. try 구문은 필수적으로 try와 except 구문을 사용해야 합니다. 이외에 선택적인 구문으로 else와 finally 구문이 있습니다.

 

파이썬의 전체 try 구문은 다음과 같습니다.

 

try:
	# 오류가 발생할 가능성이 있는 코드
except:
	# 오류 발생시 수행할 코드
else:
	# 오류가 발생하지 않았을 때 수행할 코드
finally:
	# 오류 발생에 상관없이 수행하는 코드

 

또한 이 try 구문과 짝을 이루는 예약어가 있습니다. 바로 raise 예약어입니다.

 

raise는 의도적으로 예외를 발생시킬 수 있습니다.

 

 

예외처리 try except

가장 발생시키기 쉬운 1 / 0 오류 연산을 발생시켜 보겠습니다.

 

>>> 1 / 0
Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    1/0
ZeroDivisionError: division by zero

 

이제 try 구문을 사용하여 이 오류를 처리해 보겠습니다.

 

>>> try:
...     1 / 0
... except:
...     print("오류 발생")
...
오류 발생

 

try 구문의 코드가 실행되고 에러가 발생하면서 except에 정의한 코드가 실행되었습니다. 하지만 1 / 0의 오류가 발생하는 에러는 "ZeroDivisionError"입니다. except 구문을 좀 더 타이트하게 "ZeroDivisionError" 일 때만 "오류 발생" 메시지를 출력해 보겠습니다.

 

>>> try:
...     1 / 0
... except ZeroDivisionError:
...     print("오류 발생")
...
오류 발생

 

그렇지만 오류는 ZeroDivisionError만 있는 것이 아닙니다. 파이썬 3에서 기본 예외 클래스 계층 구조는 다음과 같습니다.

 

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

 

이는 기본적인 예외 계층일 뿐 다른 예외 계층까지 확인한다면 훨씬 더 많은 예외가 존재합니다. 그렇다면 except 구문에 이 많은 예외를 전부 정의해야 할까요? 그렇지 않습니다. 필요한 예외만 정의하고 나머지 예외는 몽땅 싸잡아 처리하면 됩니다.

 

우선 의도적으로 raise 예약어를 활용해 "ZeroDivisionError"오류를 발생해 보겠습니다.

 

>>> try:
...     raise ZeroDivisionError('error error error')
... except ZeroDivisionError:
...     print("오류 발생")
...
오류 발생

 

try 구문에 1 / 0 코드 대신 raise를 사용했습니다. 만약 예외가 발생했지만 특별히 처리를 하지 않지만 어떤 오류가 발생했는지 알고 싶다면 raise를 한번 더 사용하면 됩니다.

 

>>> try:
...     raise ZeroDivisionError('error error error')
... except ZeroDivisionError:
...     print("오류 발생")
...     raise
...
오류 발생
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: error error error

 

raise를 한 번 더 사용하여 좀 깔끔하지는 않지만 어떤 오류 인지는 확인 가능합니다. 이 raise를 사용하는 데 있어 주의 사항이 있습니다.

 

※ raise 예약어 사용 시 주의 사항 ※

raise 예약어에 사용되는 예외 이름은 파이썬에서 정의된 예외 이름을 사용해야 합니다. 정의되지 않은 예외 이름을 사용해도 어차피 try구문에서 예외 발생을 하지만 이 예외는 raise에서 사용한 이름이 정의되지 않은 "NameError"만 발생시킬 뿐입니다.

 

이제 모든 예외 사항을 처리하는 방법을 알려 드리겠습니다.

 

>>> try:
...     1 / 0
... except Exception as err:
...     print("오류 발생", err)
...
오류 발생 division by zero

 

except 구문에 Exception을 사용하면 모든 예외를 처리하는 except구문을 만들 수 있습니다. 또한 as 구문으로 예외에서 발생하는 오류 내용을 확인할 수 있습니다.

 

그렇다면 except 구문을 정의할 때 확실히 처리해야 할 예외들과 그렇지 않은 예외로 구분해야 할 때가 있습니다. 이럴 때 다음과 같이 사용할 수 있습니다.

 

try:
    # 오류가 예상되는 코드
except ValueError:
    # ValueError 발생시 수행 코드
except TypeError:
    # TypeError 발생시 수행 코드 
except Exception as msg:
    # 기타 오류 발생시 수행 코드

 

만약에 위의 "ValueError"와 "TypeError"예외 처리 로직이 동일하다면 다음과 같이 코드를 줄일 수 있습니다. 동일한 기능을 수행하는데 코드가 짧고 가독성이 좋은 코드가 제일 좋은 코드이니까요. ^.^

 

try:
    # 오류가 예상되는 코드
except (ValueError, TypeError):
    # ValueError 와 TypeError
    # 발생시 수행 코드
except Exception as msg:
    # 기타 오류 발생시 수행 코드

 

 

예외처리 try except else finally

이제 선택 사항인 else구문과 finally 구문에 대해 알아보겠습니다.

맨 처음 설명드렸듯이 else 구문은 except가 수행되지 않았을 때 수행합니다. 마치 if... else와 같은 역할을 수행합니다. 그리고 finally는 try가 오류 발생과는 상관없이 무조건 실행됩니다.

 

솔직히 finally 구문은 필요하다 생각하지만 else구문은 그다지 필요할 것 같지 않습니다. 하지만 문법으로 잘 정돈된 코드를 보면 가독성이 훨씬 높아지니 정리가 필요할 때 적절히 사용하면 좋은 구문이 될 거 같습니다.

 

그러면 else 구문부터 확인해 보겠습니다.

 

###################################
# else 구문 미 실행
###################################
>>> try:
...     1 / 0
... except:
...     print("run except")
... else:
...     print("run else")
...
run except

###################################
# else 구문 실행
###################################
>>> try:
...     1 / 1
... except:
...     print("run except")
... else:
...     print("run else")
...
1.0
run else

 

위의 예제처럼 try 구문에 오류가 발생하지 않았을 때 else구문이 실행되는 것을 확인할 수 있습니다. 만약 이 코드의 else 구문을 없앤다면 다음과 같을 것입니다.

 

###################################
# else 구문 실행
###################################
>>> try:
...     1 / 1
...     print("run else")
... except:
...     print("run except")
...
1.0
run else

 

어차피 1 / 1의 코드 부분이 오류가 발생하면 "run else" 메시지가 출력하지 않을 것이고 오류가 발생하지 않으면 "run else" 메시지가 출력하기 때문에 결과적으로 동일한 결과를 가져오게 됩니다. 그러므로 else구문 사용은 프로그래머의 판단에 좌우합니다.

 

이제 마지막인 finally 구문 설명은 일단 예제부터 보여 드리겠습니다.

 

#################################
# 예외 발생해도 fillay 실행
#################################
>>> try:
...     1 / 0
... except:
...     print("run except")
... finally:
...     print("run finally")
...
run except
run finally


#################################
# 정상수행 되어도 fillay 실행
#################################
>>> try:
...     1 / 1
... except:
...     print("run except")
... finally:
...     print("run finally")
...
1.0
run finally

 

예제를 보면 좀 이상하지 않은가요? 마치 finally가 없어도 동일 기능 로직을 구현할 수 있을 것 같습니다.

 

try:
    # 실행 코드
except:
    print("run except")
    
print("run finally")

 

이렇게 로직을 구현하면 굳이 finally 구문을 따로 만들지 않아도 try 구문이 끝난 다음에 다음 라인이 실행되므로 finally 구문처럼 실행이 됩니다. 그렇다면 finally 조차도 불필요할까요?

 

아마도 불필요한 구문이었다면 굳이 이렇게 길게 설명 안 하고 try except 구문만 설명하고 이 글을 마쳤을 것입니다.

 

이 finally 구문은 소켓이나 파일을 open 한 상태에서 반복문을 처리할 때 오류 발생으로 close 할 때 아주 유용하게 사용됩니다.

 

while True:
    try:
        1 / 0
    except:
        print("run except")
        break
    finally:
        print("run finally")

# 실행 결과
run except
run finally

 

즉, 오류가 발생하여 except를 실행 후 while을 빠져나오기 위해 break구문을 실행했습니다. 이때 try구문이 실행 종료가 되면서 finally를 실행하여 "run finally" 메시지가 출력되었습니다. 

 

간혹 finally에 작성한 코드를 except에 선언하면 동일 코드를 만들 수 있지 않을까?라고 생각이 들 수 있습니다. 물론 맞습니다. 

 

위에서 잠깐 언급했던 소켓이나 파일을 open 했을 때 finally구문을 사용하지 않으면 정상적으로 끝났을 때와 예외 상황이 발생했을 때를 고려해서 close를 해야 합니다. 즉, 프로그래머에게 고민하고 기억해야 하는 사항을 증가시켜 줍니다. 하지만 이 finally구문을 적절히 사용하여 한 번만 close를 해놓으면 더 이상 close에 대해서는 더 이상 신경 쓰지 않아도 되는 해결사의 역할을 톡톡히 합니다.

 

MORE