Cách ưa thích nào để nối chuỗi trong Python?


358

Vì Python stringkhông thể thay đổi, tôi đã tự hỏi làm thế nào để nối chuỗi hiệu quả hơn?

Tôi có thể viết như thế:

s += stringfromelsewhere

hoặc như thế này:

s = []
s.append(somestring)

later

s = ''.join(s)

Trong khi viết câu hỏi này, tôi tìm thấy một bài viết hay nói về chủ đề này.

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

Nhưng đó là trong Python 2.x., vậy câu hỏi sẽ là có gì đó thay đổi trong Python 3?


Câu trả lời:


433

Cách tốt nhất để nối một chuỗi vào biến chuỗi là sử dụng +hoặc +=. Điều này là do nó dễ đọc và nhanh chóng. Chúng cũng nhanh như vậy, cái nào bạn chọn là vấn đề của hương vị, cái sau là phổ biến nhất. Dưới đây là thời gian với các timeitmô-đun:

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

Tuy nhiên, những người khuyên nên có danh sách và nối thêm vào danh sách đó và sau đó tham gia vào danh sách đó, hãy làm như vậy bởi vì việc thêm chuỗi vào danh sách có lẽ rất nhanh so với việc mở rộng chuỗi. Và điều này có thể đúng, trong một số trường hợp. Ví dụ, ở đây là một triệu phụ lục của chuỗi một ký tự, đầu tiên là chuỗi, sau đó vào danh sách:

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

OK, hóa ra ngay cả khi chuỗi kết quả dài một triệu ký tự, việc nối thêm vẫn nhanh hơn.

Bây giờ, hãy thử nối thêm một chuỗi ký tự dài một trăm nghìn lần:

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

Chuỗi kết thúc, do đó, kết thúc dài khoảng 100 MB. Điều đó khá chậm, việc thêm vào một danh sách nhanh hơn nhiều. Rằng thời gian đó không bao gồm trận chung kết a.join(). Vì vậy, sẽ mất bao lâu?

a.join(a):
0.43739795684814453

Oup. Hóa ra ngay cả trong trường hợp này, nối / nối chậm hơn.

Vậy khuyến nghị này đến từ đâu? Con trăn 2?

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

Chà, nối / nối sẽ nhanh hơn một chút nếu bạn đang sử dụng các chuỗi cực dài (mà bạn thường không biết, bạn sẽ có chuỗi nào trong bộ nhớ 100MB?)

Nhưng móc sắt thực sự là Python 2.3. Nơi tôi thậm chí sẽ không chỉ cho bạn thời gian, vì nó quá chậm mà chưa hoàn thành. Những bài kiểm tra đột nhiên mất vài phút . Ngoại trừ việc nối thêm / tham gia, cũng nhanh như dưới thời Pythons sau này.

Vâng Chuỗi kết nối rất chậm trong Python trở lại thời kỳ đồ đá. Nhưng vào ngày 2.4, nó không còn nữa (hoặc ít nhất là Python 2.4.7), do đó, khuyến nghị sử dụng nối / nối đã trở nên lỗi thời vào năm 2008, khi Python 2.3 ngừng cập nhật và bạn nên ngừng sử dụng nó. :-)

(Cập nhật: Hóa ra khi tôi thực hiện kiểm tra cẩn thận hơn bằng cách sử dụng ++=nhanh hơn cho hai chuỗi trên Python 2.3. Đề xuất sử dụng ''.join()phải là một sự hiểu lầm)

Tuy nhiên, đây là CPython. Thực hiện khác có thể có mối quan tâm khác. Và đây chỉ là một lý do khác tại sao tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Đừng sử dụng một kỹ thuật được cho là "nhanh hơn" trừ khi bạn lần đầu tiên đo nó.

Do đó, phiên bản "tốt nhất" để thực hiện nối chuỗi là sử dụng + hoặc + = . Và nếu điều đó trở nên chậm chạp đối với bạn, điều này khá khó xảy ra, thì hãy làm điều gì đó khác.

Vậy tại sao tôi sử dụng nhiều phụ lục / tham gia vào mã của mình? Bởi vì đôi khi nó thực sự rõ ràng hơn. Đặc biệt là khi bất cứ điều gì bạn nên ghép lại với nhau nên được phân tách bằng dấu cách hoặc dấu phẩy hoặc dòng mới.


10
Nếu bạn có nhiều chuỗi (n> 10) "" .join (list_of_strings) vẫn nhanh hơn
Mikko Ohtamaa

11
Lý do tại sao + = nhanh là vì có một vụ hack hiệu suất trong cpython nếu số lần truy cập là 1 - nó bị phá vỡ trên hầu hết các triển khai python khác (ngoại trừ một bản dựng pypy được cấu hình khá đặc biệt)
Ronny

17
Tại sao điều này được nâng cao rất nhiều? Làm thế nào tốt hơn là sử dụng một thuật toán chỉ hiệu quả trên một triển khai cụ thể và có những gì cơ bản là một bản hack dễ vỡ để sửa một thuật toán thời gian bậc hai? Ngoài ra, bạn hoàn toàn hiểu sai quan điểm "tối ưu hóa sớm là gốc rễ của mọi tội lỗi". Câu nói đó đang nói về tối ưu hóa NHỎ. Điều này sẽ đi từ O (n ^ 2) đến O (n) KHÔNG phải là một tối ưu hóa nhỏ.
Wes

12
Đây là trích dẫn thực tế: "Chúng ta nên quên đi những hiệu quả nhỏ, nói về 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Tuy nhiên, chúng ta không nên bỏ qua cơ hội của mình trong 3% quan trọng đó. Một lập trình viên giỏi sẽ không bị ru ngủ trong sự tự mãn bởi lý do như vậy, anh ta sẽ khôn ngoan xem xét kỹ mã quan trọng, nhưng chỉ sau khi mã đó được xác định "
Wes

2
Không ai nói rằng a + b chậm. Đó là bậc hai khi bạn đang thực hiện a = a + b nhiều lần. a + b + c không chậm, tôi lặp lại không chậm vì nó chỉ phải đi qua từng chuỗi một lần, trong khi nó phải duyệt lại các chuỗi trước đó nhiều lần với cách tiếp cận a = a + b (giả sử rằng đó là trong một vòng lặp của một số loại). Hãy nhớ chuỗi là bất biến.
Wes

52

Nếu bạn đang nối nhiều giá trị, thì không. Áp dụng một danh sách là tốn kém. Bạn có thể sử dụng StringIO cho điều đó. Đặc biệt là nếu bạn đang xây dựng nó qua rất nhiều hoạt động.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

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

buf.getvalue()
# 'foofoofoo'

Nếu bạn đã có một danh sách đầy đủ được trả về cho bạn từ một số thao tác khác, thì chỉ cần sử dụng ''.join(aList)

Từ câu hỏi thường gặp về python: Cách hiệu quả nhất để nối nhiều chuỗi với nhau là gì?

Các đối tượng str và byte là bất biến, do đó việc nối nhiều chuỗi với nhau là không hiệu quả vì mỗi phép nối tạo ra một đối tượng mới. Trong trường hợp chung, tổng chi phí thời gian chạy là bậc hai trong tổng chiều dài chuỗi.

Để tích lũy nhiều đối tượng str, thành ngữ được đề xuất là đặt chúng vào danh sách và gọi str.join () ở cuối:

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

(một thành ngữ hợp lý hiệu quả khác là sử dụng io.StringIO)

Để tích lũy nhiều đối tượng byte, thành ngữ được đề xuất là mở rộng một đối tượng bytearray bằng cách sử dụng phép nối tại chỗ (toán tử + =):

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

Chỉnh sửa: Tôi đã ngớ ngẩn và có kết quả được dán ngược, làm cho nó giống như việc thêm vào danh sách nhanh hơn cStringIO. Tôi cũng đã thêm các bài kiểm tra cho bytearray / str concat, cũng như vòng kiểm tra thứ hai bằng cách sử dụng danh sách lớn hơn với chuỗi lớn hơn. (trăn 2.7.3)

ví dụ kiểm tra ipython cho danh sách lớn các chuỗi

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
cStringIOkhông tồn tại trong Py3. Sử dụng io.StringIOthay thế.
lvc

2
Về lý do tại sao việc nối lại chuỗi liên tục có thể tốn kém: joelonsoftware.com/articles/fog0000000319.html
Wes

36

Trong Python> = 3.6, chuỗi f mới là một cách hiệu quả để nối chuỗi.

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

8

Phương pháp được đề xuất vẫn là sử dụng nối và nối.


1
Như bạn thấy từ câu trả lời của tôi, điều này phụ thuộc vào số lượng chuỗi bạn đang nối. Tôi đã thực hiện một số thời gian về điều này (xem bài nói chuyện mà tôi đã liên kết trong các nhận xét về câu trả lời của mình) và nói chung trừ khi nó hơn mười, hãy sử dụng +.
Lennart Regebro

1
PEP8 đề cập đến điều này ( python.org/dev/peps/pep-0008/#programming-recommendations ). Lý do là trong khi CPython có tối ưu hóa đặc biệt cho nối chuỗi với + =, thì các triển khai khác có thể không.
Quantum7

8

Nếu các chuỗi bạn đang nối là chữ, hãy sử dụng nối chuỗi theo chuỗi

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

Điều này hữu ích nếu bạn muốn nhận xét về một phần của chuỗi (như trên) hoặc nếu bạn muốn sử dụng chuỗi thô hoặc dấu ngoặc kép cho một phần của nghĩa đen nhưng không phải tất cả.

Vì điều này xảy ra ở lớp cú pháp, nó sử dụng các toán tử nối không.


7

Bạn viết hàm này

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

Sau đó, bạn có thể gọi đơn giản bất cứ nơi nào bạn muốn

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 hỗ trợ Monica

7

Sử dụng nối chuỗi tại chỗ bằng '+' là phương pháp nối chuỗi TUYỆT VỜI về tính ổn định và triển khai chéo vì nó không hỗ trợ tất cả các giá trị. Tiêu chuẩn PEP8 không khuyến khích điều này và khuyến khích sử dụng định dạng (), tham gia () và chắp thêm () để sử dụng lâu dài.

Như trích dẫn từ phần "Khuyến nghị lập trình" được liên kết:

Ví dụ: không dựa vào việc triển khai hiệu quả nối chuỗi tại chỗ của CPython cho các câu lệnh ở dạng a + = b hoặc a = a + b. Tối ưu hóa này rất dễ hỏng ngay cả trong CPython (nó chỉ hoạt động đối với một số loại) và hoàn toàn không có trong các triển khai không sử dụng tính năng đếm lại. Trong các phần nhạy cảm về hiệu năng của thư viện, nên sử dụng biểu mẫu '' .join (). Điều này sẽ đảm bảo rằng sự kết hợp xảy ra trong thời gian tuyến tính trên các triển khai khác nhau.


5
Liên kết tham khảo sẽ rất tuyệt :)

6

Trong khi hơi ngày, Mã Giống như một Pythonista: Idiomatic Python khuyến join()hơn + trong phần này . PythonSpeedPerformanceTips cũng vậy trong phần của nó về nối chuỗi , với tuyên bố từ chối sau:

Độ chính xác của phần này bị tranh chấp liên quan đến các phiên bản sau của Python. Trong CPython 2.5, nối chuỗi khá nhanh, mặc dù điều này có thể không áp dụng tương tự cho các triển khai Python khác. Xem ConcatenationTestCode để thảo luận.


6

Như @jdi đề cập đến tài liệu Python đề nghị sử dụng str.joinhoặc io.StringIOđể nối chuỗi. Và nói rằng một nhà phát triển nên mong đợi thời gian bậc hai từ +=một vòng lặp, mặc dù có tối ưu hóa kể từ Python 2.4. Như câu trả lời này nói:

Nếu Python phát hiện ra rằng đối số bên trái không có tham chiếu nào khác, thì nó gọi reallocđể cố gắng tránh một bản sao bằng cách thay đổi kích thước chuỗi tại chỗ. Đây không phải là thứ bạn nên dựa vào, bởi vì đó là một chi tiết triển khai và bởi vì nếu realloccuối cùng cần phải di chuyển chuỗi thường xuyên, hiệu suất sẽ giảm xuống O (n ^ 2).

Tôi sẽ đưa ra một ví dụ về mã trong thế giới thực mà ngây thơ dựa vào +=tối ưu hóa này, nhưng nó đã không được áp dụng. Mã dưới đây chuyển đổi một chuỗi lặp ngắn thành các đoạn lớn hơn được sử dụng trong API số lượng lớn.

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

Mã này có thể chạy trong nhiều giờ vì độ phức tạp của thời gian bậc hai. Dưới đây là các lựa chọn thay thế với cấu trúc dữ liệu được đề xuất:

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())

Và một điểm chuẩn vi mô:

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()

điểm chuẩn vi mô


5

Bạn có thể làm theo những cách khác nhau.

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

Tôi đã tạo ra bản tóm tắt nhỏ này thông qua các bài viết sau.


3

trường hợp sử dụng của tôi là hơi khác nhau. Tôi đã phải xây dựng một truy vấn trong đó có hơn 20 trường là động. Tôi đã làm theo phương pháp này bằng cách sử dụng phương thức định dạng

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

điều này tương đối đơn giản hơn đối với tôi thay vì sử dụng + hoặc các cách khác


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.