Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 463 – 예외를 포착하는 표현식

Author:
Chris Angelico <rosuav at gmail.com>
Status:
Rejected
Type:
Standards Track
Created:
15-Feb-2014
Python-Version:
3.5
Post-History:
20-Feb-2014, 16-Feb-2014
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

거부 공지

https://mail.python.org/pipermail/python-dev/2014-March/133118.html 에서 발췌:

“”” 저는 이 PEP를 거부하고자 합니다. 원하는 의미를 고려하면 제안된 문법은 받아들일 만하다고 생각합니다. 다만 여전히 다소 어색합니다. 아마도 lambda에 쓰이는 콜론보다 나쁘지는 않을 것이며(여기의 콜론이 try/except의 콜론을 떠올리게 하듯이, lambda의 콜론은 def에 쓰이는 콜론을 떠올리게 합니다), 나열된 대안들보다는 확실히 낫습니다.

하지만 제가 지지할 수 없는 것은 동기와 근거입니다. 예를 들어 except 표현식이 생긴다고 해서 dict.get()이 불필요해질 것이라고는 생각하지 않으며, EAFP가 LBYL보다 낫다거나 Python이 “일반적으로 권장”한다는 입장에도 동의하지 않습니다. (그런 이야기는 어디서 들으셨습니까? 코드 한 줄을 반복하느니 차라리 고차 함수를 도입하겠다고 할 만큼 DRY에 집착하는 바로 그 출처에서 들으신 것입니까? :-)

공식 결정과 관련해 제게서 들으실 수 있는 것은 아마 이 정도가 최대일 것입니다. 곧 언어 서밋이 열리니, (원하는 분이 있다면) 그 자리에서 거부 이유를 더 깊이 설명해 드리겠습니다.

(그 끔찍한 약어들을 한 번도 설명하지 않았다는 점만 빼면 :-) 이 PEP는 잘 작성되었고 조사도 충실했다고 생각합니다. 또한 논의를 조율하고, 반대 의견을 수집하고, 대안을 검토하는 등 열띤 논쟁을 하나의 PEP로 만들어 내는 데 필요한 모든 일을 훌륭하게 해내셨다고 생각합니다. 수고하셨습니다, Chris(그리고 도와주신 모든 분). 다음 PEP에서도 행운이 있기를 바랍니다! “””

초록

PEP 308이 표현식 안에서 값에 기반한 조건을 사용하는 수단을 도입한 것처럼, 이 체계는 예외에 기반한 조건을 표현식의 일부로 사용할 수 있게 합니다.

동기

여러 함수와 메서드에는 예외를 발생시키는 대신 지정된 값을 반환하도록 하는 매개변수가 있습니다. 현재 체계는 임시방편적이고 일관성이 없으며, 이 기능을 갖추려면 각 함수를 개별적으로 작성해야 합니다. 모든 함수가 이를 지원하는 것도 아닙니다.

  • dict.get(key, default) - KeyError 대신 두 번째 위치 인자 사용
  • next(iter, default) - StopIteration 대신 두 번째 위치 인자 사용
  • list.pop() - 기본값을 반환할 방법이 없음
  • seq[index] - 범위 오류를 처리할 방법이 없음
  • min(sequence, default=default) - ValueError 대신 키워드 인자 사용
  • statistics.mean(data) - 빈 이터레이터를 처리할 방법이 없음

이 기능이 Python 역사 초기에 존재했다면 dict.get()과 관련 메서드들을 만들 필요가 없었을 것입니다. 없는 키를 처리하는 단 하나의 명백한 방법은 예외에 대응하는 것이었을 것입니다. 부재를 한 가지 방식으로 알리는 메서드 하나가 작성되고, 그 부재에 대응하는 데에는 일관된 기법 하나가 사용됩니다. 그 대신 우리에게는 dict.get()이 있고, Python 3.4부터는 min(… default=default)도 있으며, 그 밖에도 무수히 많은 것들이 있습니다. 표현식 안에서 검사하기 위한 LBYL 문법은 있지만, 현재 EAFP 표기법은 없습니다. 다음을 비교해 보십시오.:

# LBYL:
if key in dic:
    process(dic[key])
else:
    process(None)
# As an expression:
process(dic[key] if key in dic else None)

# EAFP:
try:
    process(dic[key])
except KeyError:
    process(None)
# As an expression:
process(dic[key] except KeyError: None)

Python은 일반적으로 EAFP 방침을 권장하지만, 이를 가능하게 하려면 dic.get(key,None) 같은 유틸리티 함수를 늘려 나가야 합니다.

근거

현재 체계에서는 함수 작성자가 기본값의 필요성을 예측하고 이를 지원하도록 구현해야 합니다. 그렇게 하지 않으면 완전한 try/except 블록이 필요합니다.

try/except는 문이기 때문에 표현식 도중에 예외를 잡는 것은 불가능합니다. if/else가 조건문에 대해, lambda가 함수 정의에 대해 그렇게 하듯이, 이 제안은 표현식 문맥에서 예외를 잡을 수 있게 합니다.

이는 함수가 기본값을 제공하는 깔끔하고 일관된 방법을 제공합니다. 함수는 그저 적절한 예외를 발생시키고, 호출자가 그 예외를 잡으면 됩니다.

어떤 상황에서는 LBYL 기법을 사용할 수 있습니다(예를 들어, 어떤 시퀀스를 인덱싱하기 전에 길이가 충분한지 확인하는 경우입니다). 이 방법이 모든 경우에 안전한 것은 아니지만, 대개 편리하기 때문에 프로그래머들은 LBYL의 간결한 표기를 위해 EAFP의 안전성을 희생하고 싶은 유혹을 받게 됩니다. 게다가 일부 LBYL 기법(예를 들어 인자 세 개를 받는 getattr을 사용하는 경우)은 코드를 속성 조회가 아니라 리터럴 문자열처럼 보이도록 왜곡하여 가독성에 영향을 줄 수 있습니다. 편리한 EAFP 표기법은 이 모든 문제를 해결합니다.

이를 수행하는 헬퍼 함수를 편리하게 작성할 방법은 없습니다. 가장 가까운 것은 lambda를 사용하는 보기 흉한 방식입니다.:

def except_(expression, exception_list, default):
    try:
        return expression()
    except exception_list:
        return default()
value = except_(lambda: 1/x, ZeroDivisionError, lambda: float("nan"))

이 방법은 투박하고 여러 예외 절을 처리할 수 없습니다. 아니면 다음과 같이 eval을 사용하는 방법도 있습니다.:

def except_(expression, exception_list, default):
    try:
        return eval(expression, globals_of_caller(), locals_of_caller())
    except exception_list as exc:
        l = locals_of_caller().copy()
        l['exc'] = exc
        return eval(default, globals_of_caller(), l)

def globals_of_caller():
    return sys._getframe(2).f_globals

def locals_of_caller():
    return sys._getframe(2).f_locals

value = except_("""1/x""",ZeroDivisionError,""" "Can't divide by zero" """)

이 방법은 훨씬 더 투박하며 구현에 의존하는 꼼수에 기댑니다. (CPython 이외의 인터프리터를 위한 globals_of_caller()와 locals_of_caller()를 작성하는 것은 독자의 연습 문제로 남겨 둡니다.)

Raymond Hettinger는 이러한 일관된 API를 원한다고 표명했습니다. 과거에도 이와 비슷한 것이 여러 차례 요청되었습니다.

제안

‘or’ 연산자와 세 부분으로 된 ‘if-else’ 표현식이 거짓 같은 값을 잡아 대체하는 단락 평가 방식을 제공하듯이, 이 문법은 예외를 잡아 대체하는 단락 평가 방식을 제공합니다.

현재는 다음이 동작합니다.:

lst = [1, 2, None, 3]
value = lst[2] or "No value"

이 제안은 다음을 추가합니다.:

lst = [1, 2]
value = (lst[2] except IndexError: "No value")

구체적으로, 제안하는 문법은 다음과 같습니다.:

(expr except exception_list: default)

여기서 expr, exception_list, default는 모두 표현식입니다. 먼저 expr이 평가됩니다. 예외가 발생하지 않으면 그 값이 전체 표현식의 값이 됩니다. 예외가 발생하면 exception_list가 평가되며, 문 형태의 try/except와 마찬가지로 그 결과는 타입이나 튜플이어야 합니다. 일치하는 예외가 있으면 대응하는 default 표현식이 평가되어 표현식의 값이 됩니다. 문 형태의 try/except와 마찬가지로, 일치하지 않는 예외는 위로 전파됩니다.

제너레이터 표현식이 따르는 것과 같은 규칙에 따라, 괄호가 완전히 불필요한 경우가 아니라면 전체 표현식을 괄호로 감싸야 합니다. 이는 중첩된 except 표현식이 올바르게 해석되도록 보장하며, 향후 문법을 확장할 여지를 남깁니다. 여러 except 절에 관해서는 아래를 참고하십시오.

현재 제안은 예외 객체를 캡처하는 것을 허용하지 않는다는 점에 유의하십시오. 이것이 필요한 경우에는 문 형태를 사용해야 합니다. (이에 대한 논의와 자세한 설명은 아래를 참고하십시오.)

이 삼항 연산자의 우선순위는 lambda와 if/else 사이가 됩니다.

2단계 캐시의 다음 예를 살펴보십시오.:

for key in sequence:
    x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
    # do something with x

이는 다음과 같이 다시 작성할 수 없습니다.:

x = lvl1.get(key, lvl2.get(key, f(key)))

이 방법은 더 짧기는 하지만 get()에 전달할 기본값을 먼저 계산해야 하므로 캐시를 두는 의미가 없어집니다. .get() 버전은 계산 순서가 거꾸로이고, 예외를 검사하는 버전은 예상대로 앞에서부터 계산합니다. 쓸 만한 것 중 가장 가까운 등가 형태는 다음과 같습니다.:

x = lvl1.get(key) or lvl2.get(key) or f(key)

이 방법은 값이 0이 아니어야 하고, 캐시 객체가 이 기능을 지원해야만 동작합니다.

대안 제안

python-ideas에서 논의하는 과정에서 다음과 같은 문법 제안이 나왔습니다.:

value = expr except default if Exception [as e]
value = expr except default for Exception [as e]
value = expr except default from Exception [as e]
value = expr except Exception [as e] return default
value = expr except (Exception [as e]: default)
value = expr except Exception [as e] try default
value = expr except Exception [as e] continue with default
value = default except Exception [as e] else expr
value = try expr except Exception [as e]: default
value = expr except default # Catches anything
value = expr except(Exception) default # Catches only the named type(s)
value = default if expr raise Exception
value = expr or else default if Exception
value = expr except Exception [as e] -> default
value = expr except Exception [as e] pass default

기존 키워드를 재사용하지 말고 새 키워드를 만들자는 제안도 있었습니다. 이러한 제안은 마지막 형태와 구조가 같으며, ‘pass’ 자리에 다른 키워드를 쓴다는 점만 다릅니다. 제안된 키워드로는 ‘then’, ‘when’, ‘use’가 있습니다. 또한 “default if expr raise Exception”(expr이 예외를 발생시키면 default) 제안과 관련해서는 새 키워드 “raises”를 사용하자는 의견도 있었습니다.

‘as’ 캡처 절을 포함하는 형태는 단순성을 위해 모두 이 제안에서 보류했지만, 제안 내용을 정확히 기록하기 위해 위의 표에는 남겨 두었습니다.

이 제안이 가장 지지하는 네 가지 형태는 순서대로 다음과 같습니다.:

value = (expr except Exception: default)
value = (expr except Exception -> default)
value = (expr except Exception pass default)
value = (expr except Exception then default)

네 가지 형태 모두 왼쪽에서 오른쪽으로 평가하는 순서를 유지합니다. 먼저 기본 표현식을 평가하고, 그다음에 예외 목록을, 마지막으로 기본값을 평가합니다. 표현식은 지연 평가되므로 이 점은 중요합니다. 이와 달리 위에 나열한 임시방편 대안 중 여러 가지는 (함수의 특성상) 기본값을 미리 평가해야 합니다. 콜론을 사용하는 선호 형태는 “except exception_list:”를 사용한다는 점에서 try/except와 대응하고, “키워드 name_list: 하위 표현식” 구조를 가진다는 점에서 lambda와 대응합니다. 또한 딕셔너리 방식으로 예외를 기본값에 매핑하는 것으로도 읽을 수 있습니다. 화살표를 사용하면 많은 프로그래머에게 익숙하지 않고 현재 비슷한 의미로 쓰이지도 않는 토큰이 새로 도입되지만, 그 점을 빼면 꽤 읽기 쉽습니다. 영어 단어 “pass”는 어렴풋하게 비슷한 의미를 가지고 있고(함수 인자에 대해 흔히 쓰는 “pass by value/reference”라는 표현을 생각해 보십시오), “pass”는 이미 키워드이기도 합니다. 하지만 키워드로서의 의미가 이것과 전혀 관련이 없으므로 혼란을 일으킬 수 있습니다. “then”을 사용하면 영어로는 자연스럽지만, 언어에 새 키워드가 추가됩니다. 흔히 쓰이는 단어는 아니라 해도 새 키워드라는 점은 마찬가지입니다.

왼쪽에서 오른쪽으로 평가하는 순서는 대부분의 표현식이 평가되는 순서와 같기 때문에 가독성에 매우 중요합니다. 다음과 같은 대안은:

value = (expr except default if Exception)

양 끝을 먼저 평가하고 가운데를 나중에 평가하므로 이 순서를 깨뜨립니다. (예외 목록은 대개 상수이므로) 이것이 심각한 문제로 보이지 않을 수도 있지만, 여러 except/if를 함께 쓰거나 기존 if/else와 함께 쓰거나 이 둘을 섞어 쓰는 등 여러 절이 만나는 경우에는 혼란을 더합니다. 선호하는 순서를 사용하면 문법이 어떻게 중첩되든 하위 표현식은 항상 왼쪽에서 오른쪽으로 평가됩니다.

기존 표기법은 유지하되 필수 괄호의 위치를 옮기면 다음과 같은 제안이 됩니다.:

value = expr except (Exception: default)
value = expr except(Exception: default)

이 형태는 함수 호출이나 딕셔너리 초기화 구문을 연상시킵니다. 콜론이 스위트를 시작하는 것으로 오해될 일은 없지만, 한편으로 새 문법은 지연 평가를 보장하는 반면 딕셔너리는 그렇지 않습니다. 혼란을 줄일 가능성이 있는 만큼 혼란을 늘릴 가능성도 있으므로, 혼란을 줄일 수 있다는 점만으로는 정당화되지 않는다고 판단합니다.

사용 예

표현식이 어떻게 파싱되는지 보여 주기 위해 각 예마다 거의 동등한 문장 형태를 함께 제시합니다. 두 형태가 항상 엄밀하게 동등하지는 않지만 같은 목적을 달성합니다. 인터프리터가 한쪽을 다른 쪽으로 변환하는 것은 결코 안전하지 않습니다.

이 예제 중 상당수는 Python 표준 라이브러리에서 그대로 가져온 것이며, 파일 이름과 줄 번호는 2014년 2월 초 기준으로 정확합니다. 이러한 패턴 중 상당수는 매우 흔합니다.

인자를 가져오되, 없으면 None을 기본값으로 사용:

cond = (args[1] except IndexError: None)

# Lib/pdb.py:803:
try:
    cond = args[1]
except IndexError:
    cond = None

가능하면 시스템에서 정보 가져오기:

pwd = (os.getcwd() except OSError: None)

# Lib/tkinter/filedialog.py:210:
try:
    pwd = os.getcwd()
except OSError:
    pwd = None

번역을 시도하되, 실패하면 원문으로 대체:

e.widget = (self._nametowidget(W) except KeyError: W)

# Lib/tkinter/__init__.py:1222:
try:
    e.widget = self._nametowidget(W)
except KeyError:
    e.widget = W

이터레이터에서 읽되, 소진된 뒤에는 빈 줄로 계속 진행:

line = (readline() except StopIteration: '')

# Lib/lib2to3/pgen2/tokenize.py:370:
try:
    line = readline()
except StopIteration:
    line = ''

플랫폼별 정보 가져오기(DRY 측면의 개선에 주목하십시오). 이 예제는 더 나아가 일련의 개별 할당문을 하나의 큰 dict 초기화로 바꿀 수도 있습니다:

# sys.abiflags may not be defined on all platforms.
_CONFIG_VARS['abiflags'] = (sys.abiflags except AttributeError: '')

# Lib/sysconfig.py:529:
try:
    _CONFIG_VARS['abiflags'] = sys.abiflags
except AttributeError:
    # sys.abiflags may not be defined on all platforms.
    _CONFIG_VARS['abiflags'] = ''

인덱스로 항목을 가져오되, 없으면 None을 기본값으로 사용(dict.get과 비슷함):

def getNamedItem(self, name):
    return (self._attrs[name] except KeyError: None)

# Lib/xml/dom/minidom.py:573:
def getNamedItem(self, name):
    try:
        return self._attrs[name]
    except KeyError:
        return None

숫자를 이름으로 변환하되, 실패하면 숫자 그대로 사용:

g = (grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid)
u = (pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid)

# Lib/tarfile.py:2198:
try:
    g = grp.getgrnam(tarinfo.gname)[2]
except KeyError:
    g = tarinfo.gid
try:
    u = pwd.getpwnam(tarinfo.uname)[2]
except KeyError:
    u = tarinfo.uid

속성을 조회하되, 없으면 기본값으로 대체:

mode = (f.mode except AttributeError: 'rb')

# Lib/aifc.py:882:
if hasattr(f, 'mode'):
    mode = f.mode
else:
    mode = 'rb'

return (sys._getframe(1) except AttributeError: None)
# Lib/inspect.py:1350:
return sys._getframe(1) if hasattr(sys, "_getframe") else None

EAFP 방식으로 긴 계산을 수행하면서, 0으로 나누기를 일종의 끈끈한(sticky) NaN처럼 처리:

value = (calculate(x) except ZeroDivisionError: float("nan"))

try:
    value = calculate(x)
except ZeroDivisionError:
    value = float("nan")

일련의 숫자의 평균을 계산하되, 실패하면 0으로 대체:

value = (statistics.mean(lst) except statistics.StatisticsError: 0)

try:
    value = statistics.mean(lst)
except statistics.StatisticsError:
    value = 0

드문드문 존재하는 재정의 목록에서 객체 조회하기:

(overrides[x] or default except IndexError: default).ping()

try:
    (overrides[x] or default).ping()
except IndexError:
    default.ping()

예외 포착 범위 좁히기

Python 표준 라이브러리에서 그대로 가져온 다음 예제들은 try/except의 범위를 얼마나 편리하게 좁힐 수 있는지 보여 줍니다. 이를 try/except 문 형태로 하려면 임시 변수가 필요하지만, 표현식으로 작성하면 훨씬 깔끔합니다.

Lib/ipaddress.py:343:

try:
    ips.append(ip.ip)
except AttributeError:
    ips.append(ip.network_address)

다음과 같이 바뀝니다:

ips.append(ip.ip except AttributeError: ip.network_address)

표현식 형태는 다음과 거의 동등합니다:

try:
    _ = ip.ip
except AttributeError:
    _ = ip.network_address
ips.append(_)

Lib/tempfile.py:130:

try:
    dirlist.append(_os.getcwd())
except (AttributeError, OSError):
    dirlist.append(_os.curdir)

다음과 같이 바뀝니다:

dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir)

Lib/asyncore.py:264:

try:
    status.append('%s:%d' % self.addr)
except TypeError:
    status.append(repr(self.addr))

다음과 같이 바뀝니다:

status.append('%s:%d' % self.addr except TypeError: repr(self.addr))

각 경우에 try/except의 범위가 좁혀지므로, 예상치 못한 예외(예를 들어 “append”의 철자가 틀렸을 때의 AttributeError)가 같은 핸들러에 잡히지 않습니다. 이것만으로 호출을 별도의 줄로 분리할(위의 다섯 줄짜리 예제처럼) 이유가 되기는 어렵지만, 변환의 부수 효과로 얻는 작은 이점입니다.

다른 언어와의 비교

(이 절을 정리해 준 Andrew Barnert에게 감사드립니다. 여기에 제시된 예제들은 제안의 현재 버전을 반영하지 않으므로 수정이 필요하다는 점에 유의하십시오.)

Ruby의 “begin…rescue…rescue…else…ensure…end”는 표현식입니다(내부에 문을 포함할 수도 있습니다). 이 표현식에는 “as” 절에 해당하는 것과 bare except에 해당하는 것이 있습니다. 또한 bare except/예외 클래스/as 절이 붙은 예외 클래스와 값 사이에 구두점이나 키워드를 전혀 사용하지 않습니다. (그리고 그렇습니다, Ruby의 문/표현식 규칙을 이해하지 못하면 모호합니다.)

x = begin computation() rescue MyException => e default(e) end;
x = begin computation() rescue MyException default() end;
x = begin computation() rescue default() end;
x = begin computation() rescue MyException default() rescue OtherException other() end;

이 PEP의 용어로 표현하면 다음과 같습니다:

x = computation() except MyException as e default(e)
x = computation() except MyException default(e)
x = computation() except default(e)
x = computation() except MyException default() except OtherException other()

Erlang에는 다음과 같은 형태의 try 표현식이 있습니다

x = try computation() catch MyException:e -> default(e) end;
x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end;

클래스와 “as” 이름은 필수이지만, 둘 중 어느 쪽이든 “_”를 사용할 수 있습니다. 각 절에는 선택적인 “when” 가드도 있고, 잡을 수 있는 “throw” 절도 있지만, 여기서는 다루지 않겠습니다. 여러 예외를 처리하려면 절들을 세미콜론으로 구분하기만 하면 되는데, 이는 Python에서는 쉼표에 대응될 것이라고 생각합니다. 따라서 다음과 같습니다:

x = try computation() except MyException as e -> default(e)
x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e)

Erlang에는 “catch” 표현식도 있는데, 같은 키워드를 사용함에도 완전히 다른 것이며, 굳이 알 필요는 없습니다.

ML 계열 언어에는 이를 처리하는 두 가지 방법, 즉 “handle”과 “try”가 있습니다. 둘의 차이점은 “try”가 예외에 대해 패턴 매칭을 수행한다는 것이며, 이를 통해 여러 except 절과 as 절을 사용한 것과 같은 효과를 얻을 수 있습니다. 어느 형태든 핸들러 절은 일부 방언에서는 “=>”로, 다른 방언에서는 “->”로 구분됩니다.

혼동을 피하기 위해 함수 호출은 Python 스타일로 작성하겠습니다.

SML의 “handle”은 다음과 같습니다

let x = computation() handle MyException => default();;

OCaml의 “try”는 다음과 같습니다

let x = try computation() with MyException explanation -> default(explanation);;

let x = try computation() with

    MyException(e) -> default(e)
  | MyOtherException() -> other_default()
  | (e) -> fallback(e);;

이 PEP의 용어로 표현하면 이들은 대략 다음과 같을 것입니다:

x = computation() except MyException => default()
x = try computation() except MyException e -> default()
x = (try computation()
     except MyException as e -> default(e)
     except MyOtherException -> other_default()
     except BaseException as e -> fallback(e))

ML에서 영감을 받았지만 직접적인 관련은 없는 학계의 여러 언어는 이를 조금씩 변형하는데, 대개 키워드를 더 많이 쓰고 기호를 더 적게 씁니다. 따라서 Oz의 방식을 Python으로 옮기면 다음과 같습니다

x = try computation() catch MyException as e then default(e)

Clojure와 같은 많은 Lisp 계열 언어는 try/catch를 특수 형식(special form)으로 구현하므로(이것이 무슨 뜻인지 모르겠다면 함수 형태의 매크로라고 생각하십시오), 사실상 다음과 같이 작성하게 됩니다

try(computation(), catch(MyException, explanation, default(explanation)))

try(computation(),
    catch(MyException, explanation, default(explanation)),
    catch(MyOtherException, explanation, other_default(explanation)))

Common Lisp에서는 이를 약간 더 투박한 “handler-case” 매크로로 처리하지만, 기본 개념은 같습니다.

놀랍게도 Lisp 스타일은 Lua처럼 매크로가 없는 일부 언어에서도 사용되는데, Lua에서는 xpcall이 함수를 인자로 받습니다. 람다를 Lua 스타일 대신 Python 스타일로 작성하면 다음과 같습니다

x = xpcall(lambda: expression(), lambda e: default(e))

이 코드는 실제로는 (true, expression()) 또는 (false, default(e))를 반환하지만, 그 부분은 무시해도 된다고 생각합니다.

이 점에서 Haskell은 사실 Lua와 비슷합니다(물론 모든 것이 모나드로 처리된다는 점은 다릅니다):

x = do catch(lambda: expression(), lambda e: default(e))

함수 안에서 패턴 매칭 표현식을 작성하여 예외를 어떻게 처리할지 결정할 수 있습니다. 원하지 않는 예외를 잡았다가 다시 발생시키는 비용이 충분히 저렴하므로, 이는 관용적인 방식으로 쓰입니다.

하지만 Haskell의 중위 표기법을 사용하면 이를 더 보기 좋게 작성할 수 있습니다:

x = do expression() `catch` lambda: default()
x = do expression() `catch` lambda e: default(e)

그리고 이렇게 하면 람다의 콜론과 이 제안의 except 콜론 사이의 유사성이 훨씬 더 분명해집니다:

x = expression() except Exception: default()
x = expression() except Exception as e: default(e)

Tcl에는 Lua의 xpcall의 나머지 절반이 있습니다. catch는 예외가 잡히면 참을, 그렇지 않으면 거짓을 반환하는 함수이며, 값은 다른 방법으로 꺼냅니다. 그리고 이 모든 것은 Tcl의 모든 것이 기반을 두고 있는 암묵적인 quote-and-exec를 중심으로 구성되어 있어서, Python 용어로 설명하기가 Lisp 매크로보다도 더 어렵습니다. 그래도 대략 다음과 같습니다.

if {[ catch("computation()") "explanation"]} { default(explanation) }

Smalltalk도 Python에 대응시키기가 다소 어렵습니다. 기본적인 형태는 다음과 같을 것입니다

x := computation() on:MyException do:default()

… 하지만 이것은 사실상 Smalltalk의 예외 처리 문법이 아니라, 콜론으로 인자를 전달하는 문법입니다.

보류된 하위 제안

여러 개의 except 절

사용 사례를 검토한 결과, 이 기능은 문장 형태에서만큼 자주 필요하지 않으며, 그 문법에 대해 합의가 이루어지지 않았으므로 기능 전체를 보류합니다.

여러 개의 ‘except’ 키워드를 사용할 수 있으며, 이들은 모두 원래 표현식에서 발생한 예외(만)를 잡습니다:

# Will catch any of the listed exceptions thrown by expr;
# any exception thrown by a default expression will propagate.
value = (expr
    except Exception1: default1
    except Exception2: default2
    # ... except ExceptionN: defaultN
)

현재는 다음 형태 중 하나를 사용해야 합니다:

# Will catch an Exception2 thrown by either expr or default1
value = (
    (expr except Exception1: default1)
    except Exception2: default2
)
# Will catch an Exception2 thrown by default1 only
value = (expr except Exception1:
    (default1 except Exception2: default2)
)

괄호 없이 여러 예외 절을 나열하는 것은 문법 오류이므로(위 참조), 향후 Python 버전에서 기존 코드를 깨뜨리지 않고 이 기능을 자유롭게 추가할 수 있습니다.

예외 객체 포착하기

try/except 블록에서 예외 객체를 포착하기 위해 ‘as’를 사용하면 지역 이름 바인딩이 만들어지고, (참조 순환이 생기지 않도록) finally 절에서 그 바인딩이 암묵적으로 삭제됩니다. 표현식 문맥에서는 이것이 별로 의미가 없으며, 예외 객체를 안전하게 포착하려면 제대로 된 하위 스코프가 필요할 것입니다. 이는 리스트 컴프리헨션을 처리하는 방식과 비슷한 것입니다. 하지만 CPython은 현재 컴프리헨션의 하위 스코프를 중첩된 함수 호출로 구현하는데, 이는 클래스 정의와 같은 일부 문맥에서 영향을 미치므로 이 제안에는 적합하지 않습니다. 향후 진정한 하위 스코프를 만드는 방법이 생긴다면(이는 컴프리헨션, except 표현식, with 블록 등을 단순화할 수 있을 것입니다) 이 제안을 되살릴 수 있을 것입니다. 그때까지는 이 기능이 없어도 큰 손실은 아닙니다. 여기서 사용하는 표현식 표기법에 잘 맞는 단순한 예외 처리는 일반적으로 예외의 값이 아니라 예외의 타입에만 관심을 두기 때문입니다. 자세한 분석은 아래에 있습니다.

물론 이 문법을 사용하면 대화형 Python에서 예외를 편리하게 포착할 수 있습니다. 반환값은 “_”에 저장되지만, 예외는 현재 저장되지 않습니다. 이는 다음과 같이 쓸 수 있을 것입니다:

>>> (expr except Exception as e: e)

Python 표준 라이브러리를 조사해 보면, ‘as’의 사용은 꽤 흔하지만(except 절 다섯 개 중 대략 하나에서 나타납니다), 논리적으로 표현식 형식으로 변환할 수 있는 경우에서는 극히 드물다는것을 알 수 있습니다. 그 몇 안 되는 사용처는 그냥 바꾸지 않고 두면 됩니다. 따라서 단순함을 위해 ‘as’ 절은 이 제안에 포함하지 않습니다. ‘as’는 이미 키워드이므로, 이후 Python 버전에서 기존 코드를 깨뜨리지 않고 이를 추가할 수 있습니다.

이것이 유용할 수도 있는 한 가지 예는 Lib/imaplib.py:568입니다:

try: typ, dat = self._simple_command('LOGOUT')
except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]

이는 다음과 같이 바뀔 수 있습니다:

typ, dat = (self._simple_command('LOGOUT')
    except BaseException as e: ('NO', '%s: %s' % (type(e), e)))

또는 다른 변형이 될 수도 있습니다. 이것이 가장 설득력 있는 사용 사례라고 하기는 어렵지만, 이 코드를 잘 살펴보면 상당히 깔끔하게 정리할 수 있을 것입니다. 예외 객체가 필요함을 보여 주는 다른 예가 없으므로, 저는 이 권고를 무기한 보류하기로 했습니다.

기각된 하위 제안

finally 절

문(statement) 형식인 try… finally 또는 try… except… finally에는 논리적으로 대응하는 표현식 형식이 없습니다. 따라서 finally 키워드는 어떤 식으로도 이 제안에 포함되지 않습니다.

다른 의미를 갖는 단독 except

제안된 여러 문법에서는 예외 타입 이름을 생략하는 것이 쉽고 간결하므로 그렇게 하고 싶은 유혹이 생길 것입니다. 편의를 위해 단독 ‘except’ 절이 “except BaseException”보다 더 유용한 무언가를 의미하도록 하는 것이 유리할 수도 있습니다. 제안된 방안으로는 Exception을 잡도록 하는 것, 특정한 “일반 예외” 집합(ExpressionError라는 새 타입의 서브클래스)을 잡도록 하는 것, 또는 현재 스코프에서 ExpressionError라는 이름의 튜플을 찾되 (ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError)와 같은 내장 기본값을 두는 것이 있었습니다. 이 모든 방안은 여러 가지 이유로 거부되었습니다.

  • 무엇보다도 try/except의 문 형식과의 일관성이 깨질 것입니다. 리스트 컴프리헨션이나 삼항 if 표현식을 세로로 늘어놓은 문 형식으로 “풀어 써서” 설명할 수 있는 것처럼, 표현식 except도 거의 동등한 문으로 비교적 기계적으로 변환하여 설명할 수 있어야 합니다. 따라서 양쪽에 공통된 문법 형태는 어느 쪽에서든 같은 의미를 가져야 하며, 무엇보다도 한쪽이 다른 쪽보다 더 많은 예외를 잡는 미묘한 차이가 있어서는 안 됩니다. 그러한 차이는 눈에 띄지 않는 버그를 불러들이기 쉽기 때문입니다.
  • 둘째로, 잡기에 적절한 예외의 집합 자체가 큰 논쟁거리가 될 것입니다. 어떤 예외를 잡는 것이 “타당한지” 정확히 예측하는 것은 불가능할 것입니다. 왜 일부 예외에만 편리한 문법이라는 특혜를 주고 다른 예외에는 주지 않아야 합니까?
  • 그리고 마지막으로(이는 부분적으로, 단독 except가 “합리적인” 예외 집합으로 축소되고 나면 그 사용을 적극 권장해야 한다는 권고가 있었기 때문입니다), 잡으리라 예상하지 않은 예외를 잡게 되는 모든 상황은 불필요하게 버그를 끌어들입니다.

결과적으로, 단독 ‘except’의 사용에는 두 가지 가능성만 남습니다. 표현식 형식에서 문법적으로 금지하거나, 문 형식과 정확히 같은 의미(즉, BaseException을 잡으며 ‘as’로 그 예외를 포착할 수 없는 의미)로 허용하는 것입니다.

단독 except 절

PEP 8은 단독 ‘except’를 사용하지 말라고 올바르게 권고합니다. 문에서는 문법적으로 허용되며 하위 호환성을 위해 계속 허용되어야 하지만, 그 사용을 장려할 가치는 거의 없습니다. 표현식 except 절에서 “except:”는 SyntaxError입니다. 대신 이와 동등한 긴 형태인 “except BaseException:”을 사용하십시오. 향후 버전의 Python에서는 이를 다시 도입하기로 결정할 수도 있으며(MAY), 이는 호환성을 깨뜨리지 않고 할 수 있습니다.

except 절을 감싸는 괄호

예외를 일으킬 수 있는 표현식과는 별도로 except 절을 괄호로 감싸는 것을 허용해야 합니까? 예시:

value = expr (
    except Exception1 [as e]: default1
    except Exception2 [as e]: default2
    # ... except ExceptionN [as e]: defaultN
)

이는 보류된 하위 제안인 다중 except 절과 예외 포착 중 하나 또는 둘 모두가 포함될 때 더 설득력이 있습니다. 그러한 하위 제안이 없다면 괄호는 다음과 같을 것입니다:

value = expr except ExceptionType: default
value = expr (except ExceptionType: default)

이점은 미미하며, 독자가 except 절이 표현식과 별개라고 생각하거나 이것을 함수 호출로 오해할 가능성이 있으므로 설득력이 없습니다. 물론 원한다면 표현식을 괄호로 감쌀 수 있으며, 기본값도 마찬가지입니다:

value = (expr) except ExceptionType: (default)

이제 전체 표현식을 괄호로 감싸야 하므로(이 논의가 이루어질 당시에는 결정되지 않았던 사항입니다), 이 부분을 구분할 필요성이 줄어들었으며 많은 경우에 이는 중복이 될 것입니다.

“except: pass”의 축약형

엄밀히 말해 표현식은 아니지만, 다음과 같은 것이 비슷한 축약형으로 제안되었습니다:

statement except Exception: pass

try:
    statement
except Exception:
    pass

예를 들어, 흔한 사용 사례는 파일 삭제를 시도하는 것입니다:

os.unlink(some_file) except OSError: pass

그러나 Python 3.4에는 이미 contextlib에 이와 동등한 기능이 있습니다:

from contextlib import suppress
with suppress(OSError): os.unlink(some_file)

이는 이미 한 줄이므로(콜론 뒤에서 줄을 바꾸면 두 줄), 이를 위해 새로운 문법을 도입하고 문과 표현식을 혼동하게 만들 필요가 거의 없습니다.

흔한 반론

콜론은 항상 스위트를 시작합니다

Python의 많은 문법 요소가 문장 스위트를 시작하는 데 콜론을 사용하는 것(if, while, with, for 등)은 사실이지만, 이것이 결코 콜론의 유일한 용도는 아닙니다. 현재 Python 문법에는 콜론이 하위 표현식을 시작하는 경우가 네 가지 있습니다.

  • 딕셔너리 디스플레이 - { … key:value … }
  • 슬라이스 표기법 - [start:stop:step]
  • 함수 정의 - 매개변수 : 어노테이션
  • lambda - 인자 목록: 반환값

이 제안은 단지 다섯 번째 경우를 추가할 뿐입니다:

  • except 표현식 - 예외 목록: 결과

스타일 가이드와 PEP 8은 줄바꿈된 줄의 끝에 콜론을 두지 않도록 권장해야 합니다. 그렇게 하면 스위트의 시작처럼 보일 수 있기 때문입니다. 대신 예외 목록 앞에서 줄을 바꾸어 콜론이 두 표현식 사이에 분명히 위치하도록 할 것을 권장해야 합니다.