[Python] str.split vs str.split(maxsplit) vs str.partition

파이썬에서 문자열을 특정 구분자로 자를 수 있게 해주는 builtin 함수로 str.split, str.rsplit, str.partition, str.rpartition이 있다.

그리고 str.split함수에는 최대 몇번 자를 지 지정해줄 수 있는 maxsplit 파라미터가 있는데, 어떤 상황에서 사용하면 좋을 지 알아보자!

str.split

가장 많이 사용되는 str.split이다. 구분자로 전체 문자열을 잘라서 배열로 저장한다.

In [3]: s = "hi_hello_1_2_3_4_5"
In [4]: s.split("_")
Out[4]: ['hi', 'hello', '1', '2', '3', '4', '5']

여기서 maxsplit를 적용하면 한 번만 자르고 그 뒤에는 그대로 돌려준다.

In [3]: s = "hi_hello_1_2_3_4_5"
In [4]: s.split("_", maxsplit=1)
Out[4]: ['hi', 'hello_1_2_3_4_5']

무슨 장점이 있을까?

먼저 메모리 측면에서 장점이 있다. 거의 두 배 차이..잘라지는 요소가 많아지면 많아질수록 더 큰 효과를 볼 수 있다.

문자열보다 배열이 큰 이유는 알잘딱깔센 찾아보자!

In [7]: getsizeof(s.split("_"))
Out[7]: 152

In [8]: getsizeof(s.split("_", maxsplit=1))
Out[8]: 72

두번째로는 속도 측면에서 장점이 있다. 요것도 잘라지는 요소가 많아지면 많아질수록 효과가 크다.

In [16]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.split('_', maxsplit=1)")
Out[16]: 0.20580519600002845

In [17]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.split('_')")
Out[17]: 0.2410702570000467

여기서 흥미로운 점 하나 추가! positional argument로 전달하는 것이 속도 측면에서 어느정도 이점이 있는데, 이유는 keyword argument를 해석하는 과정에서 오버로드가 발생하기 때문이다. 물론 이 차이는 문자열의 길이에 비례하지 않기 때문에 코드를 읽는 사람을 생각해서 keyword argument를 사용하는 것도 나쁘지 않다.

In [15]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.split('_', 1)")
Out[15]: 0.17547050000001718

In [16]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.split('_', maxsplit=1)")
Out[16]: 0.20580519600002845

str.rsplit

다음은 str.rsplit이다. 요건 보통 maxsplit이랑 같이 사용되는데, str.split 문자열의 왼쪽부터 잘라내는데 그러면 맨 오른쪽에 있는 값을 찾기 위해서는 항상 문자열 전체를 잘라내야 하기 때문에 오른쪽에 있는 값을 빠르게 가져오기 위해서 str.rsplit을 사용한다.

아까와 다르게 5가 따로 잘린 것을 확인할 수 있다. maxsplit을 사용하는 이유는 str.split에서 설명한 것과 동일하다.

In [18]: s.rsplit("_", maxsplit=1)
Out[18]: ['hi_hello_1_2_3_4', '5']

str.partition, str.rpartition

흠 그러면 split 함수가 있는데 str.partition은 왜 있는 걸까? 그냥 str.split으로 지지고 볶고 하면 될 거 같은데..🤔

라는 생각이 들 수 있지만 str.partition은 첫번째 요소를 가져오는데 특화된 녀석이다.

In [19]: s.partition("_")
Out[19]: ('hi', '_', 'hello_1_2_3_4_5')

str.rpartition은? r이 붙어있는 걸 보니 str.rsplit과 같이 오른쪽에서 잘라오겠지요?

In [20]: s.rpartition("_")
Out[20]: ('hi_hello_1_2_3_4', '_', '5')

하나 가져오기용인건 알겠는데 무슨 장점있냐! 속도 측면에서 차이가 있다.

In [21]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.partition('_')")
Out[21]: 0.13420031200007543

In [22]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.split('_', 1)")
Out[22]: 0.20099570999991556

In [23]: timeit.timeit("'hi_hello_1_2_3_4_5_6_7_8_9_10'.split('_', 1)")
Out[23]: 0.16223106200004622

와우..positional argument로 던진 것보다 빠른 것을 확인할 수 있다.

그렇다면 요녀석은 대체 왜 빠른 것일까..

이런 건..CPython 코드를 까보는게 가장 빠르다.

먼저, str.split부터 살펴보자.. C언어를 잘 몰라서 코드를 기가맥히게 풀이는 할 수 없당 ㅠ..그래도 대충 보면 sep_len이 구분자의 길이인 거 같고..길이가 1일때는 split_char 함수를 수행하는 것을 알 수 있다.

STRINGLIB(split)(PyObject* str_obj
    ...
    if (sep_len == 0) {
        PyErr_SetString(PyExc_ValueError, "empty separator");
        return NULL;
    }
    else if (sep_len == 1)
        return STRINGLIB(split_char)(str_obj, str, str_len, sep[0], maxcount);

split_char를 보면..

STRINGLIB(split_char)(PyObject* str_obj,
    ...
    while ((j < str_len) && (maxcount-- > 0)) {
        for(; j < str_len; j++) {
            /* I found that using memchr makes no difference */
            if (str[j] == ch) {
                SPLIT_ADD(str, i, j);
                i = j = j + 1;
                break;
            }
        }
    }

전체 문자열을 돌면서 구분자와 일치하는 문자일때 SPLIT_ADD하는 것을 볼 수 있다. 대충 여기까지만 보고, str.partition을 살펴보자.

STRINGLIB(partition)(PyObject* str_obj,
    if (sep_len == 0) {
        PyErr_SetString(PyExc_ValueError, "empty separator");
        return NULL;
    }

    out = PyTuple_New(3);
    if (!out)
        return NULL;

    pos = FASTSEARCH(str, str_len, sep, sep_len, -1, FAST_SEARCH);

out이 3개의 요소를 가진 tuple이니까 아까 봤던 Out[19]: ('hi', '_', 'hello_1_2_3_4_5') 요값인 거 같은데, split_char와는 다르게 pos라는 변수가 있다!

    ...
    PyTuple_SET_ITEM(out, 0, STRINGLIB_NEW(str, pos));
    Py_INCREF(sep_obj);
    PyTuple_SET_ITEM(out, 1, sep_obj);
    pos += sep_len;
    PyTuple_SET_ITEM(out, 2, STRINGLIB_NEW(str + pos, str_len - pos));

요 변수로 문자열을 잘라내서 out 변수에 값을 할당하는 걸 알 수 있는데, pos 값을 구하는데 FASTSEARCH라는 함수를 사용하고 있다.. 아무래도 요 녀석이 속도 차이의 핵심인듯한데, 어떤 함수길래 속도 차이를 내고 있는 것일까?

요기서 구현 코드를 볼 수 있기는 한데.. 다른 코드에 비해서 너무 길어서 내가 분석하기에는 좀 귀찮다 ㅋ.. 그래서 킹갓오버플로우에서 찾아보니 Boyer-Moore 알고리즘을 구현한 거라고 한다.(FYI. https://stackoverflow.com/a/12244891) 옛날 옛적에 구현해본 적이 있는데 지금 보니 역시나 잘 모르겠다 ㅎㅋ..암튼 빠르니까 좋은 거겟지 뭐 하하하하하

글이 좀 길어졌는데 이번 글은 여기서 마쳐야겠다.(절대 BM 알고리즘을 분석하기 싫어서 그런 거 아닙니다 6ㅇㅅㅇ..)


Written by@EHX
Software Developer, Back-End Engineer

GitHubFacebook