이 글의 작성자는 C/C++ 프로그래밍을 하던 사람이다. 이 글은 Python을 복습하며 작성하는 글이니, 부족한 부분이 있으면 얼마든지 피드백을 주시기 바란다.

 

잘못된 동작을 하려고 한다

그 동안 우리는 잘못된 동작을 하면 다음과 같이 출력되는 것을 보아왔다.

  • 잘못된 문법으로 코드를 작성했을 때

  • 범위를 벗어난 리스트/튜플에 접근하려 했을 때
  • 객체가 가지고 있지 않은 속성/메서드를 호출하려 할 때
  • 연산을 하다가 0으로 나누려고 할 때
  • 존재하지 않는 변수를 사용하려고 할 때

잘못된 문법으로 코드를 작성했다면 그냥 그걸로 끝이다. 파이썬은 문법적인 오류에 굉장히 민감하다. 그래서 문법 오류를 만나면 그 자리에서 다음과 같은 텍스트를 출력하며 바로 실행을 멈춰버린다.

SyntaxError: invalid syntax

하지만 문법이 맞다고 해서 항상 파이썬 코드가 정상적으로 동작하리란 법은 없다. 당장 위에만 봐도 비정상적으로 동작할 수 있는 경우가 4가지가 나와있다. (물론 위의 내용도 극히 일부이다) 그리고 파이썬은 이에 대한 문제점을 이런 식으로 남긴다.

IndexError: list index out of range
AttributeError: type object 'ee' has no attribute 'rr'
ZeroDivisionError: division by zero
NameError: name 'q' is not defined

 

하지만 위의 오류들은 잘못된 문법으로 인해 발생한 오류와는 달리, 프로그래머가 대응이 가능한 오류이다. 대응이 가능하다는 말이 무슨 말이냐? 이러이러한 오류가 발생했을 때, 이렇게 해라! 라는 지침을 프로그래머가 내려줄 수 있다는 것을 의미한다. 이러한 지침을 어떻게 내리느냐? try-except구문을 이용하여 처리할 수 있다.

 

try-except 사용하기

그렇다면 간단한 예제를 하나 보도록 하자. 일부러 예외가 발생할 만한 코드를 하나 작성해보자.

a = 3/0

이러면 0으로 나누는 오류가 발생할 것이다. 이 코드를 try-except구문에 다음과 같이 집어넣으면 된다.

try:
    a = 3/0
except:
    print('divide by zero!')

위의 코드를 실행하면 divide by zero!라는 텍스트가 뜨면서 평범하게 종료가 될 것이다. 만약 이 코드가 try-except구문에 없었다면, 빨간 글씨로 ZeroDivisionError :…라는 텍스트를 띄운 뒤, 비정상적으로 종료가 되었을 것이다.

이런 식으로 뭔가 잘못된 동작이 발생할 여지가 있을 때, try-except 구문에서 코드를 수행하면 예외가 발생했을 때 프로그래머가 원하는대로 처리를 할 수 있게 된다.

이를 일반화하여 표현하자면 다음과 같이 표현할 수 있다.

try:
    예외가 발생할 가능성이 있는 코드()
except:
    예외에 대응하는 코드()

 

특정 예외에 대해서만 처리하기

하지만 발생할 수 있는 예외는 한 가지가 넘을 수도 있다. 리스트나 튜플의 범위를 넘을 수도 있고, 정상적으로 접근했다고 하더라도 해당 객체에 하는 동작이 올바르지 않을 수도 있다. 하지만 지금의 코드로는 어떠한 예외가 발생하든지 단 하나의 코드로만 예외를 처리할 수 있다.

그렇다면 하나의 try-except 구문에서 여러 종류의 예외를 처리할 수 있는 방법은 무엇일까?

except 뒤에 처리하고자 하는 예외의 이름을 붙이면 된다. 다음 코드를 보자.

def divide(item):
    return 100 / item

item을 인자로 받아서 정수 100과 나눈 결과값을 리턴하는 함수이다. 이 함수에서 발생 가능한 예외는 무엇이 있을까?

 

  • item 객체가 0이 들어와서 나누기 오류가 발생할 수 있다.

item에 들어가는 객체가 정수형 객체 0이거나, 실수형 객체 0.0이면 ZeroDivisionError 예외가 발생할 수 있다.

  • item 객체가 정수형 객체 100과의 나눗셈을 지원하지 않는다.

item에 들어가는 객체가 문자열 객체인 'hello'가 들어간다면, 100 / 'hello'는 성립하지 않는 연산이다. 정수와 문자열 사이의 나눗셈은 아무런 의미가 없기 때문이다. 이 때, TypeError가 발생하게 된다.

 

그러면 각각의 예외 상황에 대응하는 코드를 작성해보자.

def divide(item):
    try:
        return 100 / item
    except TypeError:
        print("can't divide")
    except ZeroDivisionError:
        print("divide by zero")

그러면 다음 코드를 실행하면 오류가 발생하지 않고 끝날 것이다.

divide('hi')
divide(0.0)
can't divide
divide by zero

 

하지만 때로는 우리의 상상을 초월하는 예외 상황이 위의 함수에서 발생할 수도 있다. 당장은 어떤 예외가 발생할지 알 수는 없지만, 만약을 대비해서 확인하지 않은 예외들에 대해서 확인을 하고 싶다면, 맨 마지막에 except:를 추가하면 된다.

def divide(item):
    try:
        return 100 / item
    except TypeError:
        print("can't divide")
    except ZeroDivisionError:
        print("divide by zero")
    except:
        print('unknown exception!')

이 때, 모든 예외에 대응하는 except:는 반드시 맨 마지막에 와야 한다. 그렇지 않으면 문법 오류이다.

 

예외에 대한 세부 정보 얻기

TypeError가 발생한다고 해서, 항상 똑같은 상황에서 발생하는 것은 아니다.

  • 정수형 객체에 인덱스 연산을 수행하거나
  • 나눗셈을 지원하지 않는 객체와 나눗셈을 수행하거나

크게 보면 연산이나 함수가 부적절한 타입의 객체에 적용될 때 발생하는 예외이긴 하지만, 다음과 같은 코드를 보면 구분을 해야 할 필요성이 보이긴 한다.

def halfItem(lst, idx):
    return lst[idx] / 2

lst 객체가 인덱스 연산을 지원하지 않을 수도 있고, lst[idx]로 가져온 객체가 정수형 객체 2와 나눗셈을 지원하지 않을 수도 있다. 하지만 둘 다 TypeError로 묶이게 된다. 따라서 좀 더 정확한 정보를 알아야 할 필요가 있다.

이럴 때는 발생한 예외에 대한 세부 정보를 담은 객체를 받아올 수 있다. 다음과 같이 받아올 수 있다.

except TypeError as err:
    print(err)

위와 같이 코드를 작성하면, TypeError가 발생할 시 해당 예외에 대한 상세한 정보가 담긴 객체를 err이라는 이름으로 얻을 수 있다. 그러면 실제로 예외를 발생시켜보자.

def halfItem(lst, idx):
    try:
        return lst[idx] / 2
    except TypeError as err:
        print(err)

halfItem(1,0)
halfItem(['1','2','3'],0)

위의 코드를 실행하면 다음과 같은 결과를 얻을 수 있을 것이다.

'int' object is not subscriptable
unsupported operand type(s) for /: 'str' and 'int'

 

직접 예외를 발생시키기

때로는 프로그래머가 주어진 값들을 보고 스스로 예외를 발생시켜야 하는 경우도 있다. 예외를 발생시키려면 어떻게 해야 할까?

raise 키워드를 사용하면 예외를 발생시킬 수 있다. 다음 코드를 보자. 짝수가 아니면 예외를 발생시키는 코드이다.

def halfEvenItem(item, idx):
    if item % 2:
        raise Exception
    return item / 2

item이 나눌 수 있는 객체인가 아닌가는 여기서 신경쓰지 않기로 하자.

만약 item이 2로 나누어 떨어지지 않는 수가 들어간다면, 함수는 Exception이라는 예외를 발생시킬 것이다.

halfEvenItem(3,2)
Exception

하지만 아무런 설명도 없이 예외 사항이 발생했다! 라고만 이야기하면, 이 함수를 사용하는 사람은 왜? 라는 말을 하게 될 것이다. 따라서 예외를 발생시킬 때는 이 예외가 왜 발생했는가에 대한 정보를 담아야 한다.

다음과 같이 적절한 타입의 예외 클래스를 사용하고, 거기에 사유를 기입해서 예외를 발생시키면 호출한 쪽도 납득할 수 있을 것이다.

def halfEvenItem(item, idx):
    if item % 2:
        raise ValueError('item is not even')
    return item / 2

 

except 안에서도 여전히 예외를 발생시킬 수 있다. 좀 전에 divide 함수에 대해 예외처리를 하던 코드를 다시 보도록 하자.

def divide(item):
    try:
        return 100 / item
    except TypeError:
        print("can't divide")
    except ZeroDivisionError:
        print("divide by zero")
    except:
        print('unknown exception!')

위와 같이 함수를 만든 사람이 알 수 없는 예외를 확인했을 때, 이대로 넘어가면 함수를 호출한 쪽에서 아무런 예외가 발생하지 않았구나! 하면서 넘어갈 수 있다. 이 코드 대로라면 divide 함수를 호출한 쪽에서 아무리 try-except를 걸어도 예외가 오지 않기 때문이다. 예외를 넘기는 것은 우리가 어떤 오류인지 알고, 적절하개 대처할 수 있을때만 그냥 넘겨야 한다.

예외 처리를 하는 쪽에서 오류에 대해 대응하지 못하겠다 싶을 때는 그대로 다시 raise를 해서 예외를 발생시키면 된다.

def divide(item):
...
    except:
        print('unknown exception!')
        raise

특별히 raise 뒤에 어떤 예외인지 붙일 필요는 없다. 이미 어떤 예외가 발생했는지 알고 있기 때문이다.

 

나만의 예외 만들기

파이썬은 일반적으로 쓰일 법한 예외들을 제공해준다. 하지만 때로는 파이썬에서 주어지는 예외를 쓰기 보단 나만의 예외를 사용해야 할 경우가 있기 마련이다.

그럴 때는, Exception 클래스를 상속하여 새로운 예외 클래스를 생성하면 된다. 위의 halfEvenItem 함수에서 우리 만의 예외를 만들어서 발생시켜보자. 이 함수에서는 짝수가 아니면 예외를 발생시키므로, NotEventError라고 이름을 지어보자.

class NotEvenError(Exception):
    pass

def halfEvenItem(item, idx):
    if item % 2:
        raise NotEvenError('item is not even')
    return item / 2


halfEvenItem(3,2)

위의 코드를 실행하면 다음 결과를 얻을 수 있을 것이다.

__main__.NotEvenError: item is not even

 

Categories:

Updated:

Comments