Quelle est la manière préférée de concaténer une chaîne en Python?

358

Puisque Python ne stringpeut pas être changé, je me demandais comment concaténer une chaîne plus efficacement?

Je peux écrire comme ça:

s += stringfromelsewhere

ou comme ça:

s = []
s.append(somestring)

later

s = ''.join(s)

En écrivant cette question, j'ai trouvé un bon article sur le sujet.

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

Mais c'est en Python 2.x., donc la question serait de savoir si quelque chose a changé en Python 3?

Max
la source

Réponses:

434

La meilleure façon d'ajouter une chaîne à une variable de chaîne est d'utiliser +ou +=. C'est parce qu'il est lisible et rapide. Ils sont également tout aussi rapides, celui que vous choisissez est une question de goût, ce dernier est le plus courant. Voici les horaires avec le timeitmodule:

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

Cependant, ceux qui recommandent d'avoir des listes et de les ajouter, puis de les rejoindre, le font parce que l'ajout d'une chaîne à une liste est probablement très rapide par rapport à l'extension d'une chaîne. Et cela peut être vrai, dans certains cas. Voici, par exemple, un million d'annexes d'une chaîne à un caractère, d'abord à une chaîne, puis à une liste:

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

OK, il s'avère que même lorsque la chaîne résultante a un million de caractères, l'ajout était encore plus rapide.

Essayons maintenant d'ajouter une chaîne longue de mille caractères cent mille fois:

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

La chaîne de fin, par conséquent, finit par faire environ 100 Mo de long. C'était assez lent, l'ajout à une liste était beaucoup plus rapide. Que ce timing n'inclut pas la finale a.join(). Alors, combien de temps cela prendrait-il?

a.join(a):
0.43739795684814453

Oups. Il s'avère que même dans ce cas, l'ajout / jointure est plus lent.

D'où vient donc cette recommandation? Python 2?

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

Eh bien, l'ajout / jointure est légèrement plus rapide là-bas si vous utilisez des chaînes extrêmement longues (ce que vous n'êtes généralement pas, qu'est-ce que vous auriez une chaîne de 100 Mo en mémoire?)

Mais le vrai point fort est Python 2.3. Où je ne vais même pas vous montrer les horaires, car c'est si lent que ce n'est pas encore fini. Ces tests prennent soudainement quelques minutes . À l'exception de l'ajout / jointure, qui est tout aussi rapide que sous Pythons ultérieurs.

Ouaip. La concaténation des cordes était très lente en Python à l'époque de la pierre. Mais sur 2.4, ce n'est plus le cas (ou du moins Python 2.4.7), donc la recommandation d'utiliser append / join est devenue obsolète en 2008, lorsque Python 2.3 a cessé d'être mis à jour, et vous auriez dû arrêter de l'utiliser. :-)

(Mise à jour: il s'avère que j'ai effectué les tests plus attentivement que l'utilisation +et +=est également plus rapide pour deux chaînes sur Python 2.3. La recommandation à utiliser ''.join()doit être un malentendu)

Cependant, c'est CPython. D'autres implémentations peuvent avoir d'autres préoccupations. Et c'est juste une autre raison pour laquelle l'optimisation prématurée est la racine de tout mal. N'utilisez pas une technique supposée "plus rapide" à moins que vous ne la mesuriez au préalable.

Par conséquent, la "meilleure" version pour effectuer la concaténation de chaînes consiste à utiliser + ou + = . Et si cela s'avère lent pour vous, ce qui est peu probable, faites autre chose.

Alors pourquoi est-ce que j'utilise beaucoup d'ajout / jointure dans mon code? Parce que parfois, c'est en fait plus clair. Surtout quand tout ce que vous devez concaténer ensemble doit être séparé par des espaces, des virgules ou des sauts de ligne.

Lennart Regebro
la source
10
Si vous avez plusieurs chaînes (n> 10) "" .join (list_of_strings) est toujours plus rapide
Mikko Ohtamaa
11
la raison pour laquelle + = est rapide est qu'il y a un hack de performance dans cpython si le refcount est 1 - il s'effondre sur à peu près toutes les autres implémentations de python (à l'exception d'une construction de pypy configurée plutôt spéciale)
Ronny
17
Pourquoi cela est-il tant voté? Comment est-il préférable d'utiliser un algorithme qui n'est efficace que sur une implémentation spécifique et qui a essentiellement ce qui revient à un hack fragile pour corriger un algorithme de temps quadratique? De plus, vous comprenez complètement le point de "l'optimisation prématurée est la racine de tout mal". Cette citation parle de petites optimisations. Cela va de O (n ^ 2) à O (n) qui n'est PAS une petite optimisation.
Wes
12
Voici la citation actuelle: "Nous devons oublier les petites efficacités, disons environ 97% du temps: l'optimisation prématurée est la racine de tout mal. Pourtant, nous ne devons pas laisser passer nos opportunités dans ces 3% critiques. Un bon programmeur ne être bercé dans la complaisance par un tel raisonnement, il sera sage de regarder attentivement le code critique, mais seulement après que ce code aura été identifié "
Wes
2
Personne ne dit que a + b est lent. C'est quadratique lorsque vous faites a = a + b plus d'une fois. a + b + c n'est pas lent, je répète pas lent car il ne doit traverser chaque chaîne qu'une seule fois, alors qu'il doit re-parcourir les chaînes précédentes plusieurs fois avec l'approche a = a + b (en supposant que c'est dans une boucle de quelques sortes). N'oubliez pas que les chaînes sont immuables.
Wes
52

Si vous concaténez beaucoup de valeurs, alors ni l'un ni l'autre. Ajouter une liste coûte cher. Vous pouvez utiliser StringIO pour cela. Surtout si vous le construisez sur de nombreuses opérations.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

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

buf.getvalue()
# 'foofoofoo'

Si une liste complète vous a déjà été renvoyée par une autre opération, utilisez simplement le ''.join(aList)

De la FAQ python: Quelle est la manière la plus efficace de concaténer plusieurs chaînes ensemble?

les objets str et bytes sont immuables, donc la concaténation de nombreuses chaînes ensemble est inefficace car chaque concaténation crée un nouvel objet. Dans le cas général, le coût total d'exécution est quadratique dans la longueur totale de la chaîne.

Pour accumuler de nombreux objets str, l'idiome recommandé est de les placer dans une liste et d'appeler str.join () à la fin:

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

(un autre idiome raisonnablement efficace consiste à utiliser io.StringIO)

Pour accumuler plusieurs objets octets, l'idiome recommandé est d'étendre un objet bytearray en utilisant la concaténation sur place (l'opérateur + =):

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

Edit: j'étais stupide et j'avais les résultats collés à l'envers, ce qui donne l'impression que l'ajout à une liste était plus rapide que cStringIO. J'ai également ajouté des tests pour bytearray / str concat, ainsi qu'une deuxième série de tests utilisant une liste plus longue avec des chaînes plus grandes. (python 2.7.3)

exemple de test ipython pour de grandes listes de chaînes

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
jdi
la source
2
cStringIOn'existe pas dans Py3. Utilisez io.StringIOplutôt.
lvc
2
Quant à savoir pourquoi ajouter à une chaîne à plusieurs reprises peut être coûteux: joelonsoftware.com/articles/fog0000000319.html
Wes
36

En Python> = 3.6, la nouvelle chaîne f est un moyen efficace de concaténer une chaîne.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'
SuperNova
la source
8

La méthode recommandée est toujours d'utiliser l'ajout et la jointure.

MRABE
la source
1
Comme vous le voyez dans ma réponse, cela dépend du nombre de chaînes que vous concaténez. J'ai fait quelques timings à ce sujet (voir le discours auquel j'ai lié dans mes commentaires sur ma réponse) et généralement, à moins de dix, utilisez +.
Lennart Regebro
1
PEP8 en fait mention ( python.org/dev/peps/pep-0008/#programming-recommendations ). Le rationnel est que même si CPython a des optimisations spéciales pour la concaténation de chaînes avec + =, d'autres implémentations peuvent ne pas.
Quantum7
8

Si les chaînes que vous concaténez sont des littéraux, utilisez la concaténation littérale des chaînes

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

Ceci est utile si vous souhaitez commenter une partie d'une chaîne (comme ci-dessus) ou si vous souhaitez utiliser des chaînes brutes ou des guillemets triples pour une partie d'un littéral mais pas pour la totalité.

Comme cela se produit au niveau de la couche de syntaxe, il utilise des opérateurs de concaténation zéro.

droïde
la source
7

Vous écrivez cette fonction

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

Ensuite, vous pouvez appeler simplement où vous voulez

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3
Shameem
la source
1
str_join = lambda *str_list: ''.join(s for s in str_list)
Rick soutient Monica le
7

L'utilisation de la concaténation de chaîne sur place par «+» est LA pire méthode de concaténation en termes de stabilité et d'implémentation croisée car elle ne prend pas en charge toutes les valeurs. La norme PEP8 décourage cela et encourage l'utilisation de format (), join () et append () pour une utilisation à long terme.

Comme cité dans la section liée "Recommandations de programmation":

Par exemple, ne vous fiez pas à l'implémentation efficace de CPython de la concaténation de chaînes sur place pour les instructions sous la forme a + = b ou a = a + b. Cette optimisation est fragile même dans CPython (elle ne fonctionne que pour certains types) et n'est pas présente du tout dans les implémentations qui n'utilisent pas le recomptage. Dans les parties sensibles aux performances de la bibliothèque, le formulaire '' .join () doit être utilisé à la place. Cela garantira que la concaténation se produit en temps linéaire sur différentes implémentations.

badslacks
la source
5
Le lien de référence aurait été bien :)
6

Bien que quelque peu daté, Code Like a Pythonista: Idiomatic Python recommande join()plus + dans cette section . Tout comme PythonSpeedPerformanceTips dans sa section sur la concaténation de chaînes , avec l'avertissement suivant:

La précision de cette section est contestée par rapport aux versions ultérieures de Python. Dans CPython 2.5, la concaténation de chaînes est assez rapide, bien que cela puisse ne pas s'appliquer de la même manière à d'autres implémentations Python. Voir ConcatenationTestCode pour une discussion.

Levon
la source
6

Comme @jdi le mentionne, la documentation Python suggère d'utiliser str.joinou io.StringIOpour la concaténation de chaînes. Et dit qu'un développeur devrait s'attendre +=à un temps quadratique dans une boucle, même s'il y a une optimisation depuis Python 2.4. Comme le dit cette réponse:

Si Python détecte que l'argument gauche n'a pas d'autres références, il appelle reallocpour tenter d'éviter une copie en redimensionnant la chaîne en place. Ce n'est pas quelque chose sur lequel vous devriez vous fier, car c'est un détail d'implémentation et parce que si vous avez reallocbesoin de déplacer fréquemment la chaîne, les performances se dégradent de toute façon en O (n ^ 2).

Je vais montrer un exemple de code du monde réel qui reposait naïvement sur +=cette optimisation, mais il ne s'appliquait pas. Le code ci-dessous convertit un itérable de chaînes courtes en plus gros morceaux à utiliser dans une API en bloc.

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

Ce code peut littéralement fonctionner pendant des heures en raison de la complexité temporelle quadratique. Voici des alternatives avec des structures de données suggérées:

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

Et un micro-benchmark:

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

micro-benchmark

saaj
la source
5

Vous pouvez le faire de différentes manières.

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

J'ai créé ce petit résumé à travers les articles suivants.

Kushan Gunasekera
la source
3

mon cas d'utilisation était légèrement différent. J'ai dû construire une requête où plus de 20 champs étaient dynamiques. J'ai suivi cette approche en utilisant la méthode de formatage

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

c'était relativement plus simple pour moi au lieu d'utiliser + ou d'autres moyens

Ishwar Rimal
la source