Quelle est la méthode de concaténation de chaînes la plus efficace en python?

148

Existe-t-il une méthode de concaténation de chaînes de masse efficace en Python (comme StringBuilder en C # ou StringBuffer en Java)? J'ai trouvé les méthodes suivantes ici :

  • Concaténation simple utilisant +
  • Utilisation de la liste de chaînes et de la joinméthode
  • Utilisation UserStringdu MutableStringmodule
  • Utilisation du tableau de caractères et du arraymodule
  • Utilisation cStringIOdu StringIOmodule

Mais qu'est-ce que vos experts utilisent ou suggèrent et pourquoi?

[ Une question connexe ici ]

mshsayem
la source
1
Question similaire: stackoverflow.com/questions/476772
Peter Mortensen
Pour concaténer des fragments connus en un seul, Python 3.6 aura des f''chaînes de format qui seront plus rapides que toutes les alternatives des versions précédentes de Python.
Antti Haapala

Réponses:

127

Cela pourrait vous intéresser: Une anecdote d'optimisation par Guido. Même s'il vaut la peine de se rappeler qu'il s'agit d'un ancien article et qu'il est antérieur à l'existence de choses comme ''.join(même si je suppose que string.joinfieldsc'est plus ou moins la même chose)

Sur la base de cela, le arraymodule peut être le plus rapide si vous pouvez y exprimer votre problème. Mais ''.joinc'est probablement assez rapide et a l'avantage d'être idiomatique et donc plus facile à comprendre pour les autres programmeurs python.

Enfin, la règle d'or de l'optimisation: n'optimisez que si vous savez que vous en avez besoin et mesurez plutôt que de deviner.

Vous pouvez mesurer différentes méthodes à l'aide du timeitmodule. Cela peut vous dire lequel est le plus rapide, au lieu que des inconnus aléatoires sur Internet fassent des suppositions.

John Fouhy
la source
1
Vous voulez ajouter au point sur le moment d'optimiser: assurez-vous de tester contre les pires cas. Par exemple, je peux augmenter mon échantillon afin que mon code actuel passe de 0,17 seconde à 170 secondes. Eh bien, je veux tester des échantillons de plus grande taille, car il y a moins de variation.
Flipper
2
"N'optimisez pas avant de savoir que vous en avez besoin." À moins que vous n'utilisiez simplement un idiome nominalement différent et que vous puissiez éviter de retravailler votre code avec peu d'effort supplémentaire.
jeremyjjbrown
1
L'entretien (qui est toujours le moment idéal pour approfondir votre compréhension) est l'un des endroits dont vous avez besoin. Malheureusement, je n'ai trouvé AUCUN article moderne à ce sujet. (1) La chaîne Java / C # est-elle toujours aussi mauvaise en 2017? (2) Et le C ++? (3) Parlez maintenant des derniers et des meilleurs en Python en vous concentrant sur les cas où nous devons faire des millions de concaténations. Pouvons-nous avoir confiance que la jointure fonctionnerait en temps linéaire?
user1854182
Que signifie «assez vite» .join()? La question principale est de savoir si a) créer une copie de la chaîne pour la concaténation (similaire à s = s + 'abc'), qui nécessite l'exécution O (n), ou b) simplement ajouter à la chaîne existante sans créer de copie, ce qui nécessite O (1) ?
CGFoX
64

''.join(sequenceofstrings) est ce qui fonctionne le mieux - le plus simple et le plus rapide

Alex Martelli
la source
3
@mshsayem, en Python, une séquence peut être n'importe quel objet énumérable, même une fonction.
Nick Dandoulakis
2
J'adore l' ''.join(sequence)idiome. Il est particulièrement utile de produire des listes séparées par des virgules: ', '.join([1, 2, 3])donne la chaîne '1, 2, 3'.
Andrew Keeton
7
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- dans ce cas, l'argument à joindre est un itérateur, créé via une expression de générateur. Il n'y a pas de liste temporaire qui se construit.
balpha
2
@balpha: et pourtant la version du générateur est plus lente que la version de compréhension de liste: C: \ temp> python -mtimeit "'' .join (chr (x) for x in xrange (65,91))" 100000 boucles, le meilleur de 3: 9,71 usec par boucle C: \ temp> python -mtimeit "'' .join ([chr (x) for x in xrange (65,91)])" 100000 boucles, le meilleur de 3: 7,1 usec par boucle
hughdbrown
1
@hughdbrown, oui, quand vous avez de la mémoire libre, le listcomp wazoo (cas timeit typique) peut être mieux optimisé que genexp, souvent de 20 à 30%. Quand la mémoire est serrée, les choses sont différentes - difficiles à reproduire dans le temps, cependant! -)
Alex Martelli
58

Python 3.6 a changé le jeu pour la concaténation de chaînes de composants connus avec l' interpolation de chaînes littérales .

Compte tenu du cas de test de la réponse de mkoistinen , avoir des chaînes

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'

Les prétendants sont

  • f'http://{domain}/{lang}/{path}'- 0,151 µs

  • 'http://%s/%s/%s' % (domain, lang, path) - 0,321 µs

  • 'http://' + domain + '/' + lang + '/' + path - 0,356 µs

  • ''.join(('http://', domain, '/', lang, '/', path))- 0,249 µs (notez que la construction d'un tuple de longueur constante est légèrement plus rapide que la construction d'une liste de longueur constante).

Ainsi actuellement, le code le plus court et le plus beau possible est également le plus rapide.

Dans les versions alpha de Python 3.6, l'implémentation des f''chaînes était la plus lente possible - en fait, le code d'octet généré est à peu près équivalent au ''.join()cas des appels inutiles str.__format__auxquels sans arguments retourneraient simplement selfinchangés. Ces inefficacités ont été corrigées avant la version 3.6 final.

La vitesse peut être comparée à la méthode la plus rapide pour Python 2, qui est la +concaténation sur mon ordinateur; et cela prend 0,203 µs avec des chaînes de 8 bits et 0,259 µs si les chaînes sont toutes Unicode.

Antti Haapala
la source
38

Cela dépend de ce que vous faites.

Après Python 2.5, la concaténation de chaînes avec l'opérateur + est assez rapide. Si vous ne faites que concaténer quelques valeurs, l'utilisation de l'opérateur + fonctionne mieux:

>>> x = timeit.Timer(stmt="'a' + 'b'")
>>> x.timeit()
0.039999961853027344

>>> x = timeit.Timer(stmt="''.join(['a', 'b'])")
>>> x.timeit()
0.76200008392333984

Cependant, si vous assemblez une chaîne dans une boucle, il vaut mieux utiliser la méthode de jonction de liste:

>>> join_stmt = """
... joined_str = ''
... for i in xrange(100000):
...   joined_str += str(i)
... """
>>> x = timeit.Timer(join_stmt)
>>> x.timeit(100)
13.278000116348267

>>> list_stmt = """
... str_list = []
... for i in xrange(100000):
...   str_list.append(str(i))
... ''.join(str_list)
... """
>>> x = timeit.Timer(list_stmt)
>>> x.timeit(100)
12.401000022888184

... mais notez que vous devez assembler un nombre relativement élevé de chaînes avant que la différence ne devienne perceptible.

Jason Baker
la source
2
1) Dans votre première mesure, c'est probablement la construction de la liste qui prend le temps. Essayez avec un tuple. 2) CPython fonctionne uniformément bien, mais les autres implémentations Python fonctionnent bien moins bien avec + et + =
u0b34a0f6ae
22

Selon la réponse de John Fouhy, n'optimisez pas sauf si vous devez le faire, mais si vous êtes ici et que vous posez cette question, c'est peut-être précisément parce que vous devez le faire . Dans mon cas, j'avais besoin d'assembler des URL à partir de variables de chaîne ... rapidement. Je n'ai remarqué que personne (jusqu'à présent) ne semble envisager la méthode du format de chaîne, alors j'ai pensé essayer cela et, surtout pour un intérêt léger, j'ai pensé que je jetterais l'opérateur d'interpolation de chaîne là-dedans pour un bon mesureur. Pour être honnête, je ne pensais pas que l'un ou l'autre de ces éléments s'empilerait jusqu'à une opération directe «+» ou un «.join ()». Mais devinez quoi? Sur mon système Python 2.7.5, l'opérateur d'interpolation de chaîne les règle tous et string.format () est le moins performant:

# concatenate_test.py

from __future__ import print_function
import timeit

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'
iterations = 1000000

def meth_plus():
    '''Using + operator'''
    return 'http://' + domain + '/' + lang + '/' + path

def meth_join():
    '''Using ''.join()'''
    return ''.join(['http://', domain, '/', lang, '/', path])

def meth_form():
    '''Using string.format'''
    return 'http://{0}/{1}/{2}'.format(domain, lang, path)

def meth_intp():
    '''Using string interpolation'''
    return 'http://%s/%s/%s' % (domain, lang, path)

plus = timeit.Timer(stmt="meth_plus()", setup="from __main__ import meth_plus")
join = timeit.Timer(stmt="meth_join()", setup="from __main__ import meth_join")
form = timeit.Timer(stmt="meth_form()", setup="from __main__ import meth_form")
intp = timeit.Timer(stmt="meth_intp()", setup="from __main__ import meth_intp")

plus.val = plus.timeit(iterations)
join.val = join.timeit(iterations)
form.val = form.timeit(iterations)
intp.val = intp.timeit(iterations)

min_val = min([plus.val, join.val, form.val, intp.val])

print('plus %0.12f (%0.2f%% as fast)' % (plus.val, (100 * min_val / plus.val), ))
print('join %0.12f (%0.2f%% as fast)' % (join.val, (100 * min_val / join.val), ))
print('form %0.12f (%0.2f%% as fast)' % (form.val, (100 * min_val / form.val), ))
print('intp %0.12f (%0.2f%% as fast)' % (intp.val, (100 * min_val / intp.val), ))

Les resultats:

# python2.7 concatenate_test.py
plus 0.360787868500 (90.81% as fast)
join 0.452811956406 (72.36% as fast)
form 0.502608060837 (65.19% as fast)
intp 0.327636957169 (100.00% as fast)

Si j'utilise un domaine plus court et un chemin plus court, l'interpolation l'emporte toujours. La différence est cependant plus prononcée avec des cordes plus longues.

Maintenant que j'avais un joli script de test, j'ai également testé sous Python 2.6, 3.3 et 3.4, voici les résultats. En Python 2.6, l'opérateur plus est le plus rapide! Sur Python 3, rejoindre l'emporte. Remarque: ces tests sont très répétables sur mon système. Ainsi, «plus» est toujours plus rapide sur 2.6, «intp» est toujours plus rapide sur 2.7 et «join» est toujours plus rapide sur Python 3.x.

# python2.6 concatenate_test.py
plus 0.338213920593 (100.00% as fast)
join 0.427221059799 (79.17% as fast)
form 0.515371084213 (65.63% as fast)
intp 0.378169059753 (89.43% as fast)

# python3.3 concatenate_test.py
plus 0.409130576998 (89.20% as fast)
join 0.364938726001 (100.00% as fast)
form 0.621366866995 (58.73% as fast)
intp 0.419064424001 (87.08% as fast)

# python3.4 concatenate_test.py
plus 0.481188605998 (85.14% as fast)
join 0.409673971997 (100.00% as fast)
form 0.652010936996 (62.83% as fast)
intp 0.460400978001 (88.98% as fast)

# python3.5 concatenate_test.py
plus 0.417167026084 (93.47% as fast)
join 0.389929617057 (100.00% as fast)
form 0.595661019906 (65.46% as fast)
intp 0.404455224983 (96.41% as fast)

Leçon apprise:

  • Parfois, mes hypothèses sont complètement fausses.
  • Test contre le système env. vous serez en production.
  • L'interpolation de chaîne n'est pas encore morte!

tl; dr:

  • Si vous utilisez 2.6, utilisez l'opérateur +.
  • si vous utilisez 2.7, utilisez l'opérateur '%'.
  • si vous utilisez 3.x utilisez '' .join ().
mkoistinen
la source
2
Remarque: l'interpolation de chaîne littérale est encore plus rapide pour la version 3.6+:f'http://{domain}/{lang}/{path}'
TemporalWolf
1
En outre, .format()a trois formes, afin de rapide à lent: "{}".format(x), "{0}".format(x),"{x}".format(x=x)
TemporalWolf
La vraie leçon: lorsque votre domaine de problème est petit, par exemple la composition de chaînes courtes, la méthode n'a pas d'importance le plus souvent. Et même quand cela compte, par exemple, vous construisez vraiment un million de chaînes, les frais généraux sont souvent plus importants. C'est un symptôme typique de s'inquiéter du mauvais problème. Ce n'est que lorsque la surcharge n'est pas significative, par exemple lors de la construction d'un livre entier sous forme de chaîne, la différence de méthode commence à avoir de l'importance.
Hui Zhou
7

cela dépend à peu près des tailles relatives de la nouvelle chaîne après chaque nouvelle concaténation. Avec l' +opérateur, pour chaque concaténation, une nouvelle chaîne est créée. Si les chaînes intermédiaires sont relativement longues, le +devient de plus en plus lent car la nouvelle chaîne intermédiaire est stockée.

Considérez ce cas:

from time import time
stri=''
a='aagsdfghfhdyjddtyjdhmfghmfgsdgsdfgsdfsdfsdfsdfsdfsdfddsksarigqeirnvgsdfsdgfsdfgfg'
l=[]
#case 1
t=time()
for i in range(1000):
    stri=stri+a+repr(i)
print time()-t

#case 2
t=time()
for i in xrange(1000):
    l.append(a+repr(i))
z=''.join(l)
print time()-t

#case 3
t=time()
for i in range(1000):
    stri=stri+repr(i)
print time()-t

#case 4
t=time()
for i in xrange(1000):
    l.append(repr(i))
z=''.join(l)
print time()-t

Résultats

1 0,00493192672729

2 0,000509023666382

3 0,00042200088501

4 0,000482797622681

Dans le cas de 1 et 2, nous ajoutons une grande chaîne et join () fonctionne environ 10 fois plus rapidement. Dans les cas 3 et 4, nous ajoutons une petite chaîne, et '+' fonctionne légèrement plus vite

David Bielen
la source
3

Je suis tombé sur une situation où je devais avoir une chaîne appendable de taille inconnue. Voici les résultats du benchmark (python 2.7.3):

$ python -m timeit -s 's=""' 's+="a"'
10000000 loops, best of 3: 0.176 usec per loop
$ python -m timeit -s 's=[]' 's.append("a")'
10000000 loops, best of 3: 0.196 usec per loop
$ python -m timeit -s 's=""' 's="".join((s,"a"))'
100000 loops, best of 3: 16.9 usec per loop
$ python -m timeit -s 's=""' 's="%s%s"%(s,"a")'
100000 loops, best of 3: 19.4 usec per loop

Cela semble montrer que «+ =» est le plus rapide. Les résultats du lien skymind sont un peu dépassés.

(Je me rends compte que le deuxième exemple n'est pas complet, la liste finale devrait être jointe. Cela montre cependant que la simple préparation de la liste prend plus de temps que la chaîne concat.)

MattK
la source
Je reçois moins de 1 seconde pour les 3e et 4e tests. Pourquoi avez-vous des temps aussi élevés? pastebin.com/qabNMCHS
bad_keypoints
@ronnieaka: Il obtient des temps inférieurs à 1 seconde pour tous les tests. Il obtient> 1 µs pour les 3ème et 4ème, ce que vous n'avez pas fait. J'obtiens également des temps plus lents sur ces tests (sur Python 2.7.5, Linux). Cela pourrait être le processeur, la version, les indicateurs de construction, qui sait.
Thanatos
Ces résultats de référence sont inutiles. En particulier, le premier cas, qui ne fait aucune concaténation de chaînes, renvoie simplement la deuxième valeur de chaîne intacte.
Antti Haapala
3

Un an plus tard, testons la réponse de mkoistinen avec python 3.4.3:

  • plus 0,963564149000 (95,83% aussi vite)
  • rejoindre 0.923408469000 (100,00% aussi vite)
  • formulaire 1.501130934000 (61,51% aussi rapide)
  • intp 1.019677452000 (90,56% aussi rapide)

Rien n'a changé. Join est toujours la méthode la plus rapide. Avec intp étant sans doute le meilleur choix en termes de lisibilité, vous voudrez peut-être néanmoins utiliser intp.

Ramsch
la source
1
Peut-être que cela pourrait être un ajout à la réponse mkoistinen car il manque un peu de réponse complète (ou du moins ajoutez le code que vous utilisez).
Trilarion
1

Inspiré des benchmarks de @ JasonBaker, en voici un simple comparant 10 "abcdefghijklmnopqrstuvxyz"chaînes, montrant que .join()c'est plus rapide; même avec cette petite augmentation des variables:

Caténation

>>> x = timeit.Timer(stmt='"abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz"')
>>> x.timeit()
0.9828147209324385

Joindre

>>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
>>> x.timeit()
0.6114138159765048
À
la source
Jetez un œil à la réponse acceptée (
faites
1

Pour un petit ensemble de chaînes courtes (c'est-à-dire 2 ou 3 chaînes de pas plus de quelques caractères), plus est encore beaucoup plus rapide. Utilisation du merveilleux script de mkoistinen dans Python 2 et 3:

plus 2.679107467004 (100.00% as fast)
join 3.653773699996 (73.32% as fast)
form 6.594011374000 (40.63% as fast)
intp 4.568015249999 (58.65% as fast)

Ainsi, lorsque votre code effectue un grand nombre de petites concaténations séparées, le plus est le moyen préféré si la vitesse est cruciale.

user7505681
la source
1

Les "nouvelles f-strings en Python 3.6" sont probablement le moyen le plus efficace de concaténer des chaînes.

Utilisation de% s

>>> timeit.timeit("""name = "Some"
... age = 100
... '%s is %s.' % (name, age)""", number = 10000)
0.0029734770068898797

Utiliser .format

>>> timeit.timeit("""name = "Some"
... age = 100
... '{} is {}.'.format(name, age)""", number = 10000)
0.004015227983472869

Utilisation de f

>>> timeit.timeit("""name = "Some"
... age = 100
... f'{name} is {age}.'""", number = 10000)
0.0019175919878762215

Source: https://realpython.com/python-f-strings/

SuperNova
la source