오류와 예외

프로그래밍에서 오류 또는 실수는 종종 버그라고 불린다. 이것들은 거의 대부분 프로그래머의 잘못으로부터 야기된다. 오류를 발견하고 제거하는 작업을 디버깅이라고 부른다. 오류는 대략 3가지로 분류한다.

  • 구문 오류(syntax error)
  • 실행시간 오류(runtime error)
  • 논리 오류(logical error)

구문 오류

구문 오류는 파이썬이 프로그램 구문 분석(parsing)을 시도할 때 발생하며 오류 메시지를 출력하고 프로그램을 종료한다. 구문 오류는 파이썬 언어 문법을 잘못 사용하기 때문에 발생한다. 구문 오류는 다음과 같은 상황에서 발생한다.

  • 예약어를 빠뜨릴 때
  • 예약어 사용 위치가 잘못 되었을 때
  • 괄호들을 빠뜨릴 때
  • 예약어 철자가 잘못되었을 때
  • 들여쓰기가 잘못되었을 때
  • 구역이 비었을 때(가령, if 문에서 콜론 다음에 몸통 구역에 문장이 존재해야 한다.)

구문 오류 예제들

함수를 정의할 때 예약어 def가 빠졌다

In [1]:
함수(x, y):
    return x + y
  File "<ipython-input-1-d815db7e4bbb>", line 1
    함수(x, y):
             ^
SyntaxError: invalid syntax

else는 반드시 if와 함께 사용해야 한다.

In [2]:
else:
    print('뭐가 잘못되었을까?')
  File "<ipython-input-2-6cdacc793d72>", line 1
    else:
       ^
SyntaxError: invalid syntax

if 문은 콜론(:) 으로 구역 시작을 알려야 한다.

In [3]:
if 4 < 5
    print('참이네요.')
  File "<ipython-input-3-f4f29a21d165>", line 1
    if 4 < 5
            ^
SyntaxError: invalid syntax

else 예약어 철자가 잘못되서 오류가 발생한다.

In [5]:
if True:
    print("참이군요.")
esle:
    print("거짓입니다.")
  File "<ipython-input-5-6f0d2bba34c2>", line 3
    esle:
         ^
SyntaxError: invalid syntax

if 문 몸통 구역 시작은 들여쓰기가 되어야 한다.

In [6]:
if :
print("참이군요.")
  File "<ipython-input-6-fe96636da4c4>", line 2
    print("참이군요.")
        ^
IndentationError: expected an indented block

실행시간 오류

구문 오류가 발생하지 않아 파이썬 인터프리터가 프로그램을 실행을 하는 도중 뜻하지 않게 오류가 발생해서 프로그램이 종료가 되기도 한다. 이런 오류를 실행시간 오류라고 한다. 실행 시간 오류는 파이썬 언어 문법에 위배되지는 않지만 실행이 불가능한 문장을 만날 때 발생한다. 일상적인 언어의 예를 들면 “두 팔을 퍼덕여 제주도로 날아가세요”라는 문장은 문법적으로 문제가 없지만 우리가 할 수 없는 명령을 내린 것이 된다. 흔히 발생하는 실행시간 오류는 다음과 같다.

  • 0으로 나눌 때
  • 적합하지 않은 타입들과 연산할 때
  • 정의되지 않은 식별자를 사용할 때
  • 존재하지 않는 인덱스에 접근할 때
  • 존재하지 않는 파일을 접근할 때

실행시간 오류 예제들

다음은 사과라는 변수가 정의되어 있지 않기 때문에 NameError 에러가 난다.

In [4]:
print(사과)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-4-9bd937b01ef4> in <module>()
----> 1 print(사과)

NameError: name '사과' is not defined

다음은 문자 "사과"와 정수 100을 더할 수 없기 때문에 TypeError 에러가 난다.

In [2]:
"사과" + 100
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-2ac8e5e832c6> in <module>()
----> 1 "사과" + 100

TypeError: must be str, not int

다음은 리스트의 크기가 3개이지만 인덱스는 0부터 시작하기 때문에 3번째 인덱스에 해당하는 성분이 존재하지 않기 때문에 IndexError 에러가 발생한다.

In [4]:
리스트 = ['ㄱ', 'ㄴ', 'ㄷ']
리스트[3]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-4-6d133e5e254c> in <module>()
      1 리스트 = ['ㄱ', 'ㄴ', 'ㄷ']
----> 2 리스트[3]

IndexError: list index out of range

논리 오류

논리 오류는 어디가 잘못되었는지 찾기가 가장 어려운 오류이다. 프로그램을 실행하면 구문 오류나 실행시간 오류없이 결과가 나오지만 그 결과가 잘못된 결과가 나오는 오류를 논리 오류라고 한다. 논리 오류를 일으키기 쉬운 것들은 다음과 같다.

  • 다른 변수이름 사용할 때
  • 들여쓰기 수준이 잘못되었을 때
  • 부동소수점형 계산에 정수형 연산을 할 경우
  • 연산 순서가 잘못되었을 경우
  • 논리형 식을 잘못 사용할 경우

예외

파이썬에서 예외란 모든 구문 오류 및 실행시간 오류를 말한다. 예를 들면 파일을 읽으려고 할 때 그 파일이 존재하지 않는 경우라던지, 또는 배열의 크기를 넘은 인덱스를 참조하는 경우이다. 이러한 상황을 처리해 주는 것을 예외 처리라고 한다. 파이썬에서는 아무런 처리를 하지 않는 예외에 대하여 자동으로 오류를 일으키며, 오류 문장을 출력하고 프로그램을 종료한다.

파이썬 내장 예외는 다음과 같은 클래스 구조로 되어 있다. 시스템 종료 예외를 제외한 대부분의 예외들은 Exception 클래스의 하위 클래스이다.

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

다음은 자주 보게되는 예외들이다.

클래스 이름 설명
Exception 모든 내장 예외의 기본이 되는 클래스로 사용자 정의 예외를 작성하고자 하며 이 클래스를 상속받아 구현해야 한다.
NameError 지역, 전역 이름공간 중에서 유효하지 않은 이름을 접근하는 경우 발생한다.
OSError 시스템 관련 에러이다.
SyntaxError 구문 오류로 발생하는 예외이다
TypeError 부적절한 타입의 객체에 값을 할당하는 경우 발생하는 예외이다.

예외 처리

예외가 발생할 가능성이 있는 문장들을 `try <https://docs.python.org/3/reference/compound_stmts.html#try>`__ 문을 이용해서 처리할 수 있다. try 문은 일반적으로 다음과 같이 사용한다. 예외가 발생하지 않으면 else 절이 시행이 되고 finally 절은 예외 발생과 상관없이 항상 실행이 된다.

try ... except ...

예외 발생이 가능한 문장을 try 절에 작성하고 예외 발생시 처리를 할 수 있는 문장을 except 절에 작성한다.

try:
    <예외 발생 가능 문장>
except <예외 타입>:
    <예외 처리 문장>

다음 코드에서 try 구역에 있는 문장들 중에 ValueError가 발생하면 발생한 줄 밑으로 실행하는 것을 멈추고 except 절로 진입을 한다. except 절 안에 있는 문장들을 모두 실행한 후에는 try ... except ... 다음 줄부터 실행을 계속한다. 만일 except 절 안에서 다시 오류가 발생하면 오류가 발생한 줄에서 실행을 멈추고 예외를 발생시킨다.

try:
    나이 = int(input("나이를 입력하세요: "))
    print("당신의 나이는 {:d} 살입니다.".format(나이))
except ValueError:
    print("숫자를 입력하세요.")

여기서는 int() 함수의 인자가 숫자 문자열이 아니면 ValueError가 발생한다. 따라서 except 다음에 ValueError 타입을 적어 준 것이다.

except 다음에 예외 타입을 명시하지 않으면 모든 예외에 대해서 처리를 한다. 이러한 것은 좋지 않은 방법이다. 우리가 예측할 수 없는 심각한 예외가 발생하면 처리는 되겠지만 어떠한 예측 불가능한 예외가 발생했었는지를 알 수 없게 된다.

튜플 형식을 이용해서 두 개 이상의 서로 다른 예외들도 함께 처리할 수 있다.

try:
    분자 = int(input("분자를 입력하세요: "))
    분모 = int(input("분모를 입력하세요: "))
    print("{:d} / {:d} = {:f}".format(분자, 분모, 분자/분모))
except(ValueError, ZeroDivisionError):
    print("앗! 뭔가 잘못되었습니다.")

위와 같은 경우는 숫자 아니거나 분모가 0일 때 예외 처리가 되므로 무엇때문에 예외가 발생했는지 명확히 알 수 없다.

try:
    분자 = int(input("분자를 입력하세요: "))
    분모 = int(input("분모를 입력하세요: "))
    print("{:d} / {:d} = {:f}".format(분자, 분모, 분자/분모))
except ValueError:
    print("숫자를 입력하세요.")
except ZeroDivisionError:
    print("분모는 0이 될 수 없습니다.")

위와 같이 except 절이 여러 개일 때 위에서 부터 순차적으로 예외를 적용하게 된다. 예외 처리가 어느 시점에 처리가 되면 그 이후에 있는 예외 타입에 대해서는 처리를 하지 않고 try 문을 마친다. 만일 예외가 어느 곳에서도 처리되지 않으면 그 예외를 발생시킨다.

또한 위 경우에 ValueError가 분모에서 발생했는지 분자에서 발생했는지 알 수 없다. 이것을 구분하기 위해서는 각각에 대해서 try 문을 사용해야 한다.

try:
    분자 = int(input("분자를 입력하세요: "))
except ValueError:
    print("분자를 숫자로 입력하세요.")

try:
    분모 = int(input("분모를 입력하세요: "))
except ValueError:
    print("분모를 숫자로 입력하세요.")

try:
    print("{:d} / {:d} = {:f}".format(분자, 분모, 분자/분모))
except ZeroDivisionError:
    print("분모는 0이 될 수 없습니다.")

일반적으로 광범위하게 오류를 처리하는 코드를 작성하는 것보다 작은 범위에 구체적인 오류를 처리하는 것이 더 낫다. 이렇게 되면 코드가 길어지게 되지만 반복문을 적당히 사용함으로 어느 정도 해결할 수 있을 것이다.

elsefinally

try - except 문에 필요하면 elsefinally 절을 순서대로 뒤에 추가할 수 있다.

else 절은 try 절에서 오류가 발생하지 않을 때만 실행이 된다.

In [ ]:
try:
    나이 = int(input("나이를 입력하세요: "))
except ValueError:
    print("숫자를 입력하세요!")
else:
    print("당신의 나이는 {:d} 살입니다.".format(나이))

try 절에서 예외가 발생하지 않으면 finally 절의 모든 문장을 실행하고 try 문을 끝낸다. try 절에서 예외가 발생하면 try 절의 나머지 문장을 건너 띄고 except 절에 나열된 예외와 발생된 예외가 일치하는 것을 찾는다. 일치하는 예외를 찾으면 그 except 절 문장을 실행한 후 finally 절을 실행한 후 끝낸다. 일치하는 예외를 찾지 못하면 finally 절을 수행한 후 다시 같은 예외를 발생시킨다.

In [11]:
try:
    print("예외 발생 전")
    x = 2/0
    print("예외 발생 후")
    x = "2" + 2
except ZeroDivisionError:
    print("0으로 나누는 에러 발생.")
except Exception as e:
    print(e)
    print("예외 문장")
finally:
    print("파이널리 문장.")
예외 발생 전
0으로 나누는 에러 발생.
파이널리 문장.

직접하기

  • 위 코드에서 6줄과 8줄을 바꾸면 어떤 결과가 출력되는가? 이유를 설명하시오.

try ... finally

try 안에서 예외가 발생하면 finally 절로 제어가 넘어가고 finally 절이 모두 수행되면 동일 예외가 다시 발생된다. try 절에서 예외가 발생되지 않으면 try 절의 모든 문장을 수행하고 finally 절의 모든 문장을 실행한다.

In [12]:
try:
    print("예외 발생 전")
    x = 2/0
    print("예외 발생 후")
finally:
    print("파이널리 문장.")
예외 발생 전
파이널리 문장.
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-12-9a23210ec287> in <module>()
      1 try:
      2     print("예외 발생 전")
----> 3     x = 2/0
      4     print("예외 발생 후")
      5 finally:

ZeroDivisionError: division by zero
  • 3줄에서 ZeroDivisionError가 발생하면 다음 문장부터는 수행하지 않고 finally 절로 이동하여 finally 절의 모든 문장을 수행한다. 그리고 다시 ZeroDivisionError 예외를 발생시키므로 에러문이 출력이 된다.
try:
    <예외 발생 가능 문장>
except <예외>:
    <예외 처리 문장>
except (예외1, <예외2>, ..., <예외n>):
    <예외 처리 문장>
except 예외 as 변수:
    <예외 처리 문장>
else:
    <예외가 발생하지 않은 경우에 실행되는 문장>
finally:
    <예외 발생과 상관없이 항상 수행되는 문장>

예외 인스턴스 전달

내장 예외가 발생하는 경우 단순히 예외 발생 여부뿐만 아니라, 추가적인 정보도 예외 인스턴스 객체의 인자에 전달된다. 이 정보를 이용하기 위해서는 예외 클래스의 인스턴스 객체를 변수로 할당하여 사용하면 된다. 다음 예제는 as 구문으로 예외 인스턴스 객체의 추가적인 정보를 출력한다.

In [7]:
try:
    1 + "a"
except ZeroDivisionError:
    print("0으로 나누는 에러.")
except TypeError as e:
    print("타입이 같아야 합니다.", e.args[0])
except:
    print("에러가 발생했지만 어떤 종류인지 알 수 없다.")
타입이 같아야 합니다. unsupported operand type(s) for +: 'int' and 'str'

부모 클래스 이용

예외 클래스 계층 구조에서 부모 클래스를 except 구문으로 에러를 처리하면 자식 클래스도 같은 에러를 처리할 수 있다.

In [9]:
try:
    1/0
except ArithmeticError:
    print("수와 관련된 에러.")
except TypeError:
    print("타입이 같아야 합니다.")
except:
    print("에러가 발생했지만 어떤 종류인지 알 수 없다.")
수와 관련된 에러.

ZeroDivisionError, OverflowError, FloatingPointError에 관한 에러가 발생하면 이들의 부모 클래스인 ArithmeticError에 의해서 처리할 수 있다.

예외 발생시키기

raise 문을 이용하여 예외를 발생시킬 수 있다. 다음과 같은 3가지 형식으로 사용할 수 있다.

  • raise [Exception]: 해당 예외를 발생한다.
  • raise [Exception(arg)]: 예외 발생시 관련 인자(arg)를 전달한다.
  • raise: 가장 가까이에 발생된 예외를 그대로 다시 발생시킨다.
In [14]:
raise Exception("내가 예외를 발생시켰다.")
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-14-fc15340b6eb4> in <module>()
----> 1 raise Exception("내가 예외를 발생시켰다.")

Exception: 내가 예외를 발생시켰다.

다음은 raise 문 위에서 발생된 예외가 없기 때문에 전달할 예외가 없다는 오류가 발생한다.

In [14]:
raise
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-14-26814ed17a01> in <module>()
----> 1 raise

RuntimeError: No active exception to reraise

try 절 안에서 raise를 실행시키면 앞에서 살펴본 것같이 전달한 예외가 없다는 오류가 발생하면서 except 절로 넘어가서 예외 처리를 한다.

In [15]:
try:
    raise
except Exception as e:
    print("예외 발생.", e)
예외 발생. No active exception to reraise

다음은 구체적으로 raise를 이용해서 NameError를 발생시키고 그 예외를 except 절에서 처리한다.

In [16]:
try:
    raise NameError
except NameError:
    print("이름 예외가 발생했습니다.")
이름 예외가 발생했습니다.

raise를 이용하여 가장 가까운 예외를 다시 발생시킨다.

In [17]:
try:
    1/0
except Exception as e:
    print("예외 발생:", e)
    raise
예외 발생: division by zero
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-17-a67fbbf7c7a8> in <module>()
      1 try:
----> 2     1/0
      3 except Exception as e:
      4     print("예외 발생:", e)
      5     raise

ZeroDivisionError: division by zero

다음은 raise가 발생시키는 예외를 살펴본 것이다.

In [18]:
try:
    1/0
except Exception as e:
    print("예외 발생:", e)
    1 + "ㅁ"
    10 / 0
    raise
예외 발생: division by zero
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-18-268df49e81a6> in <module>()
      1 try:
----> 2     1/0
      3 except Exception as e:

ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-18-268df49e81a6> in <module>()
      3 except Exception as e:
      4     print("예외 발생:", e)
----> 5     1 + "ㅁ"
      6     10 / 0
      7     raise

TypeError: unsupported operand type(s) for +: 'int' and 'str'

except 절 안으로 try 절에 발생한 예외가 처리를 위해서 들어온다. 그리고 바로 밑에서 1 + "ㅁ"에서 오류가 발생하면서 그 이후로 실행을 멈추고 예외를 처리하려고 시도한다. raise가 가장 가까운 예외인 1 + "ㅁ" 오류를 발생시킨다.

사용자 정의 예외

사용자가 예외를 정의해서 자신만의 예외를 사용할 수 있다. 사용자 정의 예외 클래스를 만들기 위해서는 Exception 클래스를 상속받아 만든다.

In [19]:
class 내예외(Exception):
    def __init__(self, 매개):
        self.매개 = 매개

    def 출력(self):
        print("내예외 출력 함수():", self.매개)

try:
    raise 내예외("내 예외를 발생시킵니다.")
except 내예외 as e:
    print("에러:", e)
    e.출력()
    print(e.args)
except ZeroDivisionError as e:
    print("에러의 매개:", e.args[0])
except:
    print("어떤 예외인지 모르는 예외 발생.")
에러: 내 예외를 발생시킵니다.
내예외 출력 함수(): 내 예외를 발생시킵니다.
('내 예외를 발생시킵니다.',)

직접하기

  • 내예외 클래스 __init__ 메소드에 매개변수를 추가하여 실행시켜보자.
  • 내예외 클래스를 Exception 클래스로부터 상속받지 않으면 어떻게 되는지 살펴보자.