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에 사용되는 콜론보다 나쁘지는 않을 것입니다(def의 콜론을 연상시키는 점이, 여기의 콜론이 try/except의 콜론을 연상시키는 것과 같습니다). 또한 나열된 대안보다 확실히 낫습니다.

하지만 제가 납득할 수 없는 부분은 동기와 근거입니다. 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 기법을 사용할 수 있습니다(예를 들어 어떤 시퀀스가 인덱싱하기에 충분한 길이인지 확인하는 경우입니다). 이는 모든 경우에 안전하지 않지만, 편리한 경우가 많기 때문에 프로그래머는 EAFP의 안전성을 LBYL의 표기상 간결함을 위해 희생하고 싶은 유혹을 받습니다. 또한 일부 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에 대한 바람을 expresses 합니다. 이와 유사한 것이 과거에도 requested multiple times 요청되었습니다.

제안

‘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” 제안의 맥락에서 새로운 키워드 “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와 평행하고, “keyword name_list: subexpression”을 사용하여 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)

이는 함수 호출이나 딕셔너리 초기화와 유사합니다. 콜론은 suite를 도입하는 것으로 혼동될 수 없지만, 반면 새로운 구문은 딕셔너리와 달리 지연 평가를 보장합니다. 혼란을 줄일 가능성은 혼란을 늘릴 가능성에 비추어 정당화되지 않는 것으로 간주됩니다.

사용 예

각 예제에는 표현식이 어떻게 구문 분석되는지 보여 주기 위해 대략적으로 동등한 명령문 형식이 제시됩니다. 이러한 형식이 항상 엄밀하게 동등한 것은 아니지만 동일한 목적을 달성합니다. 인터프리터가 한 형식을 다른 형식으로 변환하도록 하는 것은 안전하지 않습니다.

이러한 예제 중 상당수는 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 개선에 주목하십시오). 이 특정 예제는 일련의 개별 할당을 하나의 큰 딕셔너리 초기화로 바꾸어 더 발전시킬 수도 있습니다.:

# 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으로 나누는 경우를 일종의 고정된 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’s의 “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’s 의 “handle”입니다.

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

다음은 OCaml’s 의 “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를 특수 형식으로 구현하므로(이것이 무슨 뜻인지 모른다면 함수와 비슷한 매크로라고 생각하십시오), 사실상 다음과 같이 작성합니다.

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

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

Common Lisp에서는 약간 더 번거로운 “handler-case” macro, 를 사용하여 이를 수행하지만, 기본 개념은 동일합니다.

놀랍게도 Lisp 방식은 매크로가 없는 일부 언어에서도 사용되며, 예를 들어 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는 예외가 잡혔으면 true를, 그렇지 않으면 false를 반환하는 함수이며, 값은 다른 방법으로 가져옵니다. 그리고 이 모든 것은 Tcl의 모든 것이 기반으로 하는 암묵적 quote-and-exec를 중심으로 구성되어 있으므로, Lisp 매크로보다 Python 용어로 설명하기가 훨씬 더 어렵지만, 대략 다음과 같습니다.

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’의 사용은 상당히 일반적이지만(대략 5개의 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 절

명령문 형식인 try… finally 또는 try… except… finally에 논리적으로 대응하는 표현식 형식은 없습니다. 따라서 finally 키워드는 어떤 방식으로도 이 제안의 일부가 아닙니다.

다른 의미를 갖는 단독 except

제안된 여러 구문에서는 예외 타입 이름을 생략하는 것이 쉽고 간결하며 매력적으로 보일 수 있습니다. 편의를 위해 단독 ‘except’ 절이 “except BaseException”보다 더 유용한 의미를 갖도록 하는 것이 유리할 수도 있습니다. 제안에는 해당 절이 Exception을 포착하도록 하거나, “일반적인 예외”의 특정 집합(표현식 오류라는 새 타입의 서브클래스)을 포착하도록 하거나, 현재 스코프에서 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의 향후 버전에서는 호환성을 깨뜨리지 않고 이를 다시 도입할 수도 있습니다.

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]
  • 함수 정의 - parameter : annotation
  • 람다 - 인자 목록: 반환 값

이 제안은 다섯 번째 경우를 단순히 추가합니다:

  • except 표현식 - 예외 목록: result

스타일 가이드와 PEP 8은 줄바꿈된 줄의 끝에 콜론을 두지 않도록 권장해야 합니다. 줄 끝의 콜론은 스위트의 도입처럼 보일 가능성이 있기 때문입니다. 대신 예외 목록 앞에서 줄바꿈하여 콜론이 두 표현식 사이에 있음을 명확히 유지하도록 권장해야 합니다.