PEP 317 – 암시적 예외 인스턴스화 제거
- Author:
- Steven Taschuk <staschuk at telusplanet.net>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 06-May-2003
- Python-Version:
- 2.4
- Post-History:
- 09-Jun-2003
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
“새 코드에서는 명확성을 위해raise class(argument, ...)형식을 권장합니다(즉, 생성자를 명시적으로 호출하십시오).”—Guido van Rossum, 1997년 [1]
이 PEP는 예외를 암묵적으로 인스턴스화하는 raise 문 형식을 공식적으로 폐지하고 최종적으로 제거할 것을 제안합니다. 예를 들어, 다음과 같은 문은
raise HullBreachError
raise KitchenError, 'all out of baked beans'
이 제안에 따라 그와 동등한 다음 문으로 바꿔야 합니다
raise HullBreachError()
raise KitchenError('all out of baked beans')
뒤의 문들은 이미 올바른 문법이며, 이 PEP가 그 의미를 바꾸지 않는다는 점에 유의하십시오.
이러한 raise 형식을 제거하면 문자열 예외를 사용할 수 없게 됩니다. 따라서 이 PEP는 문자열 예외도 공식적으로 폐지하고 최종적으로 제거할 것을 제안합니다.
이 제안을 채택하면 하위 호환성이 깨집니다. 제안된 구현 일정에 따르면, Python 2.4에서는 앞으로 올바르지 않게 될 raise 사용에 대해 경고를 도입하고, Python 3.0에서는 이를 완전히 제거합니다. (PEP 5의 지침을 준수하기 위해 이 전환 기간, 즉 2.4부터 3.0까지는 최소 1년이 될 것으로 가정합니다.)
동기
문자열 예외
문자열 예외를 제거하는 것에는 논란이 없을 것으로 가정합니다. 적어도 표준 예외 타입이 클래스로 바뀐 Python 1.5 때부터 의도되었던 일이기 때문입니다 [1].
참고로 밝혀 두자면, 두 종류의 예외가 존재하면 아무런 이점 없이 언어만 복잡해지므로 문자열 예외는 제거되어야 합니다. 인스턴스 예외가 더 나은 이유는 예를 들어 다음과 같습니다.
- 클래스-인스턴스 관계가 예외 타입과 값 사이의 관계를 더 자연스럽게 표현합니다.
- 슈퍼클래스-서브클래스 관계를 사용하여 자연스럽게 구성할 수 있습니다.
- (예를 들어) 오류 보고 동작을 캡슐화할 수 있습니다.
암시적 인스턴스화
표준 예외를 클래스로 바꾸는 것에 관한 Guido의 1997년 에세이 [1]는 raise가 암묵적으로 인스턴스화할 수 있는 이유를 분명히 보여 줍니다.
“raise 문은 명시적인 인스턴스화 없이 클래스 예외를 발생시킬 수 있도록 확장되었습니다. raise 문의 “호환성 형식”이라고 불리는 다음 형식들은 […] 호환성 형식을 도입한 동기는 표준 예외를 발생시키던 기존 코드와의 하위 호환성을 허용하기 위한 것이었습니다.”
예를 들어, 다음과 같이 문자열 예외 문법을 사용하던 1.5 이전의 코드가
raise TypeError, 'not an int'
TypeError가 문자열인 Python 버전과 클래스인 버전 모두에서 동작하기를 원했습니다.
이러한 고려 사항이 해당하지 않는 경우, 즉 코드가 지원해야 하는 소프트웨어의 어떤 버전에서도 원하는 예외 타입이 문자열이 아닌 경우에는 암시적으로 인스턴스화할 합당한 이유가 없으며, 그렇게 하지 않는 편이 더 명확합니다. 예를 들면 다음과 같습니다.
- 다음 코드에서
try: raise MyError, raised except MyError, caught: pass
raise문과except문 사이의 구문적 유사성은raised와caught가 같은 객체를 가리킨다는 인상을 강하게 줍니다. 문자열 예외에서는 실제로 그렇지만, 인스턴스 예외에서는 그렇지 않습니다. - 인스턴스화가 암시적이면 그것이 언제 일어나는지, 예를 들어 예외가 발생할 때 일어나는지 아니면 잡힐 때 일어나는지가 분명하지 않습니다. 실제로는
raise에서 일어나므로 코드에 그렇게 드러나야 합니다.(C API 수준에서는 예외가 인스턴스화되지 않은 채로 “발생”하고 “포착”될 수 있다는 점에 유의하십시오. 이는 예를 들어
PyIter_Next에서 최적화로 사용됩니다. 그러나 Python에서는 그러한 최적화를 사용할 수 없으며, 사용할 수 있어서도 안 됩니다.) - 다음과 같이 인자 없이 암시적으로 인스턴스화하는
raise문은raise MyError
말하는 대로 동작하지 않습니다. 즉, 이름이 지정된 객체를 발생시키지 않습니다.
- 다음의 동등성은
raise MyError raise MyError()
클래스와 인스턴스를 뒤섞어 초보자에게 혼란의 원인이 될 수 있습니다. (게다가 인터프리터가 새 스타일 클래스와 그러한 클래스의 인스턴스를 구별할 수 있을지도 분명하지 않으므로, 암시적 인스턴스화는 예외를 새 스타일 객체로 만들려는 향후 계획에 걸림돌이 될 수 있습니다.)
요컨대 암시적 인스턴스화는 하위 호환성 외에는 아무런 이점이 없으므로, 그것이 호환성을 보장하려고 존재하는 대상, 즉 문자열 예외와 함께 단계적으로 폐지되어야 합니다.
명세
raise_stmt [3]의 문법을 다음에서
raise_stmt ::= "raise" [expression ["," expression ["," expression]]]
다음으로 변경합니다.
raise_stmt ::= "raise" [expression ["," expression]]
표현식이 없으면 raise 문은 현재와 같이 동작합니다. 즉, 현재 스코프에서 마지막으로 활성화되었던 예외를 다시 발생시키며, 현재 스코프에서 활성화된 예외가 없었다면 이것이 문제임을 알리는 TypeError가 발생합니다.
그렇지 않으면 첫 번째 표현식이 평가되어 발생 객체가 만들어집니다. 그런 다음 두 번째 표현식이 있으면 평가되어 대체 트레이스백이 만들어집니다. 두 번째 표현식이 없으면 대체 트레이스백은 None입니다.
발생 객체는 인스턴스여야 합니다. 인스턴스의 클래스가 예외 타입이 되고, 인스턴스 자체가 예외 값이 됩니다. 발생 객체가 인스턴스가 아니면(예를 들어 클래스나 문자열이면) TypeError가 발생합니다.
대체 트레이스백이 None이 아니면 트레이스백 객체여야 하며, 예외가 발생한 위치로 현재 위치 대신 사용됩니다. 트레이스백 객체도 None도 아니면 TypeError가 발생합니다.
하위 호환성
마이그레이션 계획
퓨처 문
다음 PEP 236 퓨처 문 아래에서는:
from __future__ import raise_with_two_args
raise 문의 문법과 의미가 위에서 설명한 대로 됩니다. 이 future 기능은 Python 2.4에 도입될 예정이며, 그 효과는 Python 3.0에서 표준이 될 것입니다.
아래 예제에서 볼 수 있듯이, 이 퓨처 문은 raise에 대체 트레이스백 인자를 사용하는 코드에만 필요하며, 단순한 예외 발생에는 필요하지 않습니다.
경고
제안된 변경 사항에 따라 올바르지 않게 될 raise 사용을 지적하기 위해, 모두 DeprecationWarning 범주에 속하는 세 가지 새로운 경고가 발행될 예정입니다.
첫 번째 경고는 첫 번째 표현식이 문자열로 평가되는 raise 문이 실행될 때 발행됩니다. 이 경고의 메시지는 다음과 같습니다.:
raising strings will be impossible in the future
두 번째 경고는 첫 번째 표현식이 클래스로 평가되는 raise 문이 실행될 때 발행됩니다. 이 경고의 메시지는 다음과 같습니다.:
raising classes will be impossible in the future
세 번째 경고는 표현식 세 개를 가진 raise 문이 컴파일될 때 발생합니다. (실행될 때가 아니라는 점에 유의하십시오. 이 경고가 예고하는 SyntaxError는 컴파일 시점에 발생하기 때문에 이 점이 중요합니다.) 이 경고의 메시지는 다음과 같습니다:
raising with three arguments will be impossible in the future
이 경고들은 Python 2.4에서 나타나며, 이 경고들을 일으키는 조건이 그냥 오류가 되는 Python 3.0에서 사라질 예정입니다.
예시
암묵적 인스턴스화를 사용하는 코드
다음과 같은 코드는
class MyError(Exception):
pass
raise MyError, 'spam'
raise 문이 실행될 때 경고를 발생시킵니다. raise 문은 다음과 같이 명시적으로 인스턴스화하도록 변경해야 합니다:
raise MyError('spam')
문자열 예외를 사용하는 코드
다음과 같은 코드는
MyError = 'spam'
raise MyError, 'eggs'
raise 문이 실행될 때 경고를 발생시킵니다. 예외 타입은 다음과 같이 클래스로 변경해야 하며:
class MyError(Exception):
pass
앞의 예제에서와 마찬가지로 raise 문은 다음과 같이 명시적으로 인스턴스화하도록 변경해야 합니다
raise MyError('eggs')
트레이스백 객체를 제공하는 코드
다음과 같은 코드는
raise MyError, 'spam', mytraceback
컴파일될 때 경고를 발생시킵니다. 이 문은 다음과 같이 변경해야 하며
raise MyError('spam'), mytraceback
다음 future 문을
from __future__ import raise_with_two_args
모듈의 맨 위에 추가해야 합니다. 이 future 문을 추가하면 나머지 두 경고도 오류로 바뀌므로, 앞의 예제들에서 설명한 변경 사항도 반드시 적용해야 한다는 점에 유의하십시오.
다음 특수한 경우는
raise sys.exc_type, sys.exc_info, sys.exc_traceback
(이전 예외를 다시 발생시키기 위한 것으로) 간단히 다음과 같이 변경해야 합니다
raise
계획의 실패
문자열을 발생시키거나 암묵적으로 인스턴스화하는 raise 문이 이 PEP의 단계적 도입 기간 동안 운영 환경이나 테스트에서 한 번도 실행되지 않을 수도 있습니다. 그런 경우에는 아무런 경고도 발생하지 않다가, 대신 Python 3.0이나 그 이후 버전에서 어느 날 갑자기 실패하게 됩니다. (여기서 실패란 의도한 예외 대신 잘못된 예외, 즉 raise의 인자에 대해 불평하는 TypeError가 발생하는 것을 말합니다.)
단계적 도입 기간을 연장하면 이러한 경우를 더 드물게 만들 수는 있지만, 모든 raise 문에 대해 컴파일 시점에 경고를 발생시키지 않는 한 이러한 경우를 완전히 없앨 수는 없습니다.
거부
이 PEP가 채택된다면 기존 Python 코드 거의 전부를 검토하고 아마도 수정해야 할 것입니다. 명시적 인스턴스화를 지지하는 위의 모든 인자를 받아들인다고 하더라도, 명확성이 개선되는 정도가 너무 미미하여 수정에 드는 비용과 그로 인해 새로운 버그가 생길 위험을 정당화할 수 없습니다.
따라서 이 제안은 거부되었습니다 [6].
문자열 예외는 이 제안과는 별개로 제거될 예정이라는 점에 유의하십시오. 거부된 것은 암묵적 예외 인스턴스화의 제거입니다.
논의 요약
응답자 중 소수는 이 제안에 찬성했지만, 지배적인 반응은 이러한 이전 작업이 어떤 것이든 기대되는 이점에 비해 지나치게 큰 비용이 든다는 것이었습니다. 위에서 언급했듯이, 이 점만으로도 이 PEP를 거부하기에 충분합니다.
새 스타일 예외
암시적 인스턴스화는 새 스타일 클래스의 인스턴스를 예외로 사용할 수 있게 하려는 향후 계획과 충돌할 수 있습니다. 암시적으로 인스턴스화할지를 결정하려면 raise 메커니즘이 첫 번째 인자가 클래스인지 인스턴스인지 판별해야 합니다. 그러나 새 스타일 클래스에서는 둘 사이에 명확하고 뚜렷한 구분이 없습니다.
이 제안에 따르면 예외가 이미 인스턴스화되어 있으므로 이 문제를 피할 수 있습니다. 그러나 그럴듯한 대안이 두 가지 있습니다:
- 예외 타입이
Exception의 서브클래스여야 한다고 요구하고, 다음 경우에만 암시적으로 인스턴스화합니다issubclass(firstarg, Exception)
- 다음 경우에만 암시적으로 인스턴스화합니다
isinstance(firstarg, type)
따라서 이 문제를 해결하기 위해 암시적 인스턴스화를 완전히 없앨 필요는 없습니다.
명시적 인스턴스화의 보기 흉함
일부 응답자는 명시적으로 인스턴스화하는 문법이 더 보기 흉하다고 느꼈으며, 특히 예외 생성자에 아무 인자도 전달하지 않는 경우에 그렇다고 느꼈습니다:
raise TypeError()
이 문제는 예외 인스턴스 자체에는 관심이 없는 경우, 즉 예외 타입만이 중요한 경우에 특히 심각합니다:
try:
# ... deeply nested search loop ...
raise Found
except Found:
# ...
이러한 경우에는 raise와 except 사이의 대칭성이 코드의 의도를 더 잘 표현할 수 있습니다.
Guido는 암시적으로 인스턴스화하는 문법이 구두점이 더 적으므로 인자가 하나인 경우에도 “조금 더 보기 좋다”는 의견을 밝혔습니다.
경고로 인한 성능 저하
apply()를 폐지 예정으로 지정했던 경험을 보면, 경고 프레임워크를 사용하면 상당한 성능 저하가 발생할 수 있습니다.
명시적으로 인스턴스화하는 코드는 영향을 받지 않습니다. 경고를 발생시킬지 결정하는 데 필요한 런타임 검사는 애초에 암시적으로 인스턴스화할지를 결정하는 데 필요한 검사와 정확히 같기 때문입니다. 즉, 그러한 문장은 이미 이 검사 비용을 치르고 있습니다.
암시적으로 인스턴스화하는 코드는 큰 비용을 치르게 됩니다. 시간 측정 실험에 따르면 경고를 발생시키는 데에는 (경고가 억제되든 그렇지 않든) 단순히 예외를 인스턴스화하고, 발생시키고, 잡는 것보다 약 다섯 배의 시간이 걸립니다.
raise 문이 성능이 중요한 실행 경로에 놓이는 경우는 드물기 때문에 이 성능 저하는 완화됩니다.
트레이스백 인자
현재 제안대로라면 모든 2.x 버전의 Python에서 raise에 traceback 인자를 편리하게 사용할 수 없게 됩니다.
2.4 미만 버전과 호환되려면 세 인자 형식을 사용해야 합니다. 그러나 이 형식은 2.4 이상 버전에서 경고를 발생시킵니다. 그러한 경고를 억제할 수는 있지만, 해당 종류의 경고가 컴파일 시점에 발생하므로 억제하기가 번거롭습니다.
이 PEP가 아직 검토 중이었다면, 단계적 도입 기간을 연장하여 이 반론에 대응했을 것입니다. 예를 들어, 3.0에서 처음 경고를 발생시키고 이후의 어떤 릴리스에서 오류로 바꿀 수 있었을 것입니다.
참고 문헌
Copyright
This document has been placed in the public domain.