파이썬의 iterator 을 뽑아주는 빌트인 함수 next
공식문서에서도 보듯,
next(iterator)
혹은 next(iterator, default)
같은 식으로 쓰이고 default 가 주어지지 않았을 때 iterator에 남아있는 것이 없을 때 StopIteration
에러가 발생한다.
default 파라미터의 값을 주지 않고 에러 핸들링을 하지 않아 예상과 달리 동작한 스크립트를 팀원과 같이 수정하다가 파이참에서 Go Definition (vim 단축키는 gd, 혹은 커맨드+클릭)으로 next
를 확인해보니 다음과 같았다.
참고로 해당 코드가 담긴 파일의 이름은 builtins.py
def next(iterator, default=None): # real signature unknown; restored from __doc__
"""
next(iterator[, default])
Return the next item from the iterator. If default is given and the iterator
is exhausted, it is returned instead of raising StopIteration.
"""
pass
여기서 이상한게 파이썬식 인터페이스라면 아래와 같은 함수는
def some_method(a, b=None):
pass
some_method('a')
와 some_method('a', None)
의 동작은 같아야 한다.
그런데 왜 next(iterator)
과 next(iterator, None)
의 동작은 다르고 어떻게 그런거지 라는 의문이 들어 cPython 코드에서 next
가 어떻게 구현되어 있는지까지 찾아봤다.
링크 에서 아래의 코드를 찾을 수 있었는데,
static PyObject *
builtin_next(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *it, *res;
if (!_PyArg_CheckPositional("next", nargs, 1, 2))
return NULL;
it = args[0];
if (!PyIter_Check(it)) {
PyErr_Format(PyExc_TypeError,
"'%.200s' object is not an iterator",
Py_TYPE(it)->tp_name);
return NULL;
}
res = (*Py_TYPE(it)->tp_iternext)(it);
if (res != NULL) {
return res;
} else if (nargs > 1) {
PyObject *def = args[1];
if (PyErr_Occurred()) {
if(!PyErr_ExceptionMatches(PyExc_StopIteration))
return NULL;
PyErr_Clear();
}
return Py_NewRef(def);
} else if (PyErr_Occurred()) {
return NULL;
} else {
PyErr_SetNone(PyExc_StopIteration);
return NULL;
}
}
코드에서 nargs
가 인자의 개수인데, 여기에서 next(iterator)
와 next(iterator, None)
이 분기가 되는 것이고, 인자가 1개일 때만 PyExc_StopIteration
를 일으키는 것이다.
조금 더 생각해보면 None과 Null 이 다르니 이렇게 cPython 에선 인자의 개수로 처리하는 게 맞겠다 싶고.
그런데 파이썬식 인터페이스는 def next(iterator, default=None)
로 정의가 되어 있어, cPython 으로 구현된 빌트인 메서드의 인터페이스를 파이썬식으로 표현하려면 충돌나는 부분이 있구나... 정도로 납득하고 넘어가려고 했다. (disclaimer 로 real signature unknown; restored from __doc__
란 주석도 달아왔고ㅋㅋ)
이 사실을 공유하니 팀원 분이 builtins.py
는 파이썬에서 만든 공식(?) 인터페이스 모음 함수는 아니고 파이참에서 만든 타이핑을 위한 파일이라는 걸 지적해주셨다. 그제서야 파일 경로를 보니 PyCharm > python_stubs > 123123 > builtins.py
그리고 vscode 로 go definition 을 하니 아래와 같은 코드로 이동한 것도 확인했다.
# 경로 .vscode > extensions > ms-python.vscode-pylance > dist > stdlib > typing.pyi
@runtime_checkable
class Iterator(Iterable[_T_co], Protocol[_T_co]):
@abstractmethod
def __next__(self) -> _T_co: …
def __iter__(self) -> Iterator[_T_co]: …
결국 파이썬의 귀여운 허점을 찾아냈다고 생각했지만 파이참의 허점이었다.
어쨌건 파이썬의 동작 방식을 이해하기 위해 cPython 코드까지 찾아본 건 처음이라 재밌었다.