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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
C# [1], Dart [2], Perl, Swift 및 PHP(버전 7부터)를 비롯한 여러 현대 프로그래밍 언어에는 이른바 “null-병합” 또는 “null-인식” 연산자가 있습니다. ECMAScript(즉, JavaScript)에 이러한 연산자를 추가하기 위한 3단계 초안 제안도 있습니다. [3] [4]. 이러한 연산자는 null 참조와 관련된 일반적인 패턴에 대한 구문 설탕을 제공합니다.
- “
null-병합” 연산자는 왼쪽 피연산자가null이 아니면 왼쪽 피연산자를 반환하는 이항 연산자입니다. 그렇지 않으면 오른쪽 피연산자를 반환합니다. - “
null-인식 멤버 접근” 연산자는 인스턴스가null이 아닌 경우에만 해당 인스턴스 멤버에 접근합니다. 그렇지 않으면null을 반환합니다. (이 연산자는 “안전한 탐색” 연산자라고도 합니다.) - “
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 규칙은 ?? 이항 연산자를 제공합니다. 대부분의 이항 연산자와 달리, 왼쪽 피연산자가 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에 a 속성이 있는 경우에만 a에서 속성 b를 읽고, 그렇지 않으면 AttributeError를 발생시킨다고 읽지 않는 것처럼, “a?.b”를 a가 None이 아닌 경우에만 a에서 속성 b를 읽는다고 읽을 필요도 없습니다(청자가 특정 동작을 알아야 하는 컨텍스트가 아니라면).
?? 연산자를 사용하는 결합 표현의 경우, 표현은 “None이면 … 또는”이나 “…와 결합된”으로 읽어야 합니다. 예를 들어, a.get_value() ?? 100이라는 표현은 “a 점 get_value를 호출하거나 None이면 100” 또는 “a 점 get_value를 호출한 결과와 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 데이터베이스에서 웹 사이트 정보를 가져와 JSON으로 형식화한 후 HTTP 클라이언트로 전송합니다.:
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은 이미 언어에서 “값 없음”을 나타내는 값으로 정의되어 있고, 현재 명세는 향후 프로토콜로 전환하는 것을 배제하지 않으므로(다만 내장 객체를 변경하면 호환되지 않습니다), 이 아이디어는 현재 거부합니다.
불리언 인식 연산자
이 제안은 본질적으로 값 없음 프로토콜을 추가하는 것과 동일하므로, 위의 논의도 적용됩니다.
?? 연산자와 유사한 동작은 or 표현식으로 구현할 수 있지만, or는 왼쪽 피연산자가 거짓으로 평가되는지를 확인하며 구체적으로 None인지를 확인하지는 않습니다. 이 접근 방식은 언어 변경이 더 적게 필요하므로 매력적이지만, 궁극적으로 근본적인 문제를 올바르게 해결하지는 못합니다.
검사가 None이 아닌 진릿값을 대상으로 한다고 가정하면, 더 이상 ?? 연산자가 필요하지 않습니다. 그러나 이 검사를 ?. 및 ?[] 연산자에 적용하면 완전히 유효한 연산이 적용되지 못하게 됩니다.
get_log_list()가 현재 로그 메시지를 포함하는 리스트(비어 있을 수도 있음) 또는 로깅이 활성화되어 있지 않은 경우 None을 반환할 수 있는 다음 예를 고려하십시오.:
lst = get_log_list()
lst?.append('A log message')
?.가 구체적으로 None이 아닌 참인 값을 검사하고 로그가 어떤 항목으로도 초기화되지 않았다면, 어떤 항목도 추가되지 않습니다. 이는 항목을 추가하려는 코드의 명백한 의도에 어긋납니다. 빈 리스트에서도 append 메서드를 사용할 수 있으며 다른 모든 리스트 메서드도 마찬가지이므로, 리스트가 현재 비어 있다는 이유만으로 이러한 멤버를 사용해서는 안 된다고 가정할 이유가 없습니다.
또한 표현식 대신 사용할 타당한 결과도 없습니다. 일반적인 lst.append는 None을 반환하지만, 이 개념에 따르면 lst?.append는 lst의 값에 따라 [] 또는 None이 될 수 있습니다. 이전 절의 예와 마찬가지로 이는 말이 되지 않습니다.
None이 아닌 진릿값을 검사하면 겉보기에 유효한 표현식이 더 이상 의도한 대로 실행되지 않으므로, 이 아이디어는 거부됩니다.
예외 인식 연산자
None을 만났을 때 표현식을 단락 평가하는 이유는 정상적인 상황에서 발생할 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?는 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에는 Maybe라는 개념이 있으며, 이는 특수 키워드(예: null)나 특수 인스턴스(예: None)에 의존하지 않고 선택적 값이라는 개념을 캡슐화합니다. Haskell에서 Maybe의 목적은 “무언가”와 아무것도 아닌 것을 별도로 처리하지 않도록 하는 것입니다.
pymaybe라는 Python 패키지는 이를 대략적으로 구현합니다. 문서에는 다음 예시가 나와 있습니다.:
>>> maybe('VALUE').lower()
'value'
>>> maybe(None).invalid().method().or_else('unknown')
'unknown'
maybe() 함수는 Something 인스턴스나 Nothing 인스턴스를 반환합니다. 앞 절에서 설명한 단항 후위 연산자와 유사하게, Nothing은 누락된 값에 대한 체이닝을 허용하기 위해 던더 메서드를 재정의합니다.
pymaybe의 래퍼에서 기반 값을 가져오려면 결국 or_else()가 필요하다는 점에 유의하십시오. 또한 pymaybe는 어떤 평가도 단락시키지 않습니다. pymaybe에는 몇 가지 장점이 있고 그 자체로 유용할 수 있지만, 언어에 내장된 지원이 없는 순수 Python 방식의 병합 구현이 왜 그만큼 강력하지 않은지도 보여 줍니다.
이 시나리오를 가능하게 하기 위해 내장 maybe 형식을 추가하자는 아이디어는 거부되었습니다.
조건식을 사용하십시오.
기본값을 초기화하는 또 다른 일반적인 방법은 삼항 연산자를 사용하는 것입니다. 다음은 인기 있는 Requests package에서 발췌한 내용입니다.:
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 ?? {}
참고 문헌
Copyright
This document has been placed in the public domain.