Concatenazione di stringhe vs. sostituzione di stringhe in Python


98

In Python, il dove e quando usare la concatenazione di stringhe rispetto alla sostituzione di stringhe mi sfugge. Dato che la concatenazione di stringhe ha visto grandi aumenti nelle prestazioni, questa (sempre più) è una decisione stilistica piuttosto che pratica?

Per un esempio concreto, come si dovrebbe gestire la costruzione di URI flessibili:

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)

Modifica: sono stati forniti anche suggerimenti sull'unione a un elenco di stringhe e sull'utilizzo della sostituzione con nome. Queste sono varianti sul tema centrale, che è, in che modo è il modo giusto per farlo in quale momento? Grazie per le risposte!


Divertente, in Ruby, l'interpolazione di stringhe è generalmente più veloce della concatenazione ...
Keltia

hai dimenticato di restituire "" .join ([DOMAIN, QUESTIONS, str (q_num)])
Jimmy

Non sono un esperto di Ruby, ma scommetterei che l'interpolazione è più veloce perché le stringhe sono mutabili in Ruby. Le stringhe sono sequenze immutabili in Python.
Gotgenes

1
solo un piccolo commento sugli URI. Gli URI non sono esattamente come le stringhe. Ci sono URI, quindi devi stare molto attento quando li concateni o li confronti. Esempio: un server che fornisce le sue rappresentazioni su http sulla porta 80. example.org (nessuno slah alla fine) example.org/ (slash) example.org:80/ (slah + porta 80) sono gli stessi uri ma non gli stessi corda.
karlcow

Risposte:


55

La concatenazione è (significativamente) più veloce secondo la mia macchina. Ma stilisticamente, sono disposto a pagare il prezzo della sostituzione se le prestazioni non sono critiche. Bene, e se ho bisogno di formattazione, non c'è nemmeno bisogno di porre la domanda ... non c'è altra scelta che usare l'interpolazione / modelli.

>>> 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
hai fatto test con stringhe veramente grandi (come 100000 caratteri)?
drnk

24

Non dimenticare la sostituzione con nome:

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

4
Questo codice ha almeno 2 cattive pratiche di programmazione: l'aspettativa di variabili globali (dominio e domande non sono dichiarate all'interno della funzione) e il passaggio di più variabili del necessario a una funzione format (). Downvoting perché questa risposta insegna cattive pratiche di codifica.
jperelli

12

Diffidare di concatenare le stringhe in un ciclo! Il costo della concatenazione di stringhe è proporzionale alla lunghezza del risultato. Il looping ti porta direttamente nella terra di N-quadrato. Alcuni linguaggi ottimizzeranno la concatenazione alla stringa allocata più di recente, ma è rischioso contare sul compilatore per ottimizzare l'algoritmo quadratico fino a renderlo lineare. È meglio usare la primitiva ( join?) Che accetta un intero elenco di stringhe, esegue una singola allocazione e le concatena tutte in una volta.


16
Non è attuale. Nelle ultime versioni di Python, viene creato un buffer di stringhe nascosto quando concatenate le stringhe in un ciclo.
Seun Osewa,

5
@Seun: Sì, come ho detto, alcune lingue verranno ottimizzate, ma è una pratica rischiosa.
Norman Ramsey,

11

"Poiché la concatenazione di stringhe ha visto grandi aumenti nelle prestazioni ..."

Se le prestazioni sono importanti, è bene saperlo.

Tuttavia, i problemi di prestazioni che ho riscontrato non sono mai dovuti alle operazioni sulle stringhe. In genere ho avuto problemi con I / O, ordinamento e operazioni O ( n 2 ) come colli di bottiglia.

Fino a quando le operazioni sulle stringhe non sono i limitatori delle prestazioni, mi atterrò a cose ovvie. Principalmente, si tratta di sostituzione quando è una riga o meno, concatenazione quando ha senso e uno strumento modello (come Mako) quando è grande.


10

Ciò che vuoi concatenare / interpolare e come vuoi formattare il risultato dovrebbe guidare la tua decisione.

  • L'interpolazione delle stringhe consente di aggiungere facilmente la formattazione. In effetti, la tua versione di interpolazione di stringhe non fa la stessa cosa della tua versione di concatenazione; effettivamente aggiunge una barra in avanti extra prima del q_numparametro. Per fare la stessa cosa, dovresti scrivere return DOMAIN + QUESTIONS + "/" + str(q_num)in quell'esempio.

  • L'interpolazione semplifica la formattazione dei valori numerici; "%d of %d (%2.2f%%)" % (current, total, total/current)sarebbe molto meno leggibile in forma di concatenazione.

  • La concatenazione è utile quando non si dispone di un numero fisso di elementi da stringere.

Inoltre, sappi che Python 2.6 introduce una nuova versione dell'interpolazione di stringhe, chiamata string templating :

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

Il templating delle stringhe è destinato a sostituire alla fine% -interpolation, ma penso che non accadrà per un bel po '.


Bene, accadrà ogni volta che decidi di passare a Python 3.0. Inoltre, vedere il commento di Peter per il fatto che è comunque possibile eseguire sostituzioni con nome con l'operatore%.
John Fouhy

"La concatenazione è utile quando non si dispone di un numero fisso di elementi da stringere." - Intendi una lista / matrice? In tal caso, non potresti semplicemente unirti a loro ()?
strager

"Non potresti semplicemente unirti a loro ()?" - Sì (supponendo che tu voglia separatori uniformi tra gli articoli). Le comprensioni di elenchi e generatori funzionano alla grande con string.join.
Tim Lesher

1
"Beh, accadrà ogni volta che decidi di passare a python 3.0" - No, py3k supporta ancora l'operatore%. Il prossimo possibile punto di deprecazione è 3.1, quindi ha ancora un po 'di vita in esso.
Tim Lesher

2
2 anni dopo ... Python 3.2 si sta avvicinando al rilascio e l'interpolazione% style va ancora bene.
Corey Goldberg

8

Stavo solo testando la velocità di diversi metodi di concatenazione / sostituzione di stringhe per curiosità. Una ricerca su Google sull'argomento mi ha portato qui. Ho pensato di pubblicare i risultati dei miei test nella speranza che potesse aiutare qualcuno a decidere.

    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)

... Dopo la corsa runtests((percent_, format_, format2_, concat_), runs=5), ho scoperto che il metodo% era circa il doppio più veloce degli altri su queste corde piccole. Il metodo concatenato era sempre il più lento (a malapena). C'erano differenze format()minime quando si cambiavano le posizioni nel metodo, ma il cambio di posizione era sempre almeno 0,01 più lento del normale metodo di formattazione.

Campione dei risultati del 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)

Li ho eseguiti perché utilizzo la concatenazione di stringhe nei miei script e mi chiedevo quale fosse il costo. Li ho eseguiti in ordini diversi per assicurarmi che nulla interferisse o che ottenesse prestazioni migliori essendo il primo o l'ultimo. Come nota a margine, ho inserito alcuni generatori di stringhe più lunghi in quelle funzioni come "%s" + ("a" * 1024)e il normale concatenamento era quasi 3 volte più veloce (1.1 vs 2.8) rispetto all'utilizzo dei metodi formate %. Immagino che dipenda dalle corde e da cosa stai cercando di ottenere. Se le prestazioni sono davvero importanti, potrebbe essere meglio provare cose diverse e testarle. Tendo a scegliere la leggibilità rispetto alla velocità, a meno che la velocità non diventi un problema, ma sono solo io. Quindi non mi è piaciuto il mio copia / incolla, ho dovuto mettere 8 spazi su tutto per farlo sembrare giusto. Di solito uso 4.


1
Dovresti considerare seriamente ciò che stai profilando come. Per uno il tuo concat è lento perché hai due cast di str. Con le stringhe il risultato è l'opposto, poiché il concatenamento di stringhe è effettivamente più veloce di tutte le alternative quando sono interessate solo tre stringhe.
Justus Wingert

@ JustusWingert, questo ha due anni ormai. Ho imparato molto da quando ho pubblicato questo "test". Onestamente, in questi giorni uso str.format()e str.join()oltre la normale concatenazione. Sto anche tenendo d'occhio le "f-string" di PEP 498 , che è stato recentemente accettato. Per quanto riguarda le str()chiamate che influenzano le prestazioni, sono sicuro che hai ragione su questo. Non avevo idea di quanto costose fossero le chiamate di funzione in quel momento. Continuo a pensare che i test dovrebbero essere fatti quando c'è qualche dubbio.
Cj Welborn

Dopo un rapido test con join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), sembra che joinsia anche più lento della percentuale.
gaborous l'

4

Ricorda, le decisioni stilistiche sono decisioni pratiche, se hai intenzione di mantenere o eseguire il debug del tuo codice :-) C'è una famosa citazione di Knuth (forse citando Hoare?): "Dovremmo dimenticare le piccole efficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali ".

Fintanto che stai attento a non (diciamo) trasformare un'attività O (n) in un'attività O (n 2 ), andrei con quella che trovi più facile da capire ..


0

Uso la sostituzione ovunque posso. Uso la concatenazione solo se sto costruendo una stringa in un ciclo for.


7
"costruire una stringa in un ciclo for" - spesso questo è un caso in cui è possibile utilizzare ".join e un'espressione generatore ..
John Fouhy

-1

In realtà la cosa corretta da fare, in questo caso (costruire percorsi) è usare os.path.join. Non concatenazione o interpolazione di stringhe


1
questo è vero per i percorsi del sistema operativo (come nel tuo filesystem) ma non quando si costruisce un URI come in questo esempio. Gli URI hanno sempre "/" come separatore.
Andre Blum
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.