Zero-to-Debugging în 15 minute

Nu vă dați seama de valoarea unui depanator până când nu sunteți blocați la o problemă greu de vizualizat. Dar odată ce declanșați un mediu de dezvoltare cu capacități decente de depanare, nu veți mai privi niciodată înapoi.

Doriți să știți unde vă aflați în executarea codului? Ce durează atât de mult? Doar întrerupeți-l și verificați.

Vă întrebați ce valoare este atribuită acelei variabile? Treci cu mouse-ul peste el.

Doriți să omiteți o grămadă de coduri și să continuați să rulați dintr-o altă secțiune? Du-te.

Uneori print(variable_name) nu este suficient pentru a vă face o idee despre ce se întâmplă cu proiectul dumneavoastră. Acesta este momentul în care un depanator bun vă poate ajuta să aflați lucrurile.

Python vă oferă deja un depanator încorporat sub formă de pdb (un instrument pentru linia de comandă). Dar, datorită minunatei comunități Python, există mai multe opțiuni care prezintă interfețe grafice. Și există o mulțime de medii integrate pentru dezvoltatori (IDE) care funcționează cu Python, cum ar fi PyCharm al JetBrain, Wingare’s WingIDE, și chiar Comunitatea Microsoft Visual Studio.

Dar nu sunteți aici pentru a auzi cum un depanator este mai bun decât altul sau care este mai frumos sau mai elegant. Sunteți aici pentru a afla cât de simplu este să scrieți un depanator Python care să pătrundă prin codul dvs. Asta vă oferă o privire asupra componentelor interne ale Python.

Vă voi arăta cum puteți construi unul și, făcând asta, zgâriați o mâncărime pe care o am de mult timp.

Acum să trecem la el.

Un manual rapid despre modul în care codul Python este organizat și procesat

Contrar credinței populare, Python este de fapt un limbaj compilat. Când executați codul, modulul dvs. este rulat printr-un compilator care scuipă bytecode care este stocat în cache ca .pyc sau __pycache__ fișiere. Bytecode-ul în sine este ceea ce ulterior este executat linie cu linie.

De fapt, codul CPython propriu-zis care rulează un program nu este altceva decât o instrucțiune gigantică de tip switch case care rulează în buclă. Este o afirmație if-else care analizează codul de byt al instrucțiunii, apoi o dispune pe baza a ceea ce intenționează să facă acea operație.

Instrucțiunile executabile ale codului secundar sunt menționate intern ca obiecte de cod, si dis și inspecta modulele sunt utilizate pentru a le produce sau interpreta. Acestea sunt structuri imuabile, care, deși menționate de alte obiecte – cum ar fi funcții – nu conțin ele însele referințe.

Puteți privi cu ușurință codul secundar care reprezintă orice sursă dată dis.dis(). Încercați doar cu o funcție sau o clasă aleatorie. Este un mic exercițiu îngrijit care vă va ajuta să vizualizați ce se întâmplă. Rezultatul va arăta cam așa:

>>> def sample(a, b):
...     x = a + b
...     y = x * 2
...     print('Sample: ' + str(y))
...
>>> import dis
>>> dis.dis(sample)
2       0 LOAD_FAST                0 (a)
        3 LOAD_FAST                1 (b)
        6 BINARY_ADD
        7 STORE_FAST               2 (x)
3      10 LOAD_FAST                2 (x)
       13 LOAD_CONST               1 (2)
       16 BINARY_MULTIPLY
       17 STORE_FAST               3 (y)
4      20 LOAD_GLOBAL              0 (print)
       23 LOAD_CONST               2 ('Sample: ')
       26 LOAD_GLOBAL              1 (str)
       29 LOAD_FAST                3 (y)
       32 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
       35 BINARY_ADD
       36 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
       39 POP_TOP
       40 LOAD_CONST               0 (None)
       43 RETURN_VALUE

Observați că fiecare linie din bytecode face referire la poziția respectivă în codul sursă din coloana din stânga și că nu este o relație de la unu la unu. Ar putea exista mai multe operații mai mici – s-ar putea spune chiar atomice – care să formeze o instrucțiune de nivel superior.

A obiect cadru în python este ceea ce reprezintă un cadru de execuție. Conține o referință la obiectul de cod care se execută în prezent, variabilele locale cu care rulează, numele globale (variabile) care sunt disponibile și referințe la orice cadre conexe (cum ar fi părintele care a generat-o).

Există mai multe detalii despre aceste obiecte de discutat aici, dar sperăm că acest lucru este suficient pentru a vă uda pofta de mâncare. Nu veți avea nevoie de mult mai mult în scopul depanatorului nostru, deși ar trebui să consultați secțiunea Diving Deeper pentru linkuri despre unde să priviți mai departe.

Introduceți modulul sys

Python oferă o serie de utilități în biblioteca sa standard prin intermediul sys modul. Nu numai că există lucruri de genul sys.path pentru a obține calea pythonului sau sys.platform pentru a găsi detalii despre sistemul de operare în care executați, dar există și sys.settrace() și sys.setprofile() pentru a ajuta la scrierea instrumentelor lingvistice.

Da, ai citit bine. Python are deja cârlige încorporate pentru a ajuta la analiza codului și la interacțiunea cu executarea programului. sys.settrace() funcția vă va permite să rulați un apel invers ori de câte ori execuția avansează la un nou obiect cadru și ne oferă o referință la acesta, care, la rândul său, oferă obiectul cod cu care lucrați.

Pentru un exemplu rapid despre cum arată acest lucru, să refolosim funcția de mai devreme:

def sample(a, b):
    x = a + b
    y = x * 2
    print('Sample: ' + str(y))

Presupunând că de fiecare dată când se execută un nou cadru, doriți un apel invers care tipărește obiectul codului și numărul liniei executându-l, îl puteți defini ca:

def trace_calls(frame, event, arg):
    if frame.f_code.co_name == "sample":
        print(frame.f_code)

Acum este pur și simplu o chestiune de setare a acestuia ca urmărire a apelului nostru:

sys.settrace(trace_calls)

Și executând eșantion (3,2) ar trebui să producă

$ python debugger.py
<code object sample at 0x0000000000B46C90, file “.test.py”, line 123>
Sample: 10

Aveți nevoie de declarația if pentru a filtra apelurile de funcții. În caz contrar, veți vedea o grămadă de lucruri care nu vă pasă, mai ales atunci când imprimați pe ecran. Incearca-l.

Obiectele de cod și cadru au câteva câmpuri pentru a descrie ceea ce reprezintă. Acestea includ lucruri precum fișierul care se execută, funcția, numele variabilelor, argumentele, numerele de linie și lista continuă. Acestea sunt fundamentale pentru executarea oricărui cod python și puteți accesa documentația lingvistică pentru mai multe detalii.

Ce se întâmplă dacă doriți să depanați fiecare linie?

Mecanismul de urmărire va seta apelurile ulterioare în funcție de valoarea returnată a primului apel invers. Revenind Nici unul înseamnă că ați terminat, în timp ce întoarceți o altă funcție o setează efectiv ca funcție de urmărire în cadrul respectiv.

Iată cum arată acest lucru:

5    def sample(a, b):
6        x = a + b
7        y = x * 2
8        print('Sample: ' + str(y))
9
10   def trace_calls(frame, event, arg):
11       if frame.f_code.co_name == "sample":
12           print(frame.f_code)
13           return trace_lines
14       return
15
16   def trace_lines(frame, event, arg):
17       print(frame.f_lineno)

Acum, dacă executați același cod ca înainte, îl puteți vedea tipărind numerele de linie pe măsură ce progresați prin el:

$ python .test.py
<code object sample at 0x00000000006D4DB0, file ".test.py", line 5>
6
7
8
Sample: 10
8

Punerea unei interfețe cu utilizatorul în fața ei

Folosind sofi modul python, puteți produce cu ușurință o aplicație web care interacționează direct cu codul nostru python.

Iată ce ați face:

  1. Afișați fișierul, numele funcției și numărul de linie în curs de executare.
  2. Afișați codul pentru cadrul curent cu un pointer care identifică linia.
  3. Afișați valoarea variabilelor locale.
  4. Oferiți execuție pas cu pas, ceea ce înseamnă că trebuie să blocați înainte de a executa o linie până când utilizatorul face clic pe un buton.
  5. Adăugați funcționalitate step-over.
  6. Adăugați un mecanism de eliminare.
  7. Furnizați o metodă de oprire a execuției.

Din perspectiva UI, # 1, # 2 și # 3 pot fi gestionate toate printr-un Bootstrap Panou unde # 1 este titlul și # 2 și # 3 fac parte din corpul învelit mămăligă etichete pentru a afișa spațierea corectă.

Deoarece interfața va bloca în esență așteptarea intrării utilizatorului, iar depanatorul așteaptă comenzile stop / go, este o idee bună să separați acele bucle de evenimente folosind vechiul nostru prieten multiprocesare. Apoi, puteți implementa una coadă pentru a trimite comenzi de depanare la un proces și la o coadă de aplicații diferită pentru actualizările UI în celălalt.

Prin cozile de procesare multiplă, este ușor să blocați depanatorul care așteaptă comenzile utilizatorului la trace_lines funcție folosind .obține() metodă.

Dacă se dă comanda pentru a trece la următoarea linie de cod (# 4), totul rămâne la fel, în timp ce ieșirea (# 6) va schimba valoarea de returnare înapoi la trace_calls funcție – eliminarea efectivă a apelurilor suplimentare către trace_lines – și oprire (# 7) va ridica o excepție personalizată care va întrerupe execuția.

# Block until you receive a debug command
cmd = trace_lines.debugq.get()
if cmd == 'step':
    # continue stepping through lines, return this callback
    return trace_lines
elif cmd == 'stop':
    # Stop execution
    raise StopExecution()
elif cmd == 'over':
    # step out or over code, so point to trace_calls
    return trace_calls
class StopExecution(Exception):
    """Custom exception used to abort code execution"""
    pass

Funcționalitatea step-over (# 5) este implementată la trace_calls nivel prin returnarea niciodată a apelului trace_lines.

cmd = trace_lines.debugq.get()
if cmd == 'step':
    return trace_lines
elif cmd == 'over':
    return

Da, am atașat obiectele de coadă ca proprietăți ale funcțiilor de urmărire pentru a simplifica trecerea lucrurilor în jur. Funcțiile fiind obiecte este o idee grozavă, deși nici nu ar trebui să abuzați de ea.

Acum este doar o chestiune de configurare a widgeturilor pentru afișarea datelor și a butoanelor pentru controlul fluxului.

Puteți extrage codul sursă din obiectul cod al cadrului de executare utilizând modulul de inspecție.

source = inspect.getsourcelines(frame.f_code)[0]

Acum este o chestiune de formatare linie cu linie în div și mămăligă etichete, adăugând un indicator de o culoare diferită la linia curentă (disponibil prin f_lineno și co_firstline) și lipirea asta într-un panou corpul widgetului, împreună cu reprezentarea în șir a localnicilor cadrului (care oricum este un dicționar simplu):

def formatsource(source, firstline, currentline):
    for index, item  in enumerate(source):
        # Create a div for each line to better control format
        div = Div()
        # Extremly simplified tab index check to add blank space
        if item[0:1] == 't' or item[0:1] == ' ':
            div.style="margin-left:15px;"
        # If this currently executing this line, add a red mark
        if index == lineno - firstlineno:
            div.addelement(Bold('> ', style="color:red"))
        # Add the formatted code to the div
        div.addelement(Sample(item.replace("n", "")))
        # Output the html that represents that div
        source[index] = str(div)
    return "".join(source)

Singurul lucru rămas de făcut este să înregistrați câteva apeluri de apel pentru evenimente pentru clicurile pe butoane care controlează fluxul de execuție prin adăugarea comenzilor respective la coada de depanare. Faceți acest lucru în interiorul unui sarcină handler de evenimente care se declanșează după finalizarea încărcării conținutului inițial

@asyncio.coroutine
def load(event):
    """Called when the initial html finishes loading"""
    # Start the debug process
    debugprocess.start()
    # Register click functions
    app.register('click', step, selector="#code-next-button")
    app.register('click', stop, selector="#code-stop-button")
    app.register('click', over, selector="#code-over-button")
    # Make sure the display updates
    yield from display()
@asyncio.coroutine
def step(event):
    debugq.put("step")
    # Make sure the display updates
    yield from display()
@asyncio.coroutine
def stop(event):
    debugq.put("stop")
@asyncio.coroutine
def over(event):
    debugq.put("over")

Cum ar arăta asta?

Cum sa hack impreuna un depanator grafic Python

Pentru o vizualizare a tuturor codurilor puse împreună, consultați proiectul sofi-debugger de pe GitHub:

tryexceptpass / sofi-debugger
Contribuiți la dezvoltarea sofi-debugger prin crearea unui cont pe GitHub.github.com

Câteva note despre ceea ce tocmai ai făcut

Funcțiile din sys modulul menționat aici este implementat în CPython și este posibil să nu fie disponibil în alte arome sau interpreți. Asigurați-vă că aveți în vedere acest lucru atunci când experimentați.

Acestea sunt, de asemenea, destinate în mod special utilizării cu depanatori, profileri sau instrumente similare de urmărire. Acest lucru înseamnă că nu ar trebui să vă deranjați ca parte a unui program normal sau puteți întâlni unele consecințe neintenționate, mai ales atunci când interacționați cu alte module care pot viza în mod specific aceste aceleași interfețe (cum ar fi depanatoarele reale).

Scufundări mai adânci

Pentru o scufundare mai profundă în structurile de limbaj Python, cadre, obiecte de cod și modulul dis, vă recomand cu insistență să rezervați ceva timp și să treceți prin prelegerile CPython Internals ale lui Phillip Guo (@pgbovine).


Dacă v-a plăcut articolul și doriți să citiți mai multe despre Python și practicile software, vă rugăm să vizitați tryexceptpass.org. Rămâneți informat cu cele mai recente conținuturi abonându-vă la lista de corespondență.