Programming/Python

[Python] TypeError: exceptions must derive from BaseException

Jonghyuk Baek 2021. 10. 13. 23:11

Introduction

파이썬 개발 중 아주 가끔씩, 다음과 같은 TypeError 메시지를 만나볼 수 있다.

TypeError: exceptions must derive from BaseException

뭐가 문제였을까?


Solution

정답은 바로 이전 포스트에서도 언급했듯이, 파이썬3 에서는 BaseException 내장 클래스를 상속받는 클래스만이 예외 클래스로 인식되기 때문이다.

 

따라서 만약 예외 클래스를 인자로 받아야 하는 자리에 예외 클래스로 인식되지 않는 (BaseExeption을 상속받지 않는) 클래스를 적어놓는 경우 위와 같은 에러 메시지를 보게 된다.

 

class FooClass: pass

try:
    raise FooClass()
except FooClass:
    print("Catch!")
    
>>
Traceback (most recent call last):
  File "Main.py", line 7, in <module>
    raise FooClass()
TypeError: exceptions must derive from BaseException

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "Main.py", line 8, in <module>
    except FooClass:
TypeError: catching classes that do not inherit from BaseException is not allowed

실제로 BaseException 클래스를 상속받지 않는 클래스를 raise하려 시도하는 경우 위와 같이 굉장히 친절한 에러 메시지가 출력된다. 여기서는 잘못된 클래스를 예외 클래스로 사용하려 하는 예시를 들었지만, 사실 이 에러 메시지를 마주하게 되는 대부분의 경우는 raise 이후에 예외 클래스 지정 없이, 에러 메시지로 띄울 스트링만을 적어놓는 케이스다.

 

raise "Error: Undefined argument"

>>
Traceback (most recent call last):
  File "Main.py", line 4, in <module>
    raise "Error: Undefined argument"
TypeError: exceptions must derive from BaseException

검색을 통해 이 글을 보고있는 당신은 위와 같이 작성했을 가능성이 높다...

이 경우에도, BaseException을 상속받지 않는 스트링 객체가 예외 클래스 자리에 들어가 있기 때문에 같은 에러 메시지가 뜬다.

 

 

.

.

.

BaseException Class

하지만 여기까지라면 너무 영양가가 없는 글인것 같아 이 BaseException 클래스의 기원에 대해 조금 더 설명해보려 한다. BaseException은 파이썬 2.5 버전에서 새롭게 추가된 내장 클래스로, 일반적인 클래스와 예외 클래스를 구분하기 위한 목적으로 만들어졌다. 

 

예외 클래스와 예외로 사용하지 않는 클래스를 구분하기 위해 추가된 것이라면, 그 전에는 클래스가 예외의 용도로 쓰인다는 것을 어떻게 구분했을까? 놀랍게도 파이썬 2.5 이전 버전까지는 아무 클래스나 예외 클래스처럼 raise 하는 것이 가능했다! 한번 예시를 보자.

 

# ~ Python 2.4

class AnyClass: pass

try:
    raise AnyClass()
except AnyClass:
    print("Catch!")

>>
Catch!

위와 같은 식으로 어떤 클래스나 raise 할 수 있다. 개발자들은 이런 식으로 아무 클래스나 예외로써 raise 할 수 있게 되면 예외를 위한 특정한 인터페이스를 제공하는 것이 근본적으로 불가능하기 때문에 이는 고쳐져야 할 문제라 여겼고, 문제를 해결하기 위해 모든 예외 클래스의 기본이 되는 BaseExeption 클래스를 새로이 추가한 것이다.

 

다만 비슷한 케이스인 클래스가 object를 상속하도록 변경된 경우 처럼 이 때에도 버전 업데이트 이전에 작성된 코드들이 안정적으로 실행될 수 있어야 했고, 이렇게 특정한 인터페이스를 위해서 특정한 베이스 클래스를 강제하는 것이 파이썬답지 않다(unPythonic) 는 의견도 있었기 때문에, 이 변경사항은 당시 뉴페이스로 밀고 있던 new-style class에만 적용되었다.

(new-style class는 파이썬 3의 기본형 클래스와 동일하며, 파이썬 2에서는 클래스에 object 클래스를 상속시킴으로써 구현할 수 있다. 기존의 파이썬 2 클래스는 old-style class라고 불렀으며 파이썬 3에서는 완전히 사라졌다. 참고)

 

이 때 커뮤니티에서 BaseException의 도입이 받아들여지게 된 결정적인 포인트는,

상속 계층을 이용해 except *: 대신 except BaseException: 과 같은 형태로 모든 예외를 확실하게 잡아낼 수 있다는 점이었다.

 

이 때 도입된 BaseException의 구조는 아래와 같다.

(파이썬 3.0 기준이며, 파이썬 2에서는 기존 코드와의 호환을 위해 더욱 복잡한 인터페이스를 가진다)

 

class BaseException(object):

    """Superclass representing the base of the exception hierarchy.

    Provides an 'args' attribute that contains all arguments passed
    to the constructor.  Suggested practice, though, is that only a
    single string argument be passed to the constructor.

    """

    def __init__(self, *args):
        self.args = args

    def __str__(self):
        if len(self.args) == 1:
            return str(self.args[0])
        else:
            return str(self.args)

    def __repr__(self):
        return "%s(*%s)" % (self.__class__.__name__, repr(self.args))

이를 통해 BaseException을 상속받는 클래스는 최소한의 공통적인 예외 인터페이스를 가질 수 있게 되었다.

 

# Python 2
raise "Error: Undefined argument"

>>
Traceback (most recent call last):
  File "Main.py", line 5, in <module>
    raise "Error: Undefined argument"
TypeError: exceptions must be old-style classes or derived from BaseException, not str


# Python 3
raise "Error: Undefined argument"

>>
Traceback (most recent call last):
  File "Main.py", line 4, in <module>
    raise "Error: Undefined argument"
TypeError: exceptions must derive from BaseException

이 과정에서 old-style class는 기존 방식대로 아무런 클래스나 예외로 사용할 수 있도록 남겨졌기 때문에, 파이썬 2 기준의 에러 메시지는 지금과는 살짝 다르다.

 

그리고 이렇게 기본이 되는 예외 클래스를 만들어 놓았지만, KeyboardInterruptSystemExit 같은 예외는 보통 except 문에서 예외처리를 하지 않아야 하기 때문에.. (이들까지 예외처리가 되어 버리면 사용자나 프로그램이 날리는 시스템의 정상적인 종료 시그널까지 무시해 버리는 결과를 낳는다!) 아래와 같이 불편한 모양새로 이중 except문을 작성하는 경우가 허다했다. 그래서 이 둘만 쏙 빼놓은 Exception 하위 클래스를 추가로 만들어 다른 모든 내장 예외가 이를 상속받도록 했다. 참고

 

# 기존 방식
except (KeyboardInterrupt, SystemExit):
    raise
except:
    ...

# Exception 클래스의 추가
except Exception:
	...

결과적으로는 두번째 단락과 같이 훨씬 깔끔하게 프로그램의 에러 시그널만을 캐치해낼 수 있게 되었다.

 


여기까지 에러 메시지 해석에 이어서 BaseException이 무엇인지, 어떤 원인으로 도입되었는지를 한번 알아봤다. 사실 이런 내용들은 파이썬 개발에 실질적인 큰 도움을 주지는 않을 것이다. 하지만 지금 우리가 아는 파이썬의 모습이 어떻게 해서 만들어져 왔고, 개발자들이 어떤 것이 더 Python에 어울리는 좋은 코드인지 고민했던 과정을 따라가다 보면 더욱 'Pythonic' 한 코드를 작성하는 데에 도움이 되지 않을까 생각한다.

 

아래는 참고할 만한 자료들이다.

References

Python 2.5 Release

https://www.python.org/download/releases/2.5/

PEP 352 -- Required Superclass for Exceptions

https://www.python.org/dev/peps/pep-0352/

[Python] PEP 8: E722 do not use bare 'except'

https://jh-bk.tistory.com/4

[Python] 파이썬 모듈의 예외 계층 구조 응용

https://jh-bk.tistory.com/12

[Python] 클래스가 object를 상속받는 경우 (new-style, old-style class)

https://jh-bk.tistory.com/24

반응형