Concaténation de chaînes vs substitution de chaînes en Python


98

En Python, le lieu et le moment de l'utilisation de la concaténation de chaînes par rapport à la substitution de chaînes m'échappent. Étant donné que la concaténation des cordes a connu de grandes améliorations de performances, est-ce (de plus en plus) une décision stylistique plutôt que pratique?

Pour un exemple concret, comment gérer la construction d'URI flexibles:

DOMAIN = 'http://stackoverflow.com'
QUESTIONS = '/questions'

def so_question_uri_sub(q_num):
    return "%s%s/%d" % (DOMAIN, QUESTIONS, q_num)

def so_question_uri_cat(q_num):
    return DOMAIN + QUESTIONS + '/' + str(q_num)

Edit: Il y a également eu des suggestions pour joindre une liste de chaînes et pour utiliser la substitution nommée. Ce sont des variantes sur le thème central, qui est, quelle est la bonne façon de le faire à quel moment? Merci pour les réponses!


Drôle, en Ruby, l'interpolation de chaîne est généralement plus rapide que la concaténation ...
Keltia

vous avez oublié de retourner "" .join ([DOMAIN, QUESTIONS, str (q_num)])
Jimmy

Je ne suis pas un expert de Ruby, mais je parierais que l'interpolation est plus rapide car les chaînes sont mutables dans Ruby. Les chaînes sont des séquences immuables en Python.
gotgenes

1
juste un petit commentaire sur les URI. Les URI ne sont pas exactement comme des chaînes. Il y a des URI, vous devez donc être très prudent lorsque vous les concaténez ou les comparez. Exemple: un serveur délivrant ses représentations via http sur le port 80. example.org (pas de slah à la fin) example.org/ (slash) example.org:80/ (slah + port 80) sont les mêmes uri mais pas les mêmes chaîne.
karlcow

Réponses:


55

La concaténation est (nettement) plus rapide selon ma machine. Mais stylistiquement, je suis prêt à payer le prix de la substitution si la performance n'est pas critique. Eh bien, et si j'ai besoin d'un formatage, il n'est même pas nécessaire de poser la question ... il n'y a pas d'autre choix que d'utiliser l'interpolation / la création de modèles.

>>> import timeit
>>> def so_q_sub(n):
...  return "%s%s/%d" % (DOMAIN, QUESTIONS, n)
...
>>> so_q_sub(1000)
'http://stackoverflow.com/questions/1000'
>>> def so_q_cat(n):
...  return DOMAIN + QUESTIONS + '/' + str(n)
...
>>> so_q_cat(1000)
'http://stackoverflow.com/questions/1000'
>>> t1 = timeit.Timer('so_q_sub(1000)','from __main__ import so_q_sub')
>>> t2 = timeit.Timer('so_q_cat(1000)','from __main__ import so_q_cat')
>>> t1.timeit(number=10000000)
12.166618871951641
>>> t2.timeit(number=10000000)
5.7813972166853773
>>> t1.timeit(number=1)
1.103492206766532e-05
>>> t2.timeit(number=1)
8.5206360154188587e-06

>>> def so_q_tmp(n):
...  return "{d}{q}/{n}".format(d=DOMAIN,q=QUESTIONS,n=n)
...
>>> so_q_tmp(1000)
'http://stackoverflow.com/questions/1000'
>>> t3= timeit.Timer('so_q_tmp(1000)','from __main__ import so_q_tmp')
>>> t3.timeit(number=10000000)
14.564135316080637

>>> def so_q_join(n):
...  return ''.join([DOMAIN,QUESTIONS,'/',str(n)])
...
>>> so_q_join(1000)
'http://stackoverflow.com/questions/1000'
>>> t4= timeit.Timer('so_q_join(1000)','from __main__ import so_q_join')
>>> t4.timeit(number=10000000)
9.4431309007150048

10
avez-vous fait des tests avec de vraies grandes chaînes (comme 100000 caractères)?
drnk

24

N'oubliez pas la substitution nommée:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()

4
Ce code a au moins 2 mauvaises pratiques de programmation: l'attente de variables globales (le domaine et les questions ne sont pas déclarés à l'intérieur de la fonction) et le passage de plus de variables que nécessaire à une fonction format (). Downvoter car cette réponse enseigne de mauvaises pratiques de codage
jperelli

12

Méfiez-vous de la concaténation de chaînes en boucle! Le coût de la concaténation de chaînes est proportionnel à la longueur du résultat. Looping vous mène directement au pays de N-squared. Certains langages optimiseront la concaténation à la chaîne la plus récemment allouée, mais il est risqué de compter sur le compilateur pour optimiser votre algorithme quadratique jusqu'à linéaire. Il est préférable d'utiliser la primitive ( join?) Qui prend une liste entière de chaînes, effectue une allocation unique et les concatène toutes en une seule fois.


16
Ce n'est pas courant. Dans les dernières versions de python, un tampon de chaîne masqué est créé lorsque vous concaténez des chaînes dans une boucle.
Seun Osewa

5
@Seun: Oui, comme je l'ai dit, certaines langues seront optimisées, mais c'est une pratique risquée.
Norman Ramsey

11

"Comme la concaténation de chaînes a vu de grandes améliorations dans les performances ..."

Si la performance compte, c'est bon à savoir.

Cependant, les problèmes de performances que j'ai vus ne se résument jamais à des opérations de chaîne. J'ai généralement eu des problèmes avec les E / S, le tri et les opérations O ( n 2 ) étant les goulots d'étranglement.

Tant que les opérations sur les chaînes ne sont pas les limites des performances, je m'en tiendrai à des choses évidentes. Surtout, c'est la substitution quand il s'agit d'une ligne ou moins, la concaténation quand cela a du sens et un outil de modèle (comme Mako) quand il est grand.


10

Ce que vous souhaitez concaténer / interpoler et la manière dont vous souhaitez formater le résultat doivent guider votre décision.

  • L'interpolation de chaîne vous permet d'ajouter facilement une mise en forme. En fait, votre version d'interpolation de chaîne ne fait pas la même chose que votre version de concaténation; il ajoute en fait une barre oblique supplémentaire avant le q_numparamètre. Pour faire la même chose, vous devrez écrire return DOMAIN + QUESTIONS + "/" + str(q_num)dans cet exemple.

  • L'interpolation facilite le formatage des chiffres; "%d of %d (%2.2f%%)" % (current, total, total/current)serait beaucoup moins lisible sous forme de concaténation.

  • La concaténation est utile lorsque vous n'avez pas un nombre fixe d'éléments à définir.

Sachez également que Python 2.6 introduit une nouvelle version d'interpolation de chaîne, appelée création de modèles de chaînes :

def so_question_uri_template(q_num):
    return "{domain}/{questions}/{num}".format(domain=DOMAIN,
                                               questions=QUESTIONS,
                                               num=q_num)

La création de modèles de chaînes devrait éventuellement remplacer% -interpolation, mais cela ne se produira pas avant un certain temps, je pense.


Eh bien, cela se produira chaque fois que vous déciderez de passer à python 3.0. Voir également le commentaire de Peter pour le fait que vous pouvez quand même faire des substitutions nommées avec l'opérateur%.
John Fouhy

"La concaténation est utile lorsque vous n'avez pas un nombre fixe d'éléments à définir." - Vous voulez dire une liste / un tableau? Dans ce cas, ne pourriez-vous pas simplement les rejoindre ()?
strager

"Tu ne pourrais pas les rejoindre?" - Oui (en supposant que vous vouliez des séparateurs uniformes entre les éléments). Les compréhensions de listes et de générateurs fonctionnent très bien avec string.join.
Tim Lesher

1
"Eh bien, cela arrivera chaque fois que vous déciderez de passer à python 3.0" - Non, py3k prend toujours en charge l'opérateur%. Le prochain point de dépréciation possible est 3.1, donc il a encore de la vie dedans.
Tim Lesher

2
2 ans plus tard ... python 3.2 est proche de la sortie et l'interpolation de style% est toujours correcte.
Corey Goldberg

8

Je testais juste la vitesse de différentes méthodes de concaténation / substitution de chaînes par curiosité. Une recherche google sur le sujet m'a amené ici. J'ai pensé publier les résultats de mes tests dans l'espoir que cela aiderait quelqu'un à décider.

    import timeit
    def percent_():
            return "test %s, with number %s" % (1,2)

    def format_():
            return "test {}, with number {}".format(1,2)

    def format2_():
            return "test {1}, with number {0}".format(2,1)

    def concat_():
            return "test " + str(1) + ", with number " + str(2)

    def dotimers(func_list):
            # runs a single test for all functions in the list
            for func in func_list:
                    tmr = timeit.Timer(func)
                    res = tmr.timeit()
                    print "test " + func.func_name + ": " + str(res)

    def runtests(func_list, runs=5):
            # runs multiple tests for all functions in the list
            for i in range(runs):
                    print "----------- TEST #" + str(i + 1)
                    dotimers(func_list)

... Après avoir exécuté runtests((percent_, format_, format2_, concat_), runs=5), j'ai trouvé que la méthode% était environ deux fois plus rapide que les autres sur ces petites chaînes. La méthode concat était toujours la plus lente (à peine). Il y avait de très petites différences lors du changement de position dans la format()méthode, mais le changement de position était toujours au moins 0,01 plus lent que la méthode de format normal.

Échantillon de résultats de test:

    test concat_()  : 0.62  (0.61 to 0.63)
    test format_()  : 0.56  (consistently 0.56)
    test format2_() : 0.58  (0.57 to 0.59)
    test percent_() : 0.34  (0.33 to 0.35)

Je les ai exécutés parce que j'utilise la concaténation de chaînes dans mes scripts, et je me demandais quel en était le coût. Je les ai exécutés dans des ordres différents pour m'assurer que rien n'interférait, ou obtenir de meilleures performances en premier ou en dernier. D'un autre côté, j'ai "%s" + ("a" * 1024)ajouté des générateurs de chaînes plus longues dans ces fonctions comme et le concat régulier était presque 3 fois plus rapide (1,1 contre 2,8) que l'utilisation des méthodes formatet %. Je suppose que cela dépend des cordes et de ce que vous essayez de réaliser. Si les performances comptent vraiment, il vaudrait peut-être mieux essayer différentes choses et les tester. J'ai tendance à choisir la lisibilité plutôt que la vitesse, à moins que la vitesse ne devienne un problème, mais ce n'est que moi. Donc, je n'ai pas aimé mon copier / coller, j'ai dû mettre 8 espaces sur tout pour que tout soit correct. J'utilise habituellement 4.


1
Vous devez sérieusement réfléchir à ce que vous profilez et comment. Pour un, votre concat est lent parce que vous avez deux lancers str. Avec les chaînes, le résultat est le contraire, car la concatection de chaînes est en fait plus rapide que toutes les alternatives lorsque seules trois chaînes sont concernées.
Justus Wingert

@ JustusWingert, il a maintenant deux ans. J'ai beaucoup appris depuis que j'ai posté ce «test». Honnêtement, ces jours-ci, j'utilise str.format()et au- str.join()dessus de la concaténation normale. Je garde également un œil sur les «f-strings» de PEP 498 , qui ont récemment été acceptées. En ce qui concerne les str()appels affectant les performances, je suis sûr que vous avez raison. Je n'avais aucune idée du coût des appels de fonction à l'époque. Je pense toujours que des tests doivent être effectués en cas de doute.
Cj Welborn

Après un test rapide avec join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), il semble joinégalement plus lent que le pourcentage.
gaborous

4

Rappelez-vous, les décisions stylistiques sont des décisions pratiques, si jamais vous prévoyez de maintenir ou de déboguer votre code :-) Il y a une citation célèbre de Knuth (citant peut-être Hoare?): «Nous devrions oublier les petits gains d'efficacité, disons environ 97% du temps: L'optimisation prématurée est la racine de tout Mal."

Tant que vous faites attention de ne pas (dire) transformer une tâche O (n) en une tâche O (n 2 ), j'irais avec celle que vous trouvez la plus facile à comprendre.


0

J'utilise la substitution partout où je peux. Je n'utilise la concaténation que si je construis une chaîne dans, par exemple, une boucle for.


7
"construction d'une chaîne dans une boucle for" - c'est souvent un cas où vous pouvez utiliser '' .join et une expression de générateur ..
John Fouhy

-1

En fait, la bonne chose à faire, dans ce cas (créer des chemins), est d'utiliser os.path.join. Pas de concaténation ou d'interpolation de chaînes


1
c'est vrai pour les chemins OS (comme sur votre système de fichiers) mais pas lors de la construction d'un URI comme dans cet exemple. Les URI ont toujours «/» comme séparateur.
Andre Blum
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.