วิธีใดที่ต้องการเชื่อมต่อสตริงใน Python


358

เนื่องจาก Python 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 มันค่อนข้างช้าการต่อท้ายรายการเร็วกว่ามาก a.join()ว่าระยะเวลาที่ไม่รวมสุดท้าย ดังนั้นจะใช้เวลานานเท่าไหร่?

a.join(a):
0.43739795684814453

Oups ปรากฏว่าแม้ในกรณีนี้การผนวก / เข้าร่วมจะช้าลง

ดังนั้นคำแนะนำนี้มาจากไหน? Python 2

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

ดีผนวก / เข้าร่วมเป็นเล็กน้อยเร็วขึ้นมีถ้าคุณกำลังใช้สายยาวมาก (ซึ่งคุณมักจะไม่ได้สิ่งที่คุณจะมีสตริงที่ 100MB หน่วยความจำหรือไม่?)

แต่ clincher ที่แท้จริงคือ Python 2.3 ที่ฉันจะไม่แสดงเวลาให้คุณเห็นเพราะมันช้ามากจนยังไม่เสร็จ การทดสอบเหล่านี้ใช้เวลาไม่กี่นาทีไม่กี่นาทียกเว้นภาคผนวก / การเข้าร่วมซึ่งเร็วพอ ๆ กับ Pythons ในภายหลัง

ได้. การต่อสายอักขระช้ามากใน Python ย้อนกลับไปในยุคหิน แต่ใน 2.4 มันไม่ใช่อีกต่อไป (หรืออย่างน้อย Python 2.4.7) ดังนั้นคำแนะนำในการใช้ผนวก / เข้าร่วมนั้นล้าสมัยในปี 2551 เมื่อ Python 2.3 หยุดการอัปเดตและคุณควรหยุดใช้ :-)

(อัปเดต: ปรากฎเมื่อฉันทำการทดสอบอย่างระมัดระวังยิ่งขึ้นว่าการใช้+และ+=เร็วกว่าสำหรับสองสายใน Python 2.3 เช่นกันคำแนะนำในการใช้''.join()จะต้องเป็นความเข้าใจผิด)

อย่างไรก็ตามนี่คือ CPython การใช้งานอื่น ๆ อาจมีข้อกังวลอื่น ๆ และนี่ก็เป็นอีกสาเหตุหนึ่งที่ทำให้การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นสาเหตุของความชั่วร้ายทั้งหมด อย่าใช้เทคนิคที่ควรจะ "เร็วกว่า" เว้นแต่คุณจะทำการวัดครั้งแรก

ดังนั้น "ดีที่สุด" รุ่นที่จะทำสตริงคือการใช้ + หรือ + = และถ้าสิ่งนั้นกลายเป็นว่าช้าสำหรับคุณซึ่งไม่น่าจะเป็นไปได้ให้ทำอย่างอื่น

เหตุใดฉันจึงใช้ผนวก / เข้าร่วมจำนวนมากในรหัสของฉัน เพราะบางครั้งมันชัดเจนกว่า โดยเฉพาะอย่างยิ่งเมื่อใดก็ตามที่คุณควรต่อกันเข้าด้วยกันควรคั่นด้วยช่องว่างหรือเครื่องหมายจุลภาคหรือบรรทัดใหม่


10
หากคุณมีหลายสตริง (n> 10) "" .join (list_of_strings) ยังเร็วกว่านี้
Mikko Ohtamaa

11
เหตุผลที่ + = เร็วคือว่ามีแฮ็คประสิทธิภาพใน cpython ถ้า refcount เป็น 1 - มันแตกต่างจากการใช้งานไพ ธ อนอื่น ๆ ทั้งหมด (ยกเว้นการสร้าง pypy ที่กำหนดค่าไว้ค่อนข้างพิเศษ)
Ronny

17
เหตุใดจึงมีการอัปโหลดมากเกินไป จะดีกว่าที่จะใช้อัลกอริทึมที่มีประสิทธิภาพในการใช้งานเฉพาะอย่างใดอย่างหนึ่งและมีจำนวนเท่าใดในการแฮ็กที่บอบบางเพื่อแก้ไขอัลกอริธึมกำลังสอง? นอกจากนี้คุณเข้าใจผิดอย่างสิ้นเชิงจุดของ "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากฐานของความชั่วร้ายทั้งหมด" คำพูดนั้นกำลังพูดถึงการปรับขนาดเล็ก สิ่งนี้เริ่มจาก O (n ^ 2) ถึง O (n) ที่ไม่ใช่การปรับให้เหมาะสมเล็กน้อย
Wes

12
นี่คือใบเสนอราคาที่เกิดขึ้นจริง: "เราควรลืมเกี่ยวกับประสิทธิภาพเล็กน้อยพูดถึง 97% ของเวลา: การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากฐานของความชั่วร้ายทั้งหมด แต่เราไม่ควรพลาดโอกาสสำคัญ 3% โปรแกรมเมอร์ที่ดีจะไม่ ถูกกล่อมให้พึงพอใจด้วยเหตุผลเช่นนี้เขาจะฉลาดในการดูอย่างรอบคอบถึงรหัสวิกฤติ แต่หลังจากรหัสนั้นได้รับการระบุ "
เวสสตรีท

2
ไม่มีใครพูดว่า + b ช้า มันเป็นกำลังสองเมื่อคุณทำ = 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)

จากคำถามที่พบบ่อยหลาม: วิธีที่มีประสิทธิภาพมากที่สุดในการเชื่อมโยงหลายสายเข้าด้วยกันคืออะไร?

วัตถุ str และไบต์ไม่เปลี่ยนรูปดังนั้นการเชื่อมสตริงจำนวนมากเข้าด้วยกันจึงไม่มีประสิทธิภาพเนื่องจากการต่อข้อมูลแต่ละครั้งจะสร้างวัตถุใหม่ ในกรณีทั่วไปค่าใช้จ่ายรันไทม์ทั้งหมดจะเป็นกำลังสองในความยาวสตริงทั้งหมด

ในการสะสมวัตถุหลายชิ้นสำนวนที่แนะนำคือการวางไว้ในรายการและเรียก str.join () ที่ท้าย:

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

(สำนวนอื่นที่มีประสิทธิภาพพอสมควรคือการใช้ io.StringIO)

ในการสะสมวัตถุจำนวนมากไบต์สำนวนที่แนะนำคือการขยายวัตถุ bytearray โดยใช้การต่อข้อมูลแบบแทนที่ (ตัวดำเนินการ + =):

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

แก้ไข: ฉันงี่เง่าและวางผลลัพธ์ไว้ข้างหลังทำให้ดูเหมือนว่าการต่อท้ายรายการเร็วกว่า cStringIO ฉันได้เพิ่มการทดสอบสำหรับ bytearray / str concat รวมถึงรอบที่สองของการทดสอบโดยใช้รายการที่มีขนาดใหญ่ขึ้นพร้อมกับสตริงที่ใหญ่ขึ้น (python 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
cStringIOไม่มีอยู่ใน Py3 ใช้io.StringIOแทน
lvc

2
สำหรับเหตุผลที่ผนวกกับสตริงซ้ำ ๆ อาจมีราคาแพง: joelonsoftware.com/articles/fog0000000319.html
เวสสตรีท


8

วิธีที่แนะนำยังคงใช้ผนวกและเข้าร่วม


1
ตามที่คุณเห็นจากคำตอบของฉันสิ่งนี้ขึ้นอยู่กับจำนวนสตริงที่คุณต่อกัน ฉันได้กำหนดเวลานี้แล้ว (ดูการพูดคุยที่เชื่อมโยงกับความเห็นของฉันในคำตอบของฉัน) และโดยทั่วไปแล้วหากไม่มีมากกว่าสิบให้ใช้ +
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 สนับสนุนโมนิก้า

7

การใช้การเรียงสตริงแบบแทนที่ด้วย '+' เป็นวิธีการเรียงต่อกันที่แย่ที่สุดในแง่ของความมั่นคงและการปรับใช้ข้ามซึ่งไม่สนับสนุนค่าทั้งหมด มาตรฐาน PEP8ไม่สนับสนุนสิ่งนี้และสนับสนุนให้ใช้รูปแบบ (), เข้าร่วม () และผนวก () เพื่อการใช้งานในระยะยาว

ตามที่ยกมาจากส่วน "คำแนะนำการเขียนโปรแกรม" ที่เชื่อมโยง:

ตัวอย่างเช่นอย่าพึ่งพาการใช้การรวมสตริงในสถานที่ของ CPython อย่างมีประสิทธิภาพสำหรับข้อความสั่งในรูปแบบ + = b หรือ a = a + b การเพิ่มประสิทธิภาพนี้มีความเปราะบางแม้จะอยู่ใน CPython (ใช้ได้กับบางประเภทเท่านั้น) และจะไม่ปรากฏเลยในการใช้งานที่ไม่ได้ใช้การนับใหม่ ในส่วนที่อ่อนไหวด้านประสิทธิภาพของไลบรารีควรใช้ฟอร์ม '' .join () แทน สิ่งนี้จะทำให้มั่นใจได้ว่าการต่อข้อมูลจะเกิดขึ้นในเวลาเชิงเส้นในการนำไปใช้งานต่าง ๆ


5
ลิงค์อ้างอิงน่าจะดี :)

6

ในขณะที่ค่อนข้างเก่ารหัสเช่น Pythonista: Idiomatic Pythonแนะนำjoin()มากกว่า+ ในส่วนนี้ PythonSpeedPerformanceTipsเช่นเดียวกับในส่วนของการเชื่อมต่อสายอักขระที่มีข้อจำกัดความรับผิดชอบต่อไปนี้:

ความถูกต้องของส่วนนี้มีการโต้แย้งเกี่ยวกับ Python รุ่นที่ใหม่กว่า ใน CPython 2.5 การต่อข้อมูลสตริงนั้นค่อนข้างเร็วแม้ว่าสิ่งนี้อาจไม่ได้นำไปใช้กับการประยุกต์ใช้ Python อื่นเช่นกัน ดู ConcatenationTestCode สำหรับการสนทนา


6

ในฐานะที่เป็น @jdi กล่าวถึงเอกสาร Python แนะนำให้ใช้str.joinหรือio.StringIOสำหรับการต่อสตริง และบอกว่านักพัฒนาควรคาดหวังว่าจะได้เวลากำลังสองจาก+=ในวงแม้ว่าจะมีการเพิ่มประสิทธิภาพตั้งแต่ Python 2.4 ตามที่คำตอบนี้บอกว่า:

หาก Python ตรวจพบว่าอาร์กิวเมนต์ซ้ายไม่มีการอ้างอิงอื่นมันจะเรียก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

รหัสนี้สามารถทำงานวรรณกรรมเป็นเวลาหลายชั่วโมงเนื่องจากความซับซ้อนของเวลากำลังสอง ด้านล่างเป็นทางเลือกที่มีโครงสร้างข้อมูลที่แนะนำ:

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.