파이썬에서 None과 인자 생략을 구분하는 법

알림 주소를 비우려고 None을 넘겼는데 기존 주소가 그대로 남는다. 함수는 ‘비워 달라’와 ‘건드리지 마’를 어떻게 구분할 수 있을까?

파이썬에서 None과 인자 생략을 구분하는 법
세 줄 요약
  • None이 실제 입력값이라면 ‘인자를 주지 않음’에는 별도 표시값이 필요하다.
  • Python 3.15의 sentinel을 한 번 만들고, 기본값과 is 비교에 같은 객체를 쓴다.
  • 이름이 같아도 새로 생성한 sentinel은 다른 객체다. 복사와 달리 재생성은 동일성을 보존하지 않는다.

None을 받으려면 기본값을 따로 둔다

설정 함수에서 인자를 생략하면 기존 값을 유지하고, None을 주면 값을 비우려는 경우가 있다. 기본값도 None으로 두면 함수 안에서 두 호출을 구분할 수 없다. Python 3.15의 내장 sentinel은 이때 쓸 고유한 표시값을 만든다.

아래 예제는 알림 주소의 다음 값을 결정한다. 인자를 생략하면 이전 주소를 돌려주고, None이나 새 주소를 전달하면 그 값을 돌려준다.

MISSING = sentinel("MISSING")

def next_address(current, new=MISSING):
    if new is MISSING:
        return current
    return new

assert next_address("[email protected]") == "[email protected]"
assert next_address("[email protected]", None) is None
assert next_address("[email protected]", "[email protected]") == "[email protected]"

핵심은 MISSING이라는 이름 자체가 아니다. 기본값과 비교 대상이 같은 객체여야 한다. PEP 661도 None이 유효한 값일 때 별도 센티널이 필요한 상황을 도입 이유로 든다.

if not new로 바꾸면 다른 값까지 섞인다

누락 검사는 new is MISSING으로 한다. if not new는 None뿐 아니라 빈 문자열·0·False도 같은 분기로 보낼 수 있어, ‘인자를 주었는가’라는 질문에 답하지 않는다. 새 내장 sentinel 자체는 참으로 평가된다.

sentinel 호출은 매번 고유한 객체를 만든다. 다른 파일에서 sentinel("MISSING")을 다시 호출하는 것으로 원래 표시값을 재현할 수 없다. 여러 함수가 같은 표시값을 주고받는다면 하나를 정의하고 가져다 써야 한다.

MISSING = sentinel("MISSING")
another = sentinel("MISSING")
assert another is not MISSING

이름은 출력에서 객체의 용도를 읽게 해준다. 같은 문자열이 찍힌다고 같은 객체가 되는 것은 아니다.

object()도 구분은 하지만 표현과 복사가 다르다

기존의 MISSING = object()도 한 객체를 재사용하면 None과 생략을 나눌 수 있다. 내장 sentinel의 이점은 표시값의 이름이 읽히고, 얕은 복사와 깊은 복사 뒤에도 같은 객체를 얻는다는 데 있다.

따라서 단순 함수 하나에서만 비교하던 코드를 새 기능이라는 이유로 모두 바꿀 필요는 없다. 표시값이 복사되는 자료구조에 들어가거나 여러 모듈의 함수 계약에 등장할 때 새 타입의 특성이 더 유용하다. 내장 sentinel은 3.15부터 제공되므로 이전 버전도 지원하는 라이브러리는 지원 버전과 기존 표시값의 동작을 함께 고려해야 한다.

피클은 표시값을 모듈과 이름으로 찾는다

sentinel을 피클로 저장하고 읽어도 같은 객체를 얻으려면, 그 객체를 모듈과 이름으로 찾을 수 있어야 한다. 가장 단순한 형태는 가져올 수 있는 모듈의 전역에 MISSING = sentinel("MISSING")을 정의하는 것이다.

함수 안에서만 만든 로컬 sentinel은 피클로 저장할 수 없다. 복사에 강하다는 특성을 모든 저장 경로에 일반화해서는 안 된다. 입력값을 구분할 목적이라면 먼저 하나의 표시값을 공유하고, 저장이 필요해질 때 그 정의 위치까지 정해야 한다. 그러면 None의 의미를 바꾸지 않고 생략을 별도의 선택으로 유지할 수 있다.