Une raison de ne pas utiliser «+» pour concaténer deux chaînes?

124

Un anti-modèle courant en Python consiste à concaténer une séquence de chaînes en utilisant +dans une boucle. C'est mauvais car l'interpréteur Python doit créer un nouvel objet chaîne pour chaque itération, et cela finit par prendre un temps quadratique. (Les versions récentes de CPython peuvent apparemment optimiser cela dans certains cas, mais d'autres implémentations ne le peuvent pas, donc les programmeurs sont découragés de se fier à cela.) ''.joinEst la bonne façon de le faire.

Cependant, j'ai entendu dire ( y compris ici sur Stack Overflow ) que vous ne devriez jamais, jamais utiliser +pour la concaténation de chaînes, mais à la place toujours utiliser ''.joinune chaîne de format. Je ne comprends pas pourquoi c'est le cas si vous ne concaténez que deux chaînes. Si ma compréhension est correcte, cela ne devrait pas prendre de temps quadratique, et je pense que a + bc'est plus propre et plus lisible que l'un ''.join((a, b))ou l' autre '%s%s' % (a, b).

Est-ce une bonne pratique d'utiliser +pour concaténer deux chaînes? Ou y a-t-il un problème dont je ne suis pas au courant?

Taymon
la source
C'est plus net et vous avez plus de contrôle pour ne pas faire de concaténation. MAIS son compromis légèrement plus lent, contre les cordes: P
Jakob Bowyer
Dites-vous que +c'est plus rapide ou plus lent? Et pourquoi?
Taymon
1
+ est plus rapide, In [2]: %timeit "a"*80 + "b"*80 1000000 loops, best of 3: 356 ns per loop In [3]: %timeit "%s%s" % ("a"*80, "b"*80) 1000000 loops, best of 3: 907 ns per loop
Jakob Bowyer
4
In [3]: %timeit "%s%s" % (a, b) 1000000 loops, best of 3: 590 ns per loop In [4]: %timeit a + b 10000000 loops, best of 3: 147 ns per loop
Jakob Bowyer du
1
@JakobBowyer et autres: L'argument "la concaténation de chaînes est mauvaise" n'a presque rien à voir avec la vitesse, mais profite de la conversion automatique de type avec __str__. Voir ma réponse pour des exemples.
Izkata

Réponses:

120

Il n'y a rien de mal à concaténer deux chaînes avec +. En effet, c'est plus facile à lire que ''.join([a, b]).

Vous avez raison cependant que concaténer plus de 2 chaînes avec +est une opération O (n ^ 2) (par rapport à O (n) pour join) et devient donc inefficace. Cependant, cela n'a pas à voir avec l'utilisation d'une boucle. Même a + b + c + ...est O (n ^ 2), la raison étant que chaque concaténation produit une nouvelle chaîne.

CPython2.4 et les versions ultérieures essaient d'atténuer cela, mais il est toujours conseillé de l'utiliser joinlors de la concaténation de plus de 2 chaînes.

ggozad
la source
5
@Mutant: .joinprend un itérable, donc les deux .join([a,b])et .join((a,b))sont valides.
enfant trouvé
1
Des timings intéressants suggèrent d'utiliser +ou +=dans la réponse acceptée (à partir de 2013) sur stackoverflow.com/a/12171382/378826 (de Lennart Regebro) même pour CPython 2.3+ et de ne choisir le modèle «ajouter / joindre» que si ce plus clair expose le idée de la solution du problème à portée de main.
Dilettant
49

L'opérateur Plus est une solution parfaitement adaptée pour concaténer deux chaînes Python. Mais si vous continuez à ajouter plus de deux chaînes (n> 25), vous voudrez peut-être penser à autre chose.

''.join([a, b, c]) L'astuce est une optimisation des performances.

Mikko Ohtamaa
la source
2
Un tuple ne serait-il pas mieux qu'une liste?
ThiefMaster
7
Tuple serait plus rapide - le code n'était qu'un exemple :) Habituellement, les entrées de plusieurs chaînes longues sont dynamiques.
Mikko Ohtamaa
5
@martineau Je pense qu'il veut dire générer et insérer dynamiquement des append()chaînes dans une liste.
Peter C
5
Il faut dire ici: le tuple est généralement une structure plus lente, surtout s'il est en croissance. Avec list, vous pouvez utiliser list.extend (list_of_items) et list.append (item) qui sont beaucoup plus rapides lors de la concaténation dynamique des éléments.
Antti Haapala du
6
+1 pour n > 25. Les humains ont besoin de points de référence pour commencer quelque part.
n611x007
8

L'hypothèse selon laquelle on ne devrait jamais, jamais utiliser + pour la concaténation de chaînes, mais à la place toujours utiliser «» .join peut être un mythe. Il est vrai que l'utilisation +crée des copies temporaires inutiles de l'objet chaîne immuable, mais l'autre fait non souvent cité est que l'appel joindans une boucle ajouterait généralement la surcharge de function call. Prenons votre exemple.

Créez deux listes, une à partir de la question SO liée et une autre

>>> myl1 = ['A','B','C','D','E','F']
>>> myl2=[chr(random.randint(65,90)) for i in range(0,10000)]

Permet de créer deux fonctions UseJoinet UsePlusd'utiliser les fonctionnalités joinet respectives +.

>>> def UsePlus():
    return [myl[i] + myl[i + 1] for i in range(0,len(myl), 2)]

>>> def UseJoin():
    [''.join((myl[i],myl[i + 1])) for i in range(0,len(myl), 2)]

Permet d'exécuter timeit avec la première liste

>>> myl=myl1
>>> t1=timeit.Timer("UsePlus()","from __main__ import UsePlus")
>>> t2=timeit.Timer("UseJoin()","from __main__ import UseJoin")
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=100000)/100000)
2.48 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=100000)/100000)
2.61 usec/pass
>>> 

Ils ont presque le même temps d'exécution.

Permet d'utiliser cProfile

>>> myl=myl2
>>> cProfile.run("UsePlus()")
         5 function calls in 0.001 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <pyshell#1376>:1(UsePlus)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("UseJoin()")
         5005 function calls in 0.029 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.015    0.015    0.029    0.029 <pyshell#1388>:1(UseJoin)
        1    0.000    0.000    0.029    0.029 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     5000    0.014    0.000    0.014    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {range}

Et il semble que l'utilisation de Join, entraîne des appels de fonction inutiles qui pourraient ajouter à la surcharge.

Revenons maintenant à la question. Doit-on décourager l'utilisation de +over joindans tous les cas?

Je crois que non, les choses doivent être prises en considération

  1. Longueur de la chaîne en question
  2. No d'opération de concaténation.

Et bien sûr, dans une optimisation prématurée du développement, c'est mal.

Abhijit
la source
7
Bien sûr, l'idée serait de ne pas utiliser joinà l'intérieur de la boucle elle-même - plutôt la boucle génèrerait une séquence qui serait transmise pour se joindre.
jsbueno
7

Lorsque vous travaillez avec plusieurs personnes, il est parfois difficile de savoir exactement ce qui se passe. Utiliser une chaîne de format au lieu de la concaténation peut éviter un désagrément particulier qui nous est arrivé une tonne de fois:

Disons qu'une fonction nécessite un argument et que vous l'écrivez en vous attendant à obtenir une chaîne:

In [1]: def foo(zeta):
   ...:     print 'bar: ' + zeta

In [2]: foo('bang')
bar: bang

Donc, cette fonction peut être utilisée assez souvent tout au long du code. Vos collègues savent peut-être exactement ce qu'il fait, mais ne sont pas nécessairement parfaitement à jour sur les éléments internes, et peuvent ne pas savoir que la fonction attend une chaîne. Et ainsi ils peuvent finir avec ceci:

In [3]: foo(23)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

/home/izkata/<ipython console> in foo(zeta)

TypeError: cannot concatenate 'str' and 'int' objects

Il n'y aurait aucun problème si vous utilisiez simplement une chaîne de format:

In [1]: def foo(zeta):
   ...:     print 'bar: %s' % zeta
   ...:     
   ...:     

In [2]: foo('bang')
bar: bang

In [3]: foo(23)
bar: 23

Il en va de même pour tous les types d'objets qui définissent __str__, qui peuvent également être transmis:

In [1]: from datetime import date

In [2]: zeta = date(2012, 4, 15)

In [3]: print 'bar: ' + zeta
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

TypeError: cannot concatenate 'str' and 'datetime.date' objects

In [4]: print 'bar: %s' % zeta
bar: 2012-04-15

Alors oui: si vous pouvez utiliser une chaîne de format, faites-le et profitez de ce que Python a à offrir.

Izkata
la source
1
+1 pour une opinion dissidente bien motivée. Je pense toujours que je suis favorable +.
Taymon
1
Pourquoi ne définiriez-vous pas simplement la méthode foo comme suit: print 'bar:' + str (zeta)?
EngineerWithJava54321
@ EngineerWithJava54321 Pour un exemple, zeta = u"a\xac\u1234\u20ac\U00008000"- vous devez donc l'utiliser print 'bar: ' + unicode(zeta)pour vous assurer qu'il n'y a pas d'erreur. %sle fait bien sans y penser, et est beaucoup plus court
Izkata
@ EngineerWithJava54321 D'autres exemples sont moins pertinents ici, mais "bar: %s"peuvent par exemple être traduits "zrb: %s br"dans une autre langue. La %sversion fonctionnera simplement, mais la version concatée de chaînes deviendrait un désordre pour gérer tous les cas et vos traducteurs auraient maintenant deux traductions distinctes à traiter
Izkata
S'ils ne savent pas ce qu'est l'implémentation de foo, ils rencontreront cette erreur avec any def.
intérieur du
3

J'ai fait un test rapide:

import sys

str = e = "a xxxxxxxxxx very xxxxxxxxxx long xxxxxxxxxx string xxxxxxxxxx\n"

for i in range(int(sys.argv[1])):
    str = str + e

et chronométré:

mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  8000000
8000000 times

real    0m2.165s
user    0m1.620s
sys     0m0.540s
mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  16000000
16000000 times

real    0m4.360s
user    0m3.480s
sys     0m0.870s

Il y a apparemment une optimisation pour le a = a + bcas. Il ne présente pas de temps O (n ^ 2) comme on pourrait le soupçonner.

Donc au moins en termes de performances, l'utilisation +est bonne.

Michael Slade
la source
3
Vous pouvez comparer ici le cas "rejoindre". Et il y a la question des autres implémentations de Python, telles que pypy, jython, ironpython, etc ...
jsbueno
3

Selon la documentation Python, l'utilisation de str.join () vous donnera une cohérence des performances à travers diverses implémentations de Python. Bien que CPython optimise le comportement quadratique de s = s + t, d'autres implémentations Python peuvent ne pas le faire.

Détail de l'implémentation CPython : Si s et t sont tous deux des chaînes, certaines implémentations Python telles que CPython peuvent généralement effectuer une optimisation sur place pour les affectations de la forme s = s + t ou s + = t. Le cas échéant, cette optimisation rend le temps d'exécution quadratique beaucoup moins probable. Cette optimisation dépend à la fois de la version et de l'implémentation. Pour le code sensible aux performances, il est préférable d'utiliser la méthode str.join () qui assure des performances de concaténation linéaire cohérentes entre les versions et les implémentations.

Types de séquence dans les documents Python (voir la note de bas de page [6])

Duc
la source
2

J'utilise ce qui suit avec python 3.8

string4 = f'{string1}{string2}{string3}'
Lucas Vazquez
la source
0

'' .join ([a, b]) est une meilleure solution que + .

Parce que le code doit être écrit d'une manière qui ne désavantage pas les autres implémentations de Python (PyPy, Jython, IronPython, Cython, Psyco, etc.)

form a + = b ou a = a + b est fragile même en CPython et n'est pas du tout présent dans les implémentations qui n'utilisent pas de refcounting (le comptage de références est une technique de stockage du nombre de références, pointeurs ou poignées vers un ressource comme un objet, un bloc de mémoire, un espace disque ou une autre ressource )

https://www.python.org/dev/peps/pep-0008/#programming-recommendations

muhammad ali e
la source
1
a += bfonctionne dans toutes les implémentations de Python, c'est juste que sur certaines d'entre elles, cela prend un temps quadratique lorsqu'il est fait à l'intérieur d'une boucle ; la question portait sur la concaténation de chaînes en dehors d'une boucle.
Taymon