Tuple[()]
()
파이썬에서 () 는 길이가 0 인 튜플의 리터럴 표현이다.
빈 튜플이라고 부른다.
tuple() 이나 tuple([]) 등의 표현으로도 빈 튜플을 만들 수 있다.
빈 튜플은 모두 () 와 같아서 == 연산자로 비교하면 항상 True 를 얻게 된다.
불변이라서 빈 튜플은 다른 값으로 바뀔 수도 없다.
그러니 싱글턴(singleton)으로 만들고 싶다는 충동이 생길 수도 있겠다.
싱글턴이 된다는 것은, None 을 다룰 때처럼, 비교할 때 == 대신 is 를 써도 좋다는 뜻이다.
실제로 CPython 에서는 싱글턴이다.1
>>> x = ()
>>> x is tuple([])
... True
하지만 이 성질은 CPython 에서 그렇다는 것일 뿐, 파이썬 언어가 요구하는 성질은 아니다.
파이썬 토론장에 올라온 토픽이 눈에 띈다.
Should we specify in the language reference that the empty tuple is a singleton?
이 성질을 언어 요구사항으로 강제할 것인지에 대한 토론으로 시작하더니, 싱글턴 성질을 활용해서 () 를 센티널(sentinel)로 사용하는 것의 위험성을 언급한다.
센티널이란 "지정하지 않았음", "값이 없음", "스트림의 끝에 도달했음" 등을 알리는 데 사용하는 특별한 값을 뜻한다.
센티널은 센티널을 제외한 가능한 모든 값과 구분되는 특별한 값이어야 한다.
전에는 () 를 센티널로 사용하려는 사람들이 있다고 상상해본 적도 없다.
하지만 한 가지 지적하자면, 빈 튜플인지 검사할 때, is 사용이 위험한 것이지 == 사용이 문제될 것은 없다.
필자의 센티널 스타일은 이렇다.
- 가능하면
None을 쓰자.- 그럴 수 없다면,
object()로 센티널 객체를 만들자.2
Tuple[()]
빈 튜플의 형을 표현하는 방법은 Tuple[()] 이다.
3.9 부터는 그냥 tuple[()] 을 사용해도 된다.
from typing import Tuple
empty_tuple: Tuple[()] = ()
길이가 2 인 튜플의 예를 들면 Tuple[float, float].
길이가 1 인 튜플의 예를 들면 Tuple[int].
이 패턴을 따르자면 길이가 0 인 튜플은 Tuple[] 이 되겠지만, 파이썬에서 이런 표현은 허락하지 않는다.
그렇다고 [] 없는 Tuple 을 쓸 수도 없는데, 이 표현은 모든 종류의 튜플을 가리지 않고 허용하겠다는 뜻이기 때문이다.
그래서 꼼수로 Tuple[()] 이라는 특수한 표현을 빈 튜플을 나타내는 데 쓰기로 했다.
아마 꼭 필요한 사례를 찾기는 힘들 것이다. 하지만 여러분이, 분모가 0 이 되는 조건을 따져보지 않았다고, 자다가 벌떡 일어나는 사람이라면, 이 표현 없이 살 수는 없다.
get_args
그런데, 파이썬 표준 라이브러리에서 () 를 센티널로 사용하는 곳이 있다.
바로 typing.get_args 다.
3.8: get_args(Tuple[()]) == ((),)
get_args 는 제네릭 형의 형 인자를 추출하는 함수인데, 파이썬 3.8 에서 처음 등장했다.
가령 get_args(Tuple[float, float]) == (float, float) 가 성립한다.
즉 형 인자를 튜플로 반환한다.
문제는 형 인자가 없는 경우에 사용하면 예외를 일으키거나 None 을 반환하는 대신 () 를 반환한다는 것이다.
즉 get_args(Tuple) == () 가 성립한다.
마음에 들지는 않지만, 참아줄 수는 있다.
반환값이 언제나 튜플이라서 검사 없이 언패킹할 수 있는 편리함도 있다.
파이썬 문법은 Tuple[] 같은 표현을 허락하지 않아서, () 가 정상적인 값일 수는 없다.
빈 튜플 형에 대해 적용해보면 특별한 예외 규칙 없이 ((),) 를 반환한다.
따라서 () 가 반환된다면 형 인자가 지정되지 않았다고 판단해도 좋다.
3.9, 3.10: get_args(tuple[()]) == ()
하지만 3.9 에서 이 진술이 무너진다.
이제 제네릭 형에도 Tuple 대신 tuple 을 쓸 수 있게 되었다.
따라서 빈 튜플을 표현하는 방법으로 tuple[()] 도 가능해졌다.
하지만 무슨 이유에서인지 get_args(tuple[()]) == () 이 된 것이다.
Tuple 은 여전히 3.8 처럼 동작한다.
즉, Tuple[()] 과 tuple[()] 이 get_args 에 대해 다른 값을 반환하기 시작했다.
적어도 get_args 로는 tuple 과 tuple[()] 을 구별할 수 없다.
이 어색한 상황은 3.10 까지 계속된다.
3.11+: get_args(Tuple[()]) == ()
그러다가 3.11 에서 바뀐다. What's New 에 다음과 같은 구절이 있다.
The representation of empty tuple types (Tuple[()]) is simplified. This affects introspection, e.g. get_args(Tuple[()]) now evaluates to () instead of ((),). (Contributed by Serhiy Storchaka in gh-91137.)3
반대의 변경이라면 좋았겠지만, 이제 tuple[()] 과 Tuple[()] 의 동작이 같아지기는 했다.
이 결과가 3.14 까지 이어지고 있다.
우리는 get_args 로는 Tuple[()] 과 Tuple 을 구분할 수 없는 세상에서 살게 되었다.
대단히 큰 문제라고 볼 수는 없다.
__args__ 어트리뷰트라는 대안도 있으니 피해가는 것도 어렵지 않다.
하지만 쾌적한 운전을 방해하는 자그마한 싱크홀 정도는 된다.
-
글 쓰는 시점의 CPython 최신 안정판은 3.14 다. ↩︎
-
dataclasses.MISSING처럼. 하지만 이 방법만으로는 프로세스 경계를 건널 수 없다. ↩︎ -
gh-91137 은 Empty typing.Tuple 이다. ↩︎