파이썬에서 문자열을 연결하는 가장 좋은 방법은 무엇입니까?


358

파이썬은 string바꿀 수 없기 때문에 문자열을보다 효율적으로 연결하는 방법이 궁금합니다.

나는 그렇게 쓸 수 있습니다 :

s += stringfromelsewhere

또는 이와 같이 :

s = []
s.append(somestring)

later

s = ''.join(s)

이 질문을 쓰는 동안 나는 그 주제에 관해 좋은 기사를 발견했습니다.

http://www.skymind.com/~ocrow/python_string/

그러나 그것은 Python 2.x에 있습니다. 그래서 질문은 Python 3에서 뭔가 변경 되었습니까?


답변:


433

최고의 문자열 변수에 문자열을 추가의 방법은 사용하는 것입니다 ++=. 읽기 쉽고 빠르기 때문입니다. 그들은 또한 빨리, 당신이 선택하는 것은 맛의 문제이며, 후자는 가장 일반적입니다. timeit모듈 타이밍은 다음과 같습니다 .

a = a + b:
0.11338996887207031
a += b:
0.11040496826171875

그러나 목록을 갖고 목록에 추가 한 다음 해당 목록을 조인하는 것을 권장하는 사람들은 목록에 문자열을 추가하는 것이 문자열 확장에 비해 매우 빠르기 때문에 그렇게합니다. 어떤 경우에는 이것이 사실 일 수 있습니다. 예를 들어, 여기에 하나의 문자열에 백만 개의 문자열이 추가되고, 먼저 문자열에 추가 된 다음 목록에 추가됩니다.

a += b:
0.10780501365661621
a.append(b):
0.1123361587524414

결과 문자열의 길이가 백만자인 경우에도 추가 속도가 여전히 빨라졌습니다.

자, 천자 길이의 문자열을 십만 번 추가해 봅시다 :

a += b:
0.41823482513427734
a.append(b):
0.010656118392944336

따라서 종료 문자열은 약 100MB 길이입니다. 목록에 추가하는 것이 훨씬 빨랐습니다. 해당 타이밍에는 final이 포함되지 않습니다 a.join(). 얼마나 오래 걸립니까?

a.join(a):
0.43739795684814453

웁스. 이 경우에도 추가 / 가입 속도가 느려집니다.

이 추천은 어디에서 왔습니까? 파이썬 2?

a += b:
0.165287017822
a.append(b):
0.0132720470428
a.join(a):
0.114929914474

글쎄, 매우 긴 문자열을 사용하면 추가 / 가입이 약간 빠릅니다 (일반적으로 그렇지 않은 경우 메모리에 100MB 인 문자열은 무엇입니까?)

그러나 실제 클린 처는 Python 2.3입니다. 타이밍이 너무 느려서 아직 끝나지 않았기 때문에 타이밍을 보여주지 않습니다. 이러한 테스트는 갑자기 몇 분이 걸립니다 . append / join을 제외하고, 이후 파이썬에서와 같이 빠릅니다.

예. 석기 시대에 파이썬에서 문자열 연결은 매우 느 렸습니다. 그러나 2.4에서는 더 이상 (또는 적어도 Python 2.4.7) 그렇지 않으므로 Python 2.3 업데이트가 중단 된 2008 년 add / join을 사용하는 권장 사항이 오래되어 사용을 중지해야합니다. :-)

(업데이트 : 테스트를보다 신중하게 수행했을 때 밝혀졌습니다. ++=빠른 두 개의 문자열에 대한 파이썬 2.3은 물론이고 사용하기 추천. ''.join()오해이어야 함)

그러나 이것은 CPython입니다. 다른 구현에는 다른 문제가있을 수 있습니다. 이것이 바로 조기 최적화가 모든 악의 근원 인 또 다른 이유입니다. 먼저 측정하지 않는 한 "빠른"것으로 생각되는 기술을 사용하지 마십시오.

따라서 문자열 연결을 수행하는 "최상의"버전은 + 또는 + =를 사용하는 것입니다. 입니다. 그리고 그것이 당신에게 느린 것으로 판명되면, 그것은 거의 불가능합니다. 그렇다면 다른 것을하십시오.

왜 내 코드에 많은 추가 / 결합을 사용합니까? 때로는 실제로 더 명확하기 때문입니다. 특히 함께 연결해야 할 경우 공백이나 쉼표 또는 줄 바꿈으로 구분해야합니다.


10
여러 개의 문자열이있는 경우 (n> 10) "".join (list_of_strings)이 여전히 더 빠릅니다.
Mikko Ohtamaa

11
+ =가 빠른 이유는 참조 횟수가 1 인 경우 cpython에서 성능 해킹이 발생하기 때문입니다. (python 빌드를 제외하고는 거의 모든 다른 파이썬 구현에서 거의 차이가 있습니다)
Ronny

17
왜 이렇게 많이 불려지고 있습니까? 하나의 특정 구현에서만 효율적이고 2 차 시간 알고리즘을 수정하기 위해 취약한 핵에 상당하는 알고리즘을 사용하는 것이 어떻게 더 낫습니까? 또한 "초기 최적화는 모든 악의 근원"이라는 요점을 완전히 이해하지 못합니다. 그 인용은 SMALL 최적화에 관한 것입니다. 이것은 작은 최적화가 아닌 O (n ^ 2)에서 O (n)으로 진행됩니다.
Wes

12
실제 인용문은 다음과 같습니다. "우리는 시간의 97 % 정도의 작은 효율성을 잊어야합니다. 조기 최적화는 모든 악의 근원입니다. 그러나 우리는 그 중요한 3 %의 기회를 포기해서는 안됩니다. 훌륭한 프로그래머는 그렇지 않습니다. 그러한 추론에 의해 만족감에 빠져들게된다면, 그는 중요한 코드를주의 깊게 살펴 보는 것이 현명 할 것이다. 그러나 그 코드가 식별 된 후에야 "
Wes

2
아무도 a + b가 느리다고 말하지 않습니다. a = a + b를 두 번 이상 수행하면 이차적입니다. a + b + c는 느리지 않습니다. 각 문자열을 한 번만 통과해야하기 때문에 느리게 반복 하지 않지만 a = a + b 접근 방식으로 이전 문자열을 여러 번 다시 통과해야합니다 (루프에 있다고 가정) 어떤 종류의). 문자열은 변경 불가능하다는 것을 기억하십시오.
Wes

52

많은 값을 연결하는 경우 둘 다 아닙니다. 리스트를 추가하는 것은 비싸다. 이를 위해 StringIO를 사용할 수 있습니다. 특히 많은 작업을 통해 빌드하는 경우.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

buf.write('foo')
buf.write('foo')
buf.write('foo')

buf.getvalue()
# 'foofoofoo'

다른 작업에서 이미 전체 목록이 반환 된 경우 ''.join(aList)

파이썬 FAQ에서 : 많은 문자열을 함께 연결하는 가장 효율적인 방법은 무엇입니까?

str 및 bytes 객체는 변경할 수 없으므로 많은 문자열을 함께 연결하면 각 연결이 새 객체를 생성하므로 비효율적입니다. 일반적인 경우 총 런타임 비용은 총 문자열 길이에서 2 차입니다.

많은 str 객체를 누적하기 위해 권장되는 관용구는 목록에 배치하고 끝에 str.join ()을 호출하는 것입니다.

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(또 다른 합리적으로 효율적인 관용구는 io.StringIO를 사용하는 것입니다)

많은 바이트 객체를 누적하기 위해 권장되는 관용구는 내부 연결 (+ = 연산자)을 사용하여 바이트 배열 객체를 확장하는 것입니다.

result = bytearray()
for b in my_bytes_objects:
    result += b

편집 : 바보 같았고 결과를 뒤로 붙여서 목록에 추가하는 것이 cStringIO보다 빠릅니다. 또한 bytearray / str concat에 대한 테스트와 더 큰 문자열을 가진 더 큰 목록을 사용하는 두 번째 테스트를 추가했습니다. (파이썬 2.7.3)

큰 문자열 목록에 대한 ipython 테스트 예제

try:
    from cStringIO import StringIO
except:
    from io import StringIO

source = ['foo']*1000

%%timeit buf = StringIO()
for i in source:
    buf.write(i)
final = buf.getvalue()
# 1000 loops, best of 3: 1.27 ms per loop

%%timeit out = []
for i in source:
    out.append(i)
final = ''.join(out)
# 1000 loops, best of 3: 9.89 ms per loop

%%timeit out = bytearray()
for i in source:
    out += i
# 10000 loops, best of 3: 98.5 µs per loop

%%timeit out = ""
for i in source:
    out += i
# 10000 loops, best of 3: 161 µs per loop

## Repeat the tests with a larger list, containing
## strings that are bigger than the small string caching 
## done by the Python
source = ['foo']*1000

# cStringIO
# 10 loops, best of 3: 19.2 ms per loop

# list append and join
# 100 loops, best of 3: 144 ms per loop

# bytearray() +=
# 100 loops, best of 3: 3.8 ms per loop

# str() +=
# 100 loops, best of 3: 5.11 ms per loop

2
cStringIOPy3에는 존재하지 않습니다. io.StringIO대신 사용하십시오 .
lvc


36

Python> = 3.6에서 새로운 f- 문자열문자열 을 연결하는 효율적인 방법입니다.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'

8

권장되는 방법은 여전히 ​​추가 및 결합을 사용하는 것입니다.


1
내 대답에서 알 수 있듯이 이것은 연결하는 문자열 수에 따라 다릅니다. 나는 이것에 대해 몇 가지 타이밍을 썼고 (내 답변에 대한 내 의견에 링크 된 이야기를 참조하십시오) 일반적으로 10 이상이 아닌 한 +를 사용하십시오.
Lennart Regebro

1
PEP8은 이것을 언급합니다 ( python.org/dev/peps/pep-0008/#programming-recommendations ). 합리적인 이유는 CPython에 + =를 사용한 문자열 연결에 대한 특수 최적화가 있지만 다른 구현에서는 그렇지 않을 수 있다는 것입니다.
Quantum7

8

연결하는 문자열이 리터럴 인 경우 문자열 리터럴 연결을 사용하십시오.

re.compile(
        "[A-Za-z_]"       # letter or underscore
        "[A-Za-z0-9_]*"   # letter, digit or underscore
    )

이는 문자열의 일부 (위와 같이)에 주석을 달거나 리터럴의 일부에 원시 문자열 또는 삼중 따옴표 를 사용하려는 경우에 유용 하지만 모두는 아닙니다.

이것은 구문 계층에서 발생하므로 제로 연결 연산자를 사용합니다.


7

이 함수를 작성

def str_join(*args):
    return ''.join(map(str, args))

그런 다음 원하는 곳 어디에서나 간단하게 전화 할 수 있습니다.

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3

1
str_join = lambda *str_list: ''.join(s for s in str_list)
Rick은 Monica

7

'+'로 문자열 연결을 사용하는 것은 모든 값을 지원하지 않으므로 안정성과 교차 구현 측면에서 연결의 가장 최악의 방법입니다. PEP8 표준 은이를 권장하지 않으며 장기간 사용하기 위해 format (), join () 및 append ()를 사용하도록 권장합니다.

링크 된 "프로그래밍 권장 사항"섹션에서 인용 한대로 :

예를 들어, a + = b 또는 a = a + b 형식의 명령문에 대해 CPython의 내부 문자열 연결의 효율적인 구현에 의존하지 마십시오. 이 최적화는 CPython에서도 취약하며 (일부 유형에서만 작동) 재 계산을 사용하지 않는 구현에는 전혀 존재하지 않습니다. 라이브러리의 성능에 민감한 부분에서는 ''.join () 형식을 대신 사용해야합니다. 이렇게하면 다양한 구현에서 선형 시간으로 연결이 발생합니다.


5
참조 링크는 좋았을 것입니다 :)

6

다소 년 동안, 같은 코드는 Pythonista은 : 개성있는 파이썬은 권장 join()이상 + 이 절에서 . 으로 수행 PythonSpeedPerformanceTips을 에 그 부분에서 문자열 연결 다음 면책 조항과 함께 :

이 섹션의 정확성은 이후 버전의 Python과 관련하여 논쟁의 여지가 있습니다. CPython 2.5에서는 문자열 연결이 상당히 빠르지 만 다른 Python 구현에는 적용되지 않을 수 있습니다. 토론은 ConcatenationTestCode를 참조하십시오.


6

@jdi가 언급했듯이 파이썬 문서는 문자열 연결 을 사용 str.join하거나 제안합니다 io.StringIO. 그리고 개발자는 +=Python 2.4 이후 최적화가 있었지만 루프 에서 2 차 시간을 기대해야한다고 말합니다 . 로 대답은 말한다 :

파이썬은 왼쪽 인수에 다른 참조가 없다는 것을 감지 realloc하면 문자열의 크기를 조정하여 복사를 피하려고 시도합니다. 이것은 구현 세부 사항이므로 realloc문자열을 자주 이동해야 할 경우 성능이 O (n ^ 2)로 저하 되기 때문에 의존 해야하는 것은 아닙니다 .

+=이 최적화 에 순전히 의존하는 실제 코드의 예를 보여 주지만 적용되지 않았습니다. 아래 코드는 반복 가능한 짧은 문자열을 더 큰 청크로 변환하여 대량 API에 사용합니다.

def test_concat_chunk(seq, split_by):
    result = ['']
    for item in seq:
        if len(result[-1]) + len(item) > split_by: 
            result.append('')
        result[-1] += item
    return result

이 코드는 2 차 시간 복잡성으로 인해 몇 시간 동안 문학적 실행이 가능합니다. 다음은 제안 된 데이터 구조의 대안입니다.

import io

def test_stringio_chunk(seq, split_by):
    def chunk():
        buf = io.StringIO()
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                size += buf.write(item)
            else:
                yield buf.getvalue()
                buf = io.StringIO()
                size = buf.write(item)
        if size:
            yield buf.getvalue()

    return list(chunk())

def test_join_chunk(seq, split_by):
    def chunk():
        buf = []
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                buf.append(item)
                size += len(item)
            else:
                yield ''.join(buf)                
                buf.clear()
                buf.append(item)
                size = len(item)
        if size:
            yield ''.join(buf)

    return list(chunk())

그리고 마이크로 벤치 마크 :

import timeit
import random
import string
import matplotlib.pyplot as plt

line = ''.join(random.choices(
    string.ascii_uppercase + string.digits, k=512)) + '\n'
x = []
y_concat = []
y_stringio = []
y_join = []
n = 5
for i in range(1, 11):
    x.append(i)
    seq = [line] * (20 * 2 ** 20 // len(line))
    chunk_size = i * 2 ** 20
    y_concat.append(
        timeit.timeit(lambda: test_concat_chunk(seq, chunk_size), number=n) / n)
    y_stringio.append(
        timeit.timeit(lambda: test_stringio_chunk(seq, chunk_size), number=n) / n)
    y_join.append(
        timeit.timeit(lambda: test_join_chunk(seq, chunk_size), number=n) / n)
plt.plot(x, y_concat)
plt.plot(x, y_stringio)
plt.plot(x, y_join)
plt.legend(['concat', 'stringio', 'join'], loc='upper left')
plt.show()

마이크로 벤치 마크


5

다른 방법으로 할 수 있습니다.

str1 = "Hello"
str2 = "World"
str_list = ['Hello', 'World']
str_dict = {'str1': 'Hello', 'str2': 'World'}

# Concatenating With the + Operator
print(str1 + ' ' + str2)  # Hello World

# String Formatting with the % Operator
print("%s %s" % (str1, str2))  # Hello World

# String Formatting with the { } Operators with str.format()
print("{}{}".format(str1, str2))  # Hello World
print("{0}{1}".format(str1, str2))  # Hello World
print("{str1} {str2}".format(str1=str_dict['str1'], str2=str_dict['str2']))  # Hello World
print("{str1} {str2}".format(**str_dict))  # Hello World

# Going From a List to a String in Python With .join()
print(' '.join(str_list))  # Hello World

# Python f'strings --> 3.6 onwards
print(f"{str1} {str2}")  # Hello World

다음 기사를 통해이 작은 요약을 만들었습니다.


3

내 사용 사례는 약간 다릅니다. 20 개 이상의 필드가 동적 인 쿼리를 구성해야했습니다. 나는 형식 방법을 사용하는이 접근법을 따랐다.

query = "insert into {0}({1},{2},{3}) values({4}, {5}, {6})"
query.format('users','name','age','dna','suzan',1010,'nda')

이것은 + 또는 다른 방법을 사용하는 대신 비교적 간단했습니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.