2016-11-21 23 views
9

Chciałem porównać różne zbudować ciąg w Pythonie od różnych zmiennych:Formatowanie ciągów w języku Python: czy '%' jest bardziej wydajne niż funkcja 'formatowania'?

  • użyciu + aby złączyć (dalej 'plus')
  • wykorzystaniem %
  • wykorzystaniem "".join(list)
  • użyciu format funkcja
  • przy użyciu "{0.<attribute>}".format(object)

I w porównaniu z 3 rodzajów scenari

  • łańcuchowych 2 zmiennych
  • łańcuch z 4 zmiennych
  • łańcuch z 4 zmienne, każdy dwukrotnego

że mierzy 1 milion działania każdego czasu i wykonał średnio ponad 6 pomiarów. Wpadłem na następujących taktowania:

Timings

W każdym scenariuszu, wpadłem następującego wniosku

  • Powiązanie wydaje się być jednym z najszybszych metod
  • Formatowanie przy użyciu % jest znacznie szybszy niż formatowanie z funkcją format

Wierzę, że format jest znacznie lepszy niż % (np. w this question) i % był prawie przestarzały.

Mam więc kilka pytań:

  1. Is % naprawdę szybciej niż format?
  2. Jeśli tak, dlaczego tak jest?
  3. Dlaczego numer "{} {}".format(var1, var2) jest bardziej wydajny niż "{0.attribute1} {0.attribute2}".format(object)?

Dla porównania, że ​​stosuje się następujący kod mierzyć różne czasy.

import time 
def timing(f, n, show, *args): 
    if show: print f.__name__ + ":\t", 
    r = range(n/10) 
    t1 = time.clock() 
    for i in r: 
     f(*args); f(*args); f(*args); f(*args); f(*args); f(*args); f(*args); f(*args); f(*args); f(*args) 
    t2 = time.clock() 
    timing = round(t2-t1, 3) 
    if show: print timing 
    return timing 


#Class 
class values(object): 
    def __init__(self, a, b, c="", d=""): 
     self.a = a 
     self.b = b 
     self.c = c 
     self.d = d 


def test_plus(a, b): 
    return a + "-" + b 

def test_percent(a, b): 
    return "%s-%s" % (a, b) 

def test_join(a, b): 
    return ''.join([a, '-', b]) 

def test_format(a, b): 
    return "{}-{}".format(a, b) 

def test_formatC(val): 
    return "{0.a}-{0.b}".format(val) 


def test_plus_long(a, b, c, d): 
    return a + "-" + b + "-" + c + "-" + d 

def test_percent_long(a, b, c, d): 
    return "%s-%s-%s-%s" % (a, b, c, d) 

def test_join_long(a, b, c, d): 
    return ''.join([a, '-', b, '-', c, '-', d]) 

def test_format_long(a, b, c, d): 
    return "{0}-{1}-{2}-{3}".format(a, b, c, d) 

def test_formatC_long(val): 
    return "{0.a}-{0.b}-{0.c}-{0.d}".format(val) 


def test_plus_long2(a, b, c, d): 
    return a + "-" + b + "-" + c + "-" + d + "-" + a + "-" + b + "-" + c + "-" + d 

def test_percent_long2(a, b, c, d): 
    return "%s-%s-%s-%s-%s-%s-%s-%s" % (a, b, c, d, a, b, c, d) 

def test_join_long2(a, b, c, d): 
    return ''.join([a, '-', b, '-', c, '-', d, '-', a, '-', b, '-', c, '-', d]) 

def test_format_long2(a, b, c, d): 
    return "{0}-{1}-{2}-{3}-{0}-{1}-{2}-{3}".format(a, b, c, d) 

def test_formatC_long2(val): 
    return "{0.a}-{0.b}-{0.c}-{0.d}-{0.a}-{0.b}-{0.c}-{0.d}".format(val) 


def test_plus_superlong(lst): 
    string = "" 
    for i in lst: 
     string += str(i) 
    return string 


def test_join_superlong(lst): 
    return "".join([str(i) for i in lst]) 


def mean(numbers): 
    return float(sum(numbers))/max(len(numbers), 1) 


nb_times = int(1e6) 
n = xrange(5) 
lst_numbers = xrange(1000) 
from collections import defaultdict 
metrics = defaultdict(list) 
list_functions = [ 
    test_plus, test_percent, test_join, test_format, test_formatC, 
    test_plus_long, test_percent_long, test_join_long, test_format_long, test_formatC_long, 
    test_plus_long2, test_percent_long2, test_join_long2, test_format_long2, test_formatC_long2, 
    # test_plus_superlong, test_join_superlong, 
] 
val = values("123", "456", "789", "0ab") 
for i in n: 
    for f in list_functions: 
     print ".", 
     name = f.__name__ 
     if "formatC" in name: 
      t = timing(f, nb_times, False, val) 
     elif '_long' in name: 
      t = timing(f, nb_times, False, "123", "456", "789", "0ab") 
     elif '_superlong' in name: 
      t = timing(f, nb_times, False, lst_numbers) 
     else: 
      t = timing(f, nb_times, False, "123", "456") 
     metrics[name].append(t) 

#Get Average 
print "\n===AVERAGE OF TIMINGS===" 
for f in list_functions: 
    name = f.__name__ 
    timings = metrics[name] 
    print "{:>20}:\t{:0.5f}".format(name, mean(timings)) 
+1

Użyj 'timeit' zamiast swojej własnej funkcji, może się zdarzyć, że pierwsze wykonanie będzie wolne, ale wykonanie następnej funkcji będzie szybsze, ale w rzeczywistości funkcja będzie wywoływana tylko raz. https://docs.python.org/2/library/timeit.html –

+0

Jak wspomniano w @MaximilianPeters, powinieneś używać 'timeit' do uzyskiwania godnych zaufania wyników. –

+0

Dzięki chłopaki. Sprawdziłem 'timeit', ale powinienem być na wysokim poziomie tego dnia, ponieważ wierzyłem, że jest obsługiwane tylko w Pythonie 3.x i używam głównie wersji 2.7. –

Odpowiedz

8
  1. Tak, % formatowanie ciąg jest szybsza niż metoda .format
  2. najbardziej prawdopodobne (może to mieć znacznie lepsze wyjaśnienie) ze względu na % będąc składniowe notacji (stąd szybka realizacja), natomiast .format polega co najmniej jedno dodatkowe wywołanie metody
  3. , ponieważ dostęp do wartości atrybutów obejmuje również dodatkowe wywołanie metody, mianowicie. __getattr__

uruchomiony nieco lepsze analizy (Pythona 3.6.0) za pomocą różnych sposobów timeit formatowania, których wyniki są następujące (dość wydrukowany BeautifulTable) -

 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| Type \ num_vars | 1 | 2 | 5 | 10 | 50 | 250 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| f_str_str | 0.306 | 0.064 | 0.106 | 0.183 | 0.737 | 3.422 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| f_str_int | 0.295 | 0.174 | 0.385 | 0.686 | 3.378 | 16.399 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| concat_str | 0.012 | 0.053 | 0.156 | 0.31 | 1.707 | 16.762 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| pct_s_str | 0.056 | 0.178 | 0.275 | 0.469 | 1.872 | 9.191 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| pct_s_int | 0.128 | 0.208 | 0.343 | 0.605 | 2.483 | 13.24 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| dot_format_str | 0.418 | 0.217 | 0.343 | 0.58 | 2.241 | 11.163 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| dot_format_int | 0.416 | 0.277 | 0.476 | 0.811 | 3.378 | 17.829 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| dot_format2_str | 0.433 | 0.242 | 0.416 | 0.675 | 3.152 | 16.783 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 
| dot_format2_int | 0.428 | 0.298 | 0.541 | 0.933 | 4.444 | 24.767 | 
+-----------------+-------+-------+-------+-------+-------+--------+ 

Kropka _str & _int oznacza, że ​​operacja została przeprowadzona dla odpowiednich typów wartości.

Moja konfiguracja dla przybywających na wyniki -

times = {} 

for num_vars in (1, 2, 5, 10, 50, 250): 
    f_str = "f'{" + '}{'.join([f'x{i}' for i in range(num_vars)]) + "}'" 
    # "f'{x0}{x1}" 
    concat = '+'.join([f'x{i}' for i in range(num_vars)]) 
    # 'x0+x1' 
    pct_s = '"' + '%s'*num_vars + '" % (' + ','.join([f'x{i}' for i in range(num_vars)]) + ')' 
    # '"%s%s" % (x0,x1)' 
    dot_format = '"' + '{}'*num_vars + '".format(' + ','.join([f'x{i}' for i in range(num_vars)]) + ')' 
    # '"{}{}".format(x0,x1)' 
    dot_format2 = '"{' + '}{'.join([f'{i}' for i in range(num_vars)]) + '}".format(' + ','.join([f'x{i}' for i in range(num_vars)]) + ')' 
    # '"{0}{1}".format(x0,x1)' 

    vars = ','.join([f'x{i}' for i in range(num_vars)]) 
    vals_str = tuple(map(str, range(num_vars))) 
    setup_str = f'{vars} = {vals_str}' 
    # "x0,x1 = ('0', '1')" 
    vals_int = tuple(range(num_vars)) 
    setup_int = f'{vars} = {vals_int}' 
    # 'x0,x1 = (0, 1)' 

    times[num_vars] = { 
     'f_str_str': timeit(f_str, setup_str), 
     'f_str_int': timeit(f_str, setup_int), 
     'concat_str': timeit(concat, setup_str), 
     # 'concat_int': timeit(concat, setup_int), # this will be summation, not concat 
     'pct_s_str': timeit(pct_s, setup_str), 
     'pct_s_int': timeit(pct_s, setup_int), 
     'dot_format_str': timeit(dot_format, setup_str), 
     'dot_format_int': timeit(dot_format, setup_int), 
     'dot_format2_str': timeit(dot_format2, setup_str), 
     'dot_format2_int': timeit(dot_format2, setup_int), 
    } 

table = BeautifulTable() 
table.column_headers = ['Type \ num_vars'] + list(map(str, times.keys())) 
# Order is preserved, so I didn't worry much 
for key in ('f_str_str', 'f_str_int', 'concat_str', 'pct_s_str', 'pct_s_int', 'dot_format_str', 'dot_format_int', 'dot_format2_str', 'dot_format2_int'): 
    table.append_row([key] + [times[num_vars][key] for num_vars in (1, 2, 5, 10, 50, 250)]) 
print(table) 

nie mogłem wyjść poza num_vars=250 powodu pewnych max argumentów (255) granicy z timeit.

tl; dr - Python ciąg wydajność formatowania: f-strings to najszybszy i bardziej elegancki, ale czasami (ze względu na niektóre implementation restrictions & będąc Py3.6 + tylko), może trzeba zastosować inne opcje formatowania, jak to konieczne.

+0

Świetna odpowiedź z doskonałym kodem. Dzięki! –

Powiązane problemy