Problem

W celach pedagogicznych chciałbym liczyć, ile razy dana linia jest wykonywana w danej funkcji bez modyfikowania lub dekoracji go . Na przykład dla funkcji:

def binary_search(seq, x):
    (a, b) = (0, len(seq) - 1)
    while a <= b:
        m = (a + b) / 2
        if x < seq[m]:
            b = m - 1
        elif x > seq[m]:
            a = m + 1
        else:
            return m

Właśnie piszę coś takiego:

print count_exec(binary_search, range(100), 44, line_number = 4) 

... a nawet taki:

print count_exec(binary_search(range(100), 44), line = "m = (a + b) / 2")

... Którego powinni drukować liczbę razy czwarta linia jest wykonywana (która wynosi 7). Ostatecznym celem jest zapewnienie empirycznego podejścia do złożoności dowolnej funkcji:

Complexity of binary search

Niestety

Moje obecne rozwiązanie polega na dodaniu atrybutu funkcji:

def binary_search(seq, x):
    binary_search.count = 0 # <------------------ added
    (a, b) = (0, len(seq) - 1)
    while a <= b:
        binary_search.count += 1 # <------------- added
        m = (a + b) / 2
        if x < seq[m]:
            b = m - 1
        elif x > seq[m]:
            a = m + 1
        else:
            return m

binary_search(range(100), 44)
print binary_search.count

Myślę, że mogę utworzyć zdobione funkcję count_this_line:

def binary_search(seq, x):
    (a, b) = (0, len(seq) - 1)
    while a <= b:
        count_this_line() # <-------------------- added
        m = (a + b) / 2
        ...

Może być możliwe, aby udekorować samą funkcję binary_search, ale dla mnie liczy się jako modyfikacja.

Pomysły

  • Standardowa biblioteka AST może pobrać abstrakcyjne drzewo składni o dowolnym skrypcie i nawet wykonaj go.
  • Mam małe doświadczenie w użyciu profilera Pythona. Wydaje się to dość ciężkie dla moich potrzeb. Czy może to być droga?
7
Aristide 13 sierpień 2014, 17:16

2 odpowiedzi

Najlepsza odpowiedź

Możesz użyć modułu line_profiler do zrobienia (Patrz Docs). Należy pamiętać, że musiałem uzyskać wersję zgodną 3.x z rozwidlony repo - nie jestem pewien, czy jest połączony jeszcze.

Na przykład. Umieściłem funkcję wyszukiwania binarnego w pliku, a następnie dodałem to:

prof = profile(binary_search)
prof(range(100), 44)

To jest to samo, co A @profile Decorator, jak wspomniano w Dokumentach, ale nie musisz modyfikować kodu oryginału . Pobiegłem

kernprof.py -l binsearch.py
python -m line_profiler binsearch.py.lprof

I wypłynęło to:

Function: binary_search at line 1
Total time: 4.1e-05 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def binary_search(seq, x):
     2         1            6      6.0     14.6      (a, b) = (0, len(seq) - 1)
     3         7            8      1.1     19.5      while a <= b:
     4         7            7      1.0     17.1          m = (a + b) // 2
     5         7            8      1.1     19.5          if x < seq[m]:
     6         2            2      1.0      4.9              b = m - 1
     7         5            5      1.0     12.2          elif x > seq[m]:
     8         4            4      1.0      9.8              a = m + 1
     9                                                   else:
    10         1            1      1.0      2.4              return m

"Hits" to numer, którego szukasz. Jako bonus otrzymujesz również informacje o czasie, choć byłoby bardziej dokładne z wieloma egzekucjami.

2
Jason S 15 sierpień 2014, 03:01

Po sugestii Jasona napisałem czyste rozwiązanie Pythona:

import line_profiler
import __builtin__
import cStringIO
import re

def profile(path, function_call, line_number):
    prof = line_profiler.LineProfiler()
    __builtin__.__dict__['profile'] = prof
    script = open(path).read()
    ns = locals()
    function_name = function_call[:function_call.index("(")]
    rex = re.compile("((?ms)^def %s.+)" % function_name)
    script = rex.sub(r"@profile\n\1\n%s" % function_call, script)
    exec(script, ns, ns)
    stream = cStringIO.StringIO()
    prof.print_stats(stream)
    s = stream.getvalue()
    stream.close()
    return int(re.search(r"(?m)^\s*%s\s*(\S*)" % (line_number+1), s).group(1))

if __name__ == "__main__":
    print profile("binary_search.py", "binary_search(range(100), 44)", 3)

Odczytuje Źródło skryptu zawierającego funkcję do profilu, zdobi to, dołącza żądane połączenie do końca, wykonuje go, zrzuca statystyki w ciąg, wyodrębnia liczbę trafień na dany numer linii i zwraca go jako int. Działa zgodnie z wymaganiami, ale z ważną karą wydajności.

Być może lepsze rozwiązanie polegało na upuszczeniu profilera, ale utrzymanie idei dekorowania i wykonania kodu źródłowego w locie. Edytuję moją odpowiedź, jeśli go implantuję.

W każdym razie, dziękuję Jasonowi, aby zapewnić mi wyjście!

2
Aristide 16 sierpień 2014, 20:51