2016-05-21 10 views
26

Ho scaricato una build alpha Python 3.6 dal repository Github Python e una delle mie nuove funzionalità preferite è la formattazione letterale delle stringhe. Può essere utilizzato in questo modo:Perché le stringhe in formato letterale sono così lente in Python 3.6 alpha? (ora fissato in 3.6 stabile)

>>> x = 2 
>>> f"x is {x}" 
"x is 2" 

Questo sembra fare la stessa cosa come si utilizza la funzione format su un'istanza str. Tuttavia, una cosa che ho notato è che questa formattazione letterale delle stringhe è in realtà molto lenta rispetto alla semplice chiamata format. Ecco cosa timeit dice su ogni metodo:

>>> x = 2 
>>> timeit.timeit(lambda: f"X is {x}") 
0.8658502227130764 
>>> timeit.timeit(lambda: "X is {}".format(x)) 
0.5500578542015617 

Se io uso una stringa come argomento timeit s', i miei risultati sono ancora mostrando il modello:

>>> timeit.timeit('x = 2; f"X is {x}"') 
0.5786435347381484 
>>> timeit.timeit('x = 2; "X is {}".format(x)') 
0.4145195760771685 

Come si può vedere, utilizzando format prende quasi metà del tempo. Mi aspetto che il metodo letterale sia più veloce perché è implicata meno sintassi. Cosa sta succedendo dietro le quinte che fa sì che il metodo letterale sia molto più lento?

+0

le stringhe di f sono dinamiche, quindi la stringa deve essere generata su ogni ciclo; mentre la stringa di formato è un valore letterale che viene creato prima dell'esecuzione del codice, quando viene convertito in bytecode. –

+0

@AlexHall Forse questo ha a che fare con il fatto che 'x' è assegnato a una variabile locale quando viene passato al metodo' format', ma deve essere trovato in 'globals' dalla' f "..." ' sintassi. – schwobaseggl

+1

@AlexHall: questo non è un bug. Esiste semplicemente una diversa implementazione sotto il cofano, dato che la stringa di formato deve essere analizzata in fase di compilazione, mentre 'str.format()' analizza gli slot in * runtime *. –

risposta

23

Nota: questa risposta è stata scritta per le versioni alfa Python 3.6. A new opcode added to 3.6.0b1 ha migliorato significativamente le prestazioni della stringa F.


Il f"..." sintassi effettivamente trasformata ad un'operazione str.join() sulle parti stringa letterale intorno alle {...} espressioni ei risultati delle espressioni stessi passati attraverso il metodo object.__format__() (passando qualsiasi specifica di formato :.. in). Si può vedere questo durante lo smontaggio:

>>> import dis 
>>> dis.dis(compile('f"X is {x}"', '', 'exec')) 
    1   0 LOAD_CONST    0 ('') 
       3 LOAD_ATTR    0 (join) 
       6 LOAD_CONST    1 ('X is ') 
       9 LOAD_NAME    1 (x) 
      12 FORMAT_VALUE    0 
      15 BUILD_LIST    2 
      18 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      21 POP_TOP 
      22 LOAD_CONST    2 (None) 
      25 RETURN_VALUE 
>>> dis.dis(compile('"X is {}".format(x)', '', 'exec')) 
    1   0 LOAD_CONST    0 ('X is {}') 
       3 LOAD_ATTR    0 (format) 
       6 LOAD_NAME    1 (x) 
       9 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      12 POP_TOP 
      13 LOAD_CONST    1 (None) 
      16 RETURN_VALUE 

Nota le BUILD_LIST e LOAD_ATTR .. (join) op-codici a quel risultato. Il nuovo FORMAT_VALUE occupa la parte superiore della pila più un valore di formato (analizzato in fase di compilazione) per combinarli in una chiamata object.__format__().

Così il vostro esempio, f"X is {x}", è tradotto in:

''.join(["X is ", x.__format__('')]) 

Si noti che questo richiede Python per creare un oggetto lista, e chiamare il metodo str.join().

Il str.format() chiamata è anche una chiamata di metodo, e dopo l'analisi c'è ancora una chiamata a x.__format__('') coinvolti, ma soprattutto, non c'è creazione lista coinvolti qui. È questa differenza che rende più veloce il metodo str.format().

Si noti che Python 3.6 è stato rilasciato solo come build alfa; questa implementazione può ancora facilmente cambiare. Vedere PEP 494 – Python 3.6 Release Schedule per l'orario, nonché Python issue #27078 (aperto in risposta a questa domanda) per una discussione su come migliorare ulteriormente le prestazioni dei valori letterali stringa formattati.

+0

Davvero una bella spiegazione, grazie! Non avevo idea che esistesse un metodo magico '__format__'. –

+0

Perché è esteso a ''' .join ([...])' e non alla concatenazione di stringhe? –

+3

@AlexHall: poiché la concatenazione di stringhe ha caratteristiche di prestazioni O (N^2). A + B + C deve prima creare una stringa per A + B, quindi copiare il risultato insieme a C in una nuova stringa. –

16

Prima di 3.6 beta 1, la stringa di formato f'x is {x}' è stata compilata all'equivalente di ''.join(['x is ', x.__format__('')]).Il codice risultante era inefficiente per diversi motivi:

  1. ha costruito una sequenza di frammenti di stringa ...
  2. ... e questa sequenza è una lista, non è una tupla! (è leggermente più veloce costruire tuple rispetto alle liste).
  3. è spinto una stringa vuota in pila
  4. sembrava il metodo join sulla stringa vuota
  5. è invocato __format__ oggetti Unicode anche nudi, per cui la __format__('') sarebbe sempre tornare self, o oggetti interi, per quale __format__('') come argomento restituito str(self).
  6. __format__ metodo non inserito.

Tuttavia, per una stringa più complessa e più a lungo, le stringhe formattate letterali sarebbero ancora state più veloce rispetto al corrispondente '...'.format(...) chiamata, in quanto per questi ultimi la stringa viene interpretata ogni volta che la stringa viene formattata.


questa domanda è stata la motivazione principale per issue 27078 chiedere un nuovo codice operativo bytecode Python per concatenare una stringa da frammenti. Serhiy Storchaka ha implementato questo nuovo opcode e lo ha fuso in CPython in modo che fosse disponibile in Python 3.6 sin dalla versione beta 1 (e quindi in Python 3.6.0 final).

Come risultato, le stringhe in formato letterale saranno molto più più veloci di string.format. Sono anche spesso molto più veloce rispetto alla formattazione vecchio stile in Python 3.6, se si sta solo interpolando str o int oggetti:

>>> timeit.timeit("x = 2; 'X is {}'.format(x)") 
0.32464265200542286 
>>> timeit.timeit("x = 2; 'X is %s' % x") 
0.2260766440012958 
>>> timeit.timeit("x = 2; f'X is {x}'") 
0.14437875000294298 

f'X is {x}' ora compila per

>>> dis.dis("f'X is {x}'") 
    1   0 LOAD_CONST    0 ('X is ') 
       2 LOAD_NAME    0 (x) 
       4 FORMAT_VALUE    0 
       6 BUILD_STRING    2 
       8 RETURN_VALUE 

Il nuovo BUILD_STRING , insieme a un'ottimizzazione nel codice FORMAT_VALUE elimina completamente le prime 5 delle 6 fonti di inefficienza. Il metodo __format__ non è ancora inserito, quindi richiede una ricerca del dizionario sulla classe e pertanto è più lento che chiamare __str__, ma è ora possibile evitare completamente una chiamata nei casi comuni di formattazione int o str istanze (non sottoclassi !) senza specificatori di formattazione.

0

Solo un aggiornamento notando che questo sembra essere risolto nella versione Python3.6.

>>> import dis 
>>> dis.dis(compile('f"X is {x}"', '', 'exec')) 
    1   0 LOAD_CONST    0 ('X is ') 
       2 LOAD_NAME    0 (x) 
       4 FORMAT_VALUE    0 
       6 BUILD_STRING    2 
       8 POP_TOP 
      10 LOAD_CONST    1 (None) 
      12 RETURN_VALUE 

>>> dis.dis(compile('"X is {}".format(x)', '', 'exec')) 
    1   0 LOAD_CONST    0 ('X is {}') 
       2 LOAD_ATTR    0 (format) 
       4 LOAD_NAME    1 (x) 
       6 CALL_FUNCTION   1 
       8 POP_TOP 
      10 LOAD_CONST    1 (None) 
      12 RETURN_VALUE 
+4

Sì, come già indicato nella risposta di Antti, che spiega in dettaglio cosa fa il nuovo codice operativo. –

Problemi correlati