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

Python 개선 제안 한국어 번역

PEP 505 – None 인식 연산자

Author:
Mark E. Haase <mehaase at gmail.com>, Steve Dower <steve.dower at python.org>
Status:
Deferred
Type:
Standards Track
Created:
18-Sep-2015
Python-Version:
3.8

Table of Contents

번역·라이선스 안내

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

초록

C# [1], Dart [2], Perl, Swift, PHP(버전 7부터) 등 여러 현대 프로그래밍 언어에는 이른바 “null 병합” 또는 “null 인식” 연산자가 있습니다. 또한 ECMAScript(일명 JavaScript)에 이러한 연산자를 추가하자는 3단계 초안 제안도 있습니다 [3] [4]. 이 연산자들은 null 참조를 다루는 흔한 패턴에 대한 문법적 설탕(syntactic sugar)을 제공합니다.

  • “null 병합” 연산자는 왼쪽 피연산자가 null이 아니면 왼쪽 피연산자를 반환하는 이항 연산자입니다. 그렇지 않으면 오른쪽 피연산자를 반환합니다.
  • “null 인식 멤버 접근” 연산자는 인스턴스가 null이 아닐 때에만 그 인스턴스의 멤버에 접근합니다. 그렇지 않으면 null을 반환합니다. (이를 “안전 탐색(safe navigation)” 연산자라고도 합니다.)
  • “null 인식 인덱스 접근” 연산자는 컬렉션이 null이 아닐 때에만 그 컬렉션의 요소에 접근합니다. 그렇지 않으면 null을 반환합니다. (이는 또 다른 유형의 “안전 탐색” 연산자입니다.)

이 PEP는 위에서 다룬 정의와 다른 언어의 구현을 바탕으로 Python에 세 가지 None 인식 연산자를 제안합니다. 구체적으로는 다음과 같습니다.

  • “None 병합” 이항 연산자 ??는 왼쪽 피연산자가 None이 아닌 값으로 평가되면 왼쪽 피연산자를 반환하고, 그렇지 않으면 오른쪽 피연산자를 평가하여 반환합니다. 병합 복합 대입 연산자 ??=도 포함됩니다.
  • “None 인식 속성 접근” 연산자 ?.(“maybe dot”)는 왼쪽 피연산자가 None이 아닌 값으로 평가되면 전체 표현식을 평가합니다
  • “None 인식 인덱싱” 연산자 ?[](“maybe subscript”)는 왼쪽 피연산자가 None이 아닌 값으로 평가되면 전체 표현식을 평가합니다

필요한 문법 변경의 세부 사항과 예시는 Grammar changes 절을 참조하십시오.

새 연산자를 사용하도록 수정할 수 있는 코드의 좀 더 현실적인 예시는 Examples 절을 참조하십시오.

문법과 의미

None의 특수성

None 객체는 값의 부재를 나타냅니다. 이 연산자들에서 값의 부재는 표현식의 나머지 부분에도 값이 없으므로 평가하지 않아야 함을 의미합니다.

불리언 문맥에서 “거짓”으로 평가되는 모든 값을 값이 없는 것으로 취급하자는 제안은 기각되었습니다. 그러나 이 연산자들의 목적은 “거짓” 상태가 아니라 “값의 부재” 상태를 전파하는 것입니다.

이것이 None을 특별하게 만든다고 주장하는 사람들도 있습니다. 저희는 None이 이미 특별하며, 이를 이 연산자들의 검사 대상이자 결과로 사용하더라도 기존 의미론은 전혀 바뀌지 않는다고 주장합니다.

대안적 접근 방식에 대한 논의는 Rejected Ideas 절을 참조하십시오.

문법 변경

Python 문법의 다음 규칙은 다음과 같이 갱신됩니다:

augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
            '<<=' | '>>=' | '**=' | '//=' | '??=')

power: coalesce ['**' factor]
coalesce: atom_expr ['??' factor]
atom_expr: ['await'] atom trailer*
trailer: ('(' [arglist] ')' |
          '[' subscriptlist ']' |
          '?[' subscriptlist ']' |
          '.' NAME |
          '?.' NAME)

병합(coalesce) 규칙

coalesce 규칙은 ?? 이항 연산자를 제공합니다. 대부분의 이항 연산자와 달리, 좌변이 None으로 판정될 때까지 우변은 평가되지 않습니다.

?? 연산자는 다른 이항 연산자보다 더 강하게 결합합니다. 이러한 연산자의 기존 구현 대부분이 None 값을 전파하지 않기 때문입니다(보통은 TypeError를 발생시킵니다). None이 될 가능성이 있다고 알려진 표현식은 괄호를 추가하지 않고도 기본값으로 대체할 수 있습니다.

?? 연산자가 있을 때 연산자 우선순위를 평가하면서 암묵적 괄호가 어떻게 놓이는지 보여 주는 몇 가지 예:

a, b = None, None
def c(): return None
def ex(): raise Exception()

(a ?? 2 ** b ?? 3) == a ?? (2 ** (b ?? 3))
(a * b ?? c // d) == a * (b ?? c) // d
(a ?? True and b ?? False) == (a ?? True) and (b ?? False)
(c() ?? c() ?? True) == True
(True ?? ex()) == True
(c ?? ex)() == c()

특히 a ?? 2 ** b ?? 3 같은 경우에는 하위 표현식에 다른 방식으로 괄호를 치면 TypeError가 발생합니다. int.__pow__는 None으로 호출할 수 없기 때문입니다(그리고 ?? 연산자를 사용한다는 사실 자체가 a 또는 b가 None일 수 있음을 뜻합니다). 하지만 늘 그렇듯이 괄호가 필수는 아니더라도 가독성을 높이는 데 도움이 된다면 추가해야 합니다.

?? 연산자에 대한 복합 대입도 추가됩니다. 증강 병합 할당은 이름의 현재 값이 None인 경우에만 그 이름을 다시 바인딩합니다. 대상 이름에 이미 값이 있으면 우변은 평가되지 않습니다. 예를 들어:

a = None
b = ''
c = 0

a ??= 'value'
b ??= undefined_name
c ??= shutil.rmtree('/')    # don't try this at home, kids

assert a == 'value'
assert b == ''
assert c == 0 and any(os.scandir('/'))

maybe-dot 연산자와 maybe-subscript 연산자

maybe-dot 연산자와 maybe-subscript 연산자는 아톰의 트레일러로 추가되므로, 할당 대상의 일부로 쓰이는 경우를 포함하여 일반 연산자와 똑같은 모든 위치에서 사용할 수 있습니다(자세한 내용은 아래에서 설명합니다). 기존 평가 규칙은 문법에 직접 포함되어 있지 않으므로, 필요한 변경 사항을 아래에 명시합니다.

atom은 항상 성공적으로 평가된다고 가정합니다. 그런 다음 각 trailer가 왼쪽에서 오른쪽으로 평가되며, 자신의 매개변수(인자, 첨자 또는 속성 이름)를 적용하여 다음 trailer에 쓰일 값을 만들어 냅니다. 마지막으로, await가 있다면 이를 적용합니다.

예를 들어, await a.b(c).d[e]는 현재 ['await', 'a', '.b', '(c)', '.d', '[e]']로 구문 분석되어 다음과 같이 평가됩니다:

_v = a
_v = _v.b
_v = _v(c)
_v = _v.d
_v = _v[e]
await _v

None-인식 연산자가 있으면 왼쪽에서 오른쪽으로 진행되는 평가가 단락될 수 있습니다. 예를 들어, await a?.b(c).d?[e]는 다음과 같이 평가됩니다:

_v = a
if _v is not None:
    _v = _v.b
    _v = _v(c)
    _v = _v.d
    if _v is not None:
        _v = _v[e]
await _v

Note

이 맥락에서 await는 코드가 await None을 시도하는 경우와 마찬가지로 거의 확실히 실패합니다. 여기서 None-인식 await 키워드를 추가하자고 제안하는 것은 아니며, atom_expr 문법 규칙이 이 키워드를 포함하고 있으므로 명세의 완전성을 위해 이 예에 포함했을 뿐입니다. 이 키워드가 별도의 규칙에 있었다면 전혀 언급하지 않았을 것입니다.

괄호로 묶인 표현식은 atom 규칙(위에는 나와 있지 않습니다)이 처리하며, 이 규칙은 위 변환의 단락 동작을 암묵적으로 종료합니다. 예를 들어, (a?.b ?? c).d?.e는 다음과 같이 평가됩니다:

# a?.b
_v = a
if _v is not None:
    _v = _v.b

# ... ?? c
if _v is None:
    _v = c

# (...).d?.e
_v = _v.d
if _v is not None:
    _v = _v.e

할당 대상으로 사용될 때 None-인식 연산은 “로드” 컨텍스트에서만 사용할 수 있습니다. 즉, a?.b = 1과 a?[b] = 1은 SyntaxError를 발생시킵니다. 표현식의 앞부분에서 사용하는 것(a?.b.c = 1)은 허용되지만, 병합 연산과 결합하지 않는 한 유용할 가능성은 낮습니다:

(a?.b ?? d).c = 1

표현식 읽기

maybe-dot 연산자와 maybe-subscript 연산자의 경우, 이러한 연산자를 포함하는 표현식을 이 연산자들의 일반 버전과 마찬가지로 읽고 해석하도록 하는 것이 의도입니다. “일반적인” 경우에는 a?.b?[c]와 같은 표현식과 a.b[c]의 최종 결과가 동일할 것이며, 현재 우리가 “a.b”를 “a에서 속성 b를 읽되 a에 속성 a가 있을 때만 그러하고, 없으면 AttributeError를 발생시킴”으로 읽지 않는 것처럼, “a?.b”를 “a에서 속성 b를 읽되 a가 None이 아닐 때만 그러함”으로 읽을 필요도 없습니다(듣는 사람이 구체적인 동작을 알아야 하는 맥락이 아니라면 말입니다).

?? 연산자를 사용하는 병합 표현식은 “or … if None”(None이면 …) 또는 “coalesced with”(…와 병합)로 읽어야 합니다. 예를 들어 a.get_value() ?? 100 표현식은 “call a dot get_value or 100 if None” 또는 “call a dot get_value coalesced with 100”으로 읽습니다.

Note

코드를 말로 읽으면 항상 정보가 손실되므로, 이 연산자들을 모호함 없이 읽는 방법을 정의하려고 시도하지는 않습니다. 이러한 제안은 새 문법을 추가할 때 생기는 영향에 대한 맥락을 더하기 위한 것입니다.

예시

이 절에서는 흔히 볼 수 있는 None 패턴의 예제를 몇 가지 제시하고, 이를 None 인식 연산자를 사용하도록 변환하면 어떤 모습이 되는지 보여 줍니다.

표준 라이브러리

find-pep505.py 스크립트 [5]를 사용하여 Python 3.7 표준 라이브러리를 분석한 결과, None 인식 연산자 중 하나를 사용하도록 바꿀 수 있는 코드 조각이 최대 678개 발견되었습니다.:

$ find /usr/lib/python3.7 -name '*.py' | xargs python3.7 find-pep505.py
<snip>
Total None-coalescing `if` blocks: 449
Total [possible] None-coalescing `or`: 120
Total None-coalescing ternaries: 27
Total Safe navigation `and`: 13
Total Safe navigation `if` blocks: 61
Total Safe navigation ternaries: 8

그중 일부를 새 연산자를 사용하도록 변환하기 전과 후의 예제로 아래에 보입니다.

bisect.py에서 발췌:

def insort_right(a, x, lo=0, hi=None):
    # ...
    if hi is None:
        hi = len(a)
    # ...

??= 복합 대입문을 사용하도록 갱신한 후:

def insort_right(a, x, lo=0, hi=None):
    # ...
    hi ??= len(a)
    # ...

calendar.py에서 발췌:

encoding = options.encoding
if encoding is None:
    encoding = sys.getdefaultencoding()
optdict = dict(encoding=encoding, css=options.css)

?? 연산자를 사용하도록 수정한 후:

optdict = dict(encoding=options.encoding ?? sys.getdefaultencoding(),
               css=options.css)

email/generator.py에서 발췌 (이 경우에는 ?? 대신 or를 쓸 방법이 없다는 점에 특히 유의하십시오):

mangle_from_ = True if policy is None else policy.mangle_from_

수정한 후:

mangle_from_ = policy?.mangle_from_ ?? True

asyncio/subprocess.py에서 발췌:

def pipe_data_received(self, fd, data):
    if fd == 1:
        reader = self.stdout
    elif fd == 2:
        reader = self.stderr
    else:
        reader = None
    if reader is not None:
        reader.feed_data(data)

?. 연산자를 사용하도록 수정한 후:

def pipe_data_received(self, fd, data):
    if fd == 1:
        reader = self.stdout
    elif fd == 2:
        reader = self.stderr
    else:
        reader = None
    reader?.feed_data(data)

asyncio/tasks.py에서 발췌:

try:
    await waiter
finally:
    if timeout_handle is not None:
        timeout_handle.cancel()

?. 연산자를 사용하도록 수정한 후:

try:
    await waiter
finally:
    timeout_handle?.cancel()

ctypes/_aix.py에서 발췌:

if libpaths is None:
    libpaths = []
else:
    libpaths = libpaths.split(":")

수정한 후:

libpaths = libpaths?.split(":") ?? []

os.py에서 발췌:

if entry.is_dir():
    dirs.append(name)
    if entries is not None:
        entries.append(entry)
else:
    nondirs.append(name)

?. 연산자를 사용하도록 수정한 후:

if entry.is_dir():
    dirs.append(name)
    entries?.append(entry)
else:
    nondirs.append(name)

importlib/abc.py에서 발췌:

def find_module(self, fullname, path):
    if not hasattr(self, 'find_spec'):
        return None
    found = self.find_spec(fullname, path)
    return found.loader if found is not None else None

일부를 수정한 후:

def find_module(self, fullname, path):
    if not hasattr(self, 'find_spec'):
        return None
    return self.find_spec(fullname, path)?.loader

대폭 수정한 후 (지나치다고 볼 수도 있지만, 이는 스타일 가이드가 판단할 문제입니다):

def find_module(self, fullname, path):
    return getattr(self, 'find_spec', None)?.__call__(fullname, path)?.loader

dis.py에서 발췌:

def _get_const_info(const_index, const_list):
    argval = const_index
    if const_list is not None:
        argval = const_list[const_index]
    return argval, repr(argval)

?[] 및 ?? 연산자를 사용하도록 수정한 후:

def _get_const_info(const_index, const_list):
    argval = const_list?[const_index] ?? const_index
    return argval, repr(argval)

jsonify

이 예제는 Flask 프레임워크를 프런트엔드로 사용하는 Python 웹 크롤러에서 가져온 것입니다. 이 함수는 SQL 데이터베이스에서 웹 사이트 정보를 조회한 뒤 HTTP 클라이언트로 보낼 수 있도록 JSON 형식으로 만듭니다.:

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        return jsonify(
            first_seen=site.first_seen.isoformat() if site.first_seen is not None else None,
            id=site.id,
            is_active=site.is_active,
            last_seen=site.last_seen.isoformat() if site.last_seen is not None else None,
            url=site.url.rstrip('/')
        )

first_seen과 last_seen은 모두 데이터베이스에서 null이 될 수 있으며, JSON 응답에서도 null이 될 수 있습니다. JSON에는 datetime을 표현하는 기본 방법이 없으므로, 서버의 계약은 null이 아닌 모든 날짜를 ISO-8601 문자열로 표현한다고 규정합니다.

first_seen과 last_seen 속성의 정확한 의미를 알지 못하면, 해당 속성에 여러 번 접근해도 안전한지, 성능상 문제가 없는지 알 수 없습니다.

이 코드를 고치는 한 가지 방법은 각 조건 표현식을 명시적인 값 대입과 완전한 if/else 블록으로 바꾸는 것입니다:

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        first_seen_dt = site.first_seen
        if first_seen_dt is None:
            first_seen = None
        else:
            first_seen = first_seen_dt.isoformat()

        last_seen_dt = site.last_seen
        if last_seen_dt is None:
            last_seen = None
        else:
            last_seen = last_seen_dt.isoformat()

        return jsonify(
            first_seen=first_seen,
            id=site.id,
            is_active=site.is_active,
            last_seen=last_seen,
            url=site.url.rstrip('/')
        )

이렇게 하면 함수에 코드 열 줄과 새로운 코드 경로 네 개가 추가되어, 겉보기 복잡도가 크게 증가합니다. None을 인식하는 속성 연산자를 사용하여 다시 작성하면 의도가 더 명확하고 더 짧은 코드가 됩니다:

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        return jsonify(
            first_seen=site.first_seen?.isoformat(),
            id=site.id,
            is_active=site.is_active,
            last_seen=site.last_seen?.isoformat(),
            url=site.url.rstrip('/')
        )

Grab

다음 예제는 Grab이라는 Python 스크래핑 라이브러리에서 가져온 것입니다:

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        if ctype is None:
            return 'application/octet-stream'
        else:
            return ctype

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        if filename is None:
            self.filename = self.get_random_filename()
        else:
            self.filename = filename
        if content_type is None:
            self.content_type = self.find_content_type(self.filename)
        else:
            self.content_type = content_type

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        if filename is None:
            self.filename = os.path.split(path)[1]
        else:
            self.filename = filename
        if content_type is None:
            self.content_type = self.find_content_type(self.filename)
        else:
            self.content_type = content_type

이 예제에는 기본값을 제공해야 하는 경우의 좋은 예가 여럿 들어 있습니다. 조건 표현식을 사용하도록 다시 작성하면 전체 코드 줄 수는 줄어들지만, 가독성이 반드시 향상되는 것은 아닙니다:

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        return 'application/octet-stream' if ctype is None else ctype

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        self.filename = (self.get_random_filename() if filename
            is None else filename)
        self.content_type = (self.find_content_type(self.filename)
            if content_type is None else content_type)

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        self.filename = (os.path.split(path)[1] if filename is
            None else filename)
        self.content_type = (self.find_content_type(self.filename)
            if content_type is None else content_type)

첫 번째 삼항 표현식은 깔끔하지만, 피연산자의 직관적인 순서를 뒤집습니다. ctype에 값이 있으면 그것을 반환하고, 없으면 문자열 리터럴을 대체값으로 사용해야 합니다. 나머지 삼항 표현식은 직관적이지 않고, 너무 길어서 줄을 나눠야 합니다. 전체적인 가독성은 향상되기는커녕 오히려 나빠집니다.

None 병합 연산자를 사용하여 다시 작성하면 다음과 같습니다:

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        return ctype ?? 'application/octet-stream'

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        self.filename = filename ?? self.get_random_filename()
        self.content_type = content_type ?? self.find_content_type(self.filename)

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        self.filename = filename ?? os.path.split(path)[1]
        self.content_type = content_type ?? self.find_content_type(self.filename)

이 문법은 피연산자의 순서가 직관적입니다. 예를 들어 find_content_type에서는 선호하는 값인 ctype이 대체값보다 앞에 나옵니다. 또한 문법이 간결하기 때문에 코드 줄 수가 줄고 눈으로 해석해야 할 코드도 적어지며, 왼쪽에서 오른쪽으로, 위에서 아래로 읽는 순서가 실행 흐름을 더 정확하게 따릅니다.

기각된 아이디어

이 절의 처음 세 아이디어는 None을 특별하게 취급하는 방식에 대한 대안으로 자주 제안되는 것들입니다. 이들이 기각된 이유에 대한 자세한 배경은 PEP 531과 PEP 532에서 다룬 내용과 관련 논의를 참고하십시오.

값 없음 프로토콜

어떤 값이 “값 없음”을 나타내는지를 표시하는 프로토콜을 정의하면 연산자를 사용자 정의 타입으로 일반화할 수 있습니다. 그러한 프로토콜은 값이 있는 것으로 취급해야 하면 True를, 값이 없는 것으로 취급해야 하면 False를 반환하는 던더 메서드 __has_value__(self)가 될 수 있습니다.

이렇게 일반화하면 object는 다음과 동등한 던더 메서드를 구현하게 됩니다:

def __has_value__(self):
    return True

NoneType은 다음과 동등한 던더 메서드를 구현하게 됩니다:

def __has_value__(self):
    return False

명세 절에서 x is None을 사용하는 모든 부분은 not x.__has_value__()로 대체됩니다.

이렇게 일반화하면 도메인에 특화된 “값 없음” 객체도 None처럼 병합할 수 있게 됩니다. 예를 들어 pyasn1 패키지에는 ASN.1 null을 나타내는 Null이라는 타입이 있습니다:

>>> from pyasn1.type import univ
>>> univ.Null() ?? univ.Integer(123)
Integer(123)

마찬가지로 math.nan이나 NotImplemented 같은 값도 값 없음을 나타내는 것으로 취급할 수 있습니다.

그러나 이러한 값의 “값 없음” 성질은 도메인에 특화된 것이므로, 언어는 이들을 값으로 취급해야 합니다. 예를 들어 math.nan.imag는 잘 정의되어 있으므로(0.0입니다), math.nan?.imag를 단락 평가하여 math.nan을 반환하는 것은 올바르지 않습니다.

None은 이미 언어에서 “값 없음”을 나타내는 값으로 정의되어 있고, 현재 명세가 향후 프로토콜로 전환하는 것을 막지도 않으므로(다만 내장 객체에 대한 변경은 호환되지 않을 것입니다), 이 아이디어는 당분간 기각합니다.

불리언 인식 연산자

이 제안은 근본적으로 값 없음(no-value) 프로토콜을 추가하는 것과 같으므로, 위의 논의가 여기에도 그대로 적용됩니다.

?? 연산자와 비슷한 동작은 or 표현식으로 얻을 수 있지만, or는 왼쪽 피연산자가 특별히 None인지가 아니라 거짓 같은(false-y) 값인지를 검사합니다. 이 접근 방식은 언어 변경이 더 적게 필요하므로 매력적이지만, 결국 근본적인 문제를 올바르게 해결하지 못합니다.

검사 대상이 None이 아니라 참 같은 값(truthiness)이라고 가정하면, 더 이상 ?? 연산자가 필요하지 않습니다. 그러나 이 검사를 ?. 연산자와 ?[] 연산자에 적용하면 완벽하게 유효한 연산이 적용되지 못합니다

get_log_list()가 현재 로그 메시지를 담은 리스트(비어 있을 수 있음)를 반환하거나, 로깅이 활성화되지 않았으면 None을 반환할 수 있는 다음 예를 생각해 보십시오:

lst = get_log_list()
lst?.append('A log message')

?.가 특별히 None인지가 아니라 참 값인지를 검사하고 로그가 아무 항목 없이 초기화되었다면, 항목은 절대 추가되지 않습니다. 이는 항목을 추가하려는 코드의 명백한 의도에 어긋납니다. 다른 모든 리스트 메서드와 마찬가지로 append 메서드는 빈 리스트에서도 사용할 수 있으며, 리스트가 현재 비어 있다는 이유로 이러한 멤버를 사용하면 안 된다고 가정할 이유가 없습니다.

게다가 표현식 대신 사용할 합리적인 결과값도 없습니다. 일반적인 lst.append는 None을 반환하지만, 이 아이디어에서는 lst?.append가 lst의 값에 따라 [] 또는 None이 될 수 있습니다. 앞 절의 예와 마찬가지로 이는 말이 되지 않습니다.

None이 아니라 참 같은 값을 검사하면 겉보기에 유효한 표현식이 더 이상 의도한 대로 실행되지 않으므로, 이 아이디어는 기각됩니다.

예외 인식 연산자

None을 만났을 때 표현식을 단락(short-circuit)시키는 이유는, 일반적인 상황이라면 발생했을 AttributeError나 TypeError를 피하기 위해서라고 볼 수도 있습니다. None을 검사하는 대신, ?. 연산자와 ?[] 연산자가 연산에서 발생한 AttributeError와 TypeError를 처리하고 표현식의 나머지 부분을 건너뛸 수도 있습니다.

이렇게 하면 a?.b.c?.d.e는 다음과 비슷하게 변환됩니다:

_v = a
try:
    _v = _v.b
except AttributeError:
    pass
else:
    _v = _v.c
    try:
        _v = _v.d
    except AttributeError:
        pass
    else:
        _v = _v.e

한 가지 미해결 문제는 예외가 처리되었을 때 표현식의 값으로 어떤 값을 반환해야 하는가입니다. 위의 예는 부분적인 결과를 그대로 남겨 두지만, 이는 기본값으로 대체하는 데 도움이 되지 않습니다. 대안으로 결과를 강제로 None으로 만들 수 있지만, 그러면 왜 None이 결과가 될 만큼은 특별하면서 검사 대상이 될 만큼은 특별하지 않은가라는 의문이 생깁니다.

둘째로, 이 접근 방식은 표현식의 일부로 암묵적으로 실행되는 코드 내부의 오류를 가립니다. ?.의 경우 프로퍼티나 __getattr__ 구현 내부의 모든 AttributeError가 숨겨지며, ?[]와 __getitem__ 구현에서도 마찬가지입니다.

마찬가지로 {}?.ietms() 같은 단순한 오타도 눈에 띄지 않고 넘어갈 수 있습니다.

getattr 내장 함수와 dict가 확립한 .get(key, default) 메서드 패턴이라는 형태로 이러한 종류의 오류를 처리하는 기존 관례가 있다는 것은, 이미 이 동작을 명시적으로 사용할 수 있음을 보여 줍니다.

이 접근 방식은 코드의 오류를 숨기므로 기각됩니다.

None 인식 함수 호출

None 인식 문법은 속성 접근과 인덱스 접근에 적용되므로, 함수 호출 문법에도 적용해야 하는지 묻는 것은 자연스러워 보입니다. 이는 foo?()처럼 쓸 수 있으며, 여기서 foo는 None이 아닐 때만 호출됩니다.

이 기능은 제안된 연산자가 임의의 클래스 계층 구조 순회가 아니라 부분적으로 채워진 계층적 자료 구조의 순회를 돕기 위한 것이라는 근거로 보류되었습니다. 이는 이미 이 문법을 제공하는 다른 주류 언어 중 어느 것도 선택적 함수 호출을 위한 비슷한 문법을 지원할 가치가 있다고 판단하지 않았다는 사실에도 반영되어 있습니다.

C#에서 사용하는 방법과 비슷한 우회 방법은 maybe_none?.__call__(arguments)라고 작성하는 것입니다. 호출 가능 객체가 None이면 식은 평가되지 않습니다. (C#에서는 같은 동작을 위해 호출 가능 타입에 ?.Invoke()를 사용합니다.)

? 단항 후위 연산자

None을 인식하는 동작을 일반화하고 새로 도입되는 연산자의 수를 제한하기 위해 ?로 표기하는 단항 후위 연산자가 제안되었습니다. 이 아이디어의 요지는 ?가 self를 반환하는 던더 메서드를 오버라이드하는 특수 객체를 반환할 수 있다는 것입니다. 예를 들어 foo?는 foo가 None이 아니면 foo로 평가되고, 그렇지 않으면 NoneQuestion의 인스턴스로 평가됩니다:

class NoneQuestion():
    def __call__(self, *args, **kwargs):
        return self

    def __getattr__(self, name):
        return self

    def __getitem__(self, key):
        return self

이 새로운 연산자와 새로운 타입을 사용하면 foo?.bar[baz] 같은 식은 foo가 None일 때 NoneQuestion으로 평가됩니다. 이는 멋진 일반화이지만, 기존 코드 대부분은 NoneQuestion이 무엇인지 모르기 때문에 실제로 사용하기는 어렵습니다.

앞에서 동기로 제시한 예제 중 하나로 돌아가서 다음을 살펴보십시오:

>>> import json
>>> created = None
>>> json.dumps({'created': created?.isoformat()})

JSON 직렬화기는 NoneQuestion을 직렬화하는 방법을 모르며, 다른 어떤 API도 마찬가지입니다. 실제로 이 제안을 적용하려면 표준 라이브러리와 모든 서드파티 라이브러리 전반에 걸쳐 많은 특수 로직이 필요합니다.

동시에 ? 연산자는 다른 어떤 연산자와도 결합할 수 있다는 점에서 너무 일반적일 수도 있습니다. 다음 식들은 무엇을 의미해야 하겠습니까?:

>>> x? + 1
>>> x? -= 1
>>> x? == 1
>>> ~x?

이 정도의 일반화는 유용하지 않습니다. 이 문서에서 실제로 제안하는 연산자는 일반적인 코드 패턴을 더 쉽게 작성할 수 있게 해 줄 것으로 기대되는 몇 가지 연산자로 의도적으로 제한되어 있습니다.

내장 maybe

Haskell에는 특별한 키워드(예: null)나 특별한 인스턴스(예: None)에 의존하지 않고 선택적 값이라는 개념을 캡슐화하는 Maybe라는 개념이 있습니다. Haskell에서 Maybe의 목적은 “무언가”와 없음”을 따로 처리하지 않도록 하는 것입니다.

pymaybe라는 Python 패키지가 대략적인 근사치를 제공합니다. 해당 문서에는 다음 예제가 나와 있습니다:

>>> maybe('VALUE').lower()
'value'

>>> maybe(None).invalid().method().or_else('unknown')
'unknown'

maybe() 함수는 Something 인스턴스나 Nothing 인스턴스 중 하나를 반환합니다. 앞 절에서 설명한 단항 후위 연산자와 마찬가지로, Nothing은 누락된 값에 대해 연쇄 호출을 할 수 있도록 던더 메서드를 오버라이드합니다.

pymaybe의 래퍼에서 실제 값을 꺼내려면 결국 or_else()가 필요하다는 점에 유의하십시오. 게다가 pymaybe는 어떤 평가도 단락(short circuit)하지 않습니다. pymaybe는 몇 가지 장점이 있고 그 자체로 유용할 수도 있지만, coalescing의 순수 Python 구현이 언어에 내장된 지원에 비해 훨씬 덜 강력한 이유도 보여 줍니다.

이 시나리오를 가능하게 하기 위해 내장 maybe 타입을 추가하자는 아이디어는 기각되었습니다.

그냥 조건식을 사용하십시오

기본값을 초기화하는 또 다른 일반적인 방법은 삼항 연산자를 사용하는 것입니다. 다음은 널리 쓰이는 Requests 패키지에서 발췌한 부분입니다:

data = [] if data is None else data
files = [] if files is None else files
headers = {} if headers is None else headers
params = {} if params is None else params
hooks = {} if hooks is None else hooks

이 형태는 피연산자를 직관적이지 않은 순서로 배치한다는 바람직하지 않은 결과를 낳습니다. 사람은 “가능하면 data를 사용하고, 대체값으로 []를 사용한다”라고 생각하지만, 코드는 대체값을 선호하는 값보다 앞에 둡니다.

이 패키지의 작성자는 대신 다음과 같이 작성할 수도 있었습니다:

data = data if data is not None else []
files = files if files is not None else []
headers = headers if headers is not None else {}
params = params if params is not None else {}
hooks = hooks if hooks is not None else {}

이렇게 피연산자를 배치하는 순서가 더 직관적이지만, (“not “ 때문에) 4글자가 추가로 필요합니다. 또한 data if data, files if files 등과 같이 식별자가 반복된다는 점도 두드러집니다.

None 병합 연산자를 사용하여 작성하면 예제는 다음과 같습니다:

data = data ?? []
files = files ?? []
headers = headers ?? {}
params = params ?? {}
hooks = hooks ?? {}

참고 문헌