Python'da bir dizeyi birleştirmenin tercih edilen yolu hangisidir?


359

Python's stringdeğiştirilemediğinden, bir dizeyi nasıl daha verimli bir şekilde birleştireceğinizi merak ediyordum?

Ben şöyle yazabilirim:

s += stringfromelsewhere

ya da bunun gibi:

s = []
s.append(somestring)

later

s = ''.join(s)

Bu soruyu yazarken konuyla ilgili iyi bir makale buldum.

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

Ama Python 2.x'te, bu yüzden soru Python 3'te bir şey değişti mi?


Yanıtlar:


435

İyi bir dize değişkeni bir dize ekleyerek yolu kullanımı için +ya +=. Bunun nedeni, okunabilir ve hızlı olmasıdır. Aynı zamanda hızlıdırlar, seçtiğiniz bir tat meselesidir, ikincisi en yaygın olanıdır. timeitModül ile zamanlamalar şunlardır :

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

Ancak, listelere sahip olmayı ve bu listelere eklemeyi ve sonra bu listelere katılmayı önerenler bunu yapar, çünkü bir dizeye bir dize eklemek bir dizeyi genişletmeye kıyasla muhtemelen çok hızlıdır. Ve bu bazı durumlarda doğru olabilir. Örneğin, bir karakter dizesinin önce bir dizeye, sonra bir listeye bir milyon eklenmesi:

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

Tamam, ortaya çıkan dize bir milyon karakter uzunluğunda bile, eklemenin hala daha hızlı olduğu ortaya çıkıyor.

Şimdi bin karakter uzunluğunda bir dizeyi yüz bin kez eklemeyi deneyelim:

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

Bu nedenle, son dizgi yaklaşık 100 MB uzunluğunda olur. Bu oldukça yavaştı, bir listeye eklemek çok daha hızlıydı. Bu zamanlamanın finali içermemesi a.join(). Peki bu ne kadar sürecek?

a.join(a):
0.43739795684814453

Oups. Bu durumda bile, ekleme / katılma daha yavaştır.

Peki bu öneri nereden geliyor? Python 2?

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

Son derece uzun dizeler kullanıyorsanız , ekleme / katılma biraz daha hızlıdır (genellikle değil, bellekte 100MB olan bir dizeye ne sahip olursunuz?)

Ancak gerçek kattığı Python 2.3. Zamanlamaları bile göstermeyeceğim yerde, çünkü henüz bitmediği için çok yavaş. Bu testler aniden dakikalar alır . Sonraki Python'lar kadar hızlı olan ekleme / katılma hariç.

Evet. İp çağında Python'da taş devri çok yavaştı. Ancak 2.4'te artık değil (veya en azından Python 2.4.7), bu nedenle ekleme / katılma kullanma önerisi Python 2.3'ün güncellenmesini bıraktığı 2008'de modası geçmiş ve onu kullanmayı bırakmış olmalısınız. :-)

(Güncelleme: Testi Python 2.3'te iki dizede kullanmanın +ve +=daha hızlı olduğunu daha dikkatli yaptığımda ortaya çıkıyor . Kullanım önerisi ''.join()bir yanlış anlama olmalıdır)

Ancak, bu CPython. Diğer uygulamaların başka endişeleri de olabilir. Ve bu, erken optimizasyonun tüm kötülüğün kökü olmasının bir başka nedeni. İlk ölçmediğiniz sürece "daha hızlı" olduğu varsayılan bir tekniği kullanmayın.

Bu nedenle dize birleştirme yapmak için "en iyi" sürümü + veya + = kullanmaktır . Ve eğer bu sizin için yavaş görünüyorsa, bu pek olası değildir, o zaman başka bir şey yapın.

Peki neden koduma çok fazla ekleme / katılma kullanıyorum? Çünkü bazen aslında daha net. Özellikle bir araya getirmeniz gereken her şey boşluk, virgül veya satırsonu ile ayrılmalıdır.


10
Birden fazla dizeniz varsa (n> 10) "" .join (list_of_strings) hala daha hızlıdır
Mikko

11
+ = hızlı olmasının nedeni, refcount 1 ise cpython'da bir performans hackinin olması - diğer tüm python uygulamalarında (oldukça özel yapılandırılmış bir pypy yapısı hariç) dağılıyor
Ronny

17
Bu neden bu kadar çok beğeniliyor? Sadece belirli bir uygulamada etkili olan ve kuadratik bir zaman algoritmasını düzeltmek için kırılgan bir saldırıya neden olan bir algoritma kullanmak nasıl daha iyidir? Ayrıca "erken optimizasyon tüm kötülüklerin kökü" noktasını tamamen yanlış anlıyorsunuz. Bu teklif KÜÇÜK optimizasyonlardan bahsediyor. Bu O (n ^ 2) 'den küçük bir optimizasyon DEĞİL O (n)' ye gidiyor.
Wes

12
İşte asıl alıntı: "Zamanın yaklaşık% 97'sini küçük verimlilikleri unutmalıyız: erken optimizasyon tüm kötülüklerin köküdür. Yine de bu kritik% 3'teki fırsatlarımızı kaçırmamalıyız. İyi bir programcı böyle bir akıl yürütme ile gönül rahatlığına kapılmak, eleştirel koda dikkatle bakmak akıllıca olacaktır; ancak bu kod tanımlandıktan sonra "
Wes

2
Kimse a + b'nin yavaş olduğunu söylemiyor. A = a + b'yi birden fazla kez yaptığınızda ikinci dereceden. a + b + c yavaş değil, her dizeyi yalnızca bir kez geçmek zorunda olduğu için yavaş değil tekrar ediyorum , oysa a = a + b yaklaşımı ile önceki dizeleri defalarca tekrarlamak zorunda (bir döngüde olduğu varsayılarak) Bir çeşit). Dizelerin değişmez olduğunu unutmayın.
Wes

52

Çok fazla değeri birleştiriyorsanız, ikisi de birleştirmez. Liste eklemek pahalıdır. Bunun için StringIO'yu kullanabilirsiniz. Özellikle de bir çok operasyon üzerine inşa ediyorsanız.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

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

buf.getvalue()
# 'foofoofoo'

Başka bir işlemden size tam bir liste döndürdüyseniz, ''.join(aList)

Python SSS'den: Birçok dizeyi bir araya getirmenin en etkili yolu nedir?

str ve bayt nesneleri değiştirilemez, bu nedenle birçok dizeyi birlikte birleştirmek her birleştirme yeni bir nesne oluşturduğundan verimsizdir. Genel durumda, toplam çalışma zamanı maliyeti toplam dize uzunluğunda ikinci derecedir.

Birçok str nesnesini biriktirmek için önerilen deyim onları bir listeye yerleştirmek ve sonunda str.join () öğesini çağırmaktır:

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

(bir diğer makul verimli deyim io.StringIO kullanmaktır)

Birçok bayt nesnesi biriktirmek için önerilen deyim, yerinde birleştirme (+ = operatörü) kullanarak bir baytrayray nesnesini genişletmektir:

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

Düzenleme: Aptalca ve sonuçları geriye yapıştırılan vardı, bir listeye eklemek cStringIO daha hızlı gibi görünmesini sağladı. Ayrıca bytearray / str concat için testler ve daha büyük dizelerle daha büyük bir liste kullanarak ikinci bir test turu ekledim. (python 2.7.3)

büyük dize listeleri için ipython test örneğ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
cStringIOPy3'te mevcut değil. io.StringIOBunun yerine kullanın .
lvc

2
Bir dizeye tekrar tekrar eklemenin
Wes

36

Python> = 3.6'da, yeni f-string bir dizeyi birleştirmenin etkili bir yoludur.

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

8

Önerilen yöntem hala ekleme ve katılmayı kullanmaktır.


1
Cevabımdan gördüğünüz gibi, bu kaç tane dizeyi birleştirdiğinize bağlıdır. Bu konuda bazı zamanlamalar yaptım (cevabım hakkındaki yorumlarımda bağlandığım konuşmaya bakın) ve genellikle ondan fazla olmadığı sürece + kullanın.
Lennart Regebro

1
PEP8 bundan bahseder ( python.org/dev/peps/pep-0008/#programming-recommendations ). Rasyonel, CPython'un + = ile dize birleştirme için özel optimizasyonlara sahip olmasına rağmen, diğer uygulamaların olmayabilir.
Quantum7

8

Birleştirdiğiniz dizeler değişmezse, Dize değişmez dizesini kullanın

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

Bu, bir dizenin bir kısmı hakkında yorum yapmak istiyorsanız (yukarıdaki gibi) veya bir değişmez kelimenin bir kısmı için ham dizeler veya üçlü tırnaklar kullanmak istiyorsanız, ancak hepsi değil.

Bu sözdizimi katmanında olduğu için sıfır birleştirme işleçleri kullanır.


7

Bu işlevi yazıyorsunuz

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

O zaman istediğiniz yeri arayabilirsiniz

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

'+' İle yerinde dize birleştirmeyi kullanmak, tüm değerleri desteklemediğinden, kararlılık ve çapraz uygulama açısından EN BÜYÜK birleştirme yöntemidir. PEP8 standardı bunu reddeder ve uzun süreli kullanım için format (), join () ve append () kullanımını teşvik eder.

Bağlantılı "Programlama Önerileri" bölümünde belirtildiği gibi:

Örneğin, CPython'un a + = b veya a = a + b biçimindeki ifadeler için yerinde dize birleştirmesinin etkili bir şekilde uygulanmasına güvenmeyin. Bu optimizasyon CPython'da bile kırılgandır (yalnızca bazı türler için çalışır) ve yeniden sayım kullanmayan uygulamalarda hiç mevcut değildir. Kütüphanenin performansa duyarlı bölümlerinde, bunun yerine '' .join () formu kullanılmalıdır. Bu, birleştirme işleminin çeşitli uygulamalarda doğrusal zamanda gerçekleşmesini sağlayacaktır.


6
Referans bağlantısı iyi olurdu :)


6

@Jdi'nin belirttiği gibi Python belgeleri dize birleştirmesi için str.joinveya kullanmanızı önerir io.StringIO. Ve +=Python 2.4'ten bu yana bir optimizasyon olmasına rağmen, bir geliştiricinin döngü içinde zaman beklemesi gerektiğini söylüyor . Gibi bu cevabı diyor ki:

Python, sol argümanın başka bir referansı olmadığını algılarsa realloc, dizeyi yerinde yeniden boyutlandırarak bir kopyadan kaçınmaya çalışır. Bu, hiç güvenmemeniz gereken bir şey değildir, çünkü bu bir uygulama ayrıntısıdır ve reallocdizeyi sık sık taşımak zorunda kalırsa , performans yine de O (n ^ 2) değerine düşer.

+=Bu optimizasyona saf bir şekilde dayanan gerçek dünya kodunun bir örneğini göstereceğim , ancak geçerli değildi. Aşağıdaki kod, bir dizi kısa dizeyi bir toplu API'da kullanılacak daha büyük parçalara dönüştürür.

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

Bu kod, ikinci dereceden zaman karmaşıklığı nedeniyle saatlerce çalışabilir. Aşağıda önerilen veri yapılarına sahip alternatifler bulunmaktadır:

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

Ve bir mikro kriter:

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

Mikro kriter


5

Farklı şekillerde yapabilirsiniz.

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

Bu küçük özeti aşağıdaki makaleleri kullanarak oluşturdum.


3

benim kullanım durumum biraz farklıydı. 20'den fazla alanın dinamik olduğu bir sorgu oluşturmak zorunda kaldım. Biçim yöntemini kullanma yaklaşımını izledim

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

+ veya başka yollar kullanmak yerine bu benim için nispeten daha basitti


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.