Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Va debugger ton Python!

Va debugger ton Python!

Cette presentation vous explique les bases de Pdb ainsi que GDB, afin de debugger plus facilement vos scripts Python.

Stéphane Wirtel

September 27, 2017
Tweet

More Decks by Stéphane Wirtel

Other Decks in Programming

Transcript

  1. Que fait ce code ? # Example 1 def main():

    for i in range(1, 4): i /= 4 if i == 4: print('Salut PyConFR 2017') if __name__ == '__main__': main() 4 / 107
  2. Que fait ce code ? # Example 1 def main():

    for i in range(1, 4): i /= 4 if i == 4: print('Salut PyConFR 2017') if __name__ == '__main__': main() $ python example1.py $ 5 / 107
  3. La sacro-sainte fonction print() # Example 2 def main(): for

    i in range(1, 4): i /= 4 print(i) if i == 4: print('Salut PyConFR 2017') 7 / 107
  4. La sacro-sainte fonction print() # Example 2 def main(): for

    i in range(1, 4): i /= 4 print(i) if i == 4: print('Salut PyConFR 2017') $ python example2.py 0 0 0 $ 8 / 107
  5. Utilisons logging # Example 3 import logging def main(): logging.basicConfig(level=logging.INFO)

    logging.info('start program') logging.debug("before loop") for i in range(1, 4): i /= 4 logging.debug(i) logging.debug("after loop") if i == 4: print("Hello PyConFR 2017") logging.debug("Hello PyConFR 2017") logging.info('end program') if __name__ == '__main__': main() 10 / 107
  6. Utilisons logging # Example 3 import logging def main(): logging.basicConfig(level=logging.INFO)

    logging.info('start program') logging.debug("before loop") for i in range(1, 4): i /= 4 logging.debug(i) logging.debug("after loop") if i == 4: print("Hello PyConFR 2017") logging.debug("Hello PyConFR 2017") logging.info('end program') if __name__ == '__main__': main() INFO:root:start program INFO:root:end program 11 / 107
  7. Utilisons logging # Example 4 import logging def main(): logging.basicConfig(level=logging.DEBUG)

    logging.info('start program') logging.debug("before loop") for i in range(1, 4): i /= 4 logging.debug(i) logging.debug("after loop") if i == 4: print("Hello PyConFR 2017") logging.debug("Hello PyConFR 2017") logging.info('end program') if __name__ == '__main__': main() 12 / 107
  8. Utilisons logging INFO:root:start program DEBUG:root:before loop DEBUG:root:0 DEBUG:root:0 DEBUG:root:0 DEBUG:root:after

    loop INFO:root:end program Obligé de modifier le niveau de log pour voir ce qu'il se passe. 14 / 107
  9. PDB Python Debugger est l'outil natif pour aider les développeurs

    à trouver et corriger les bugs de leur logiciel fétiche (ou celui que l'on récupère d'un ancien collègue ou prestataire...) 18 / 107
  10. Comment faire import pdb; pdb.set_trace() # Example 5 import pdb

    def main(): for i in range(1, 4): i /= 4 if i == 4: print("Hello PyConFR 2017") if __name__ == '__main__': pdb.set_trace() main() 21 / 107
  11. Comment faire import pdb; pdb.set_trace() # Example 5 import pdb

    def main(): for i in range(1, 4): i /= 4 if i == 4: print("Hello PyConFR 2017") if __name__ == '__main__': pdb.set_trace() main() $ python example5.py > example5.py(7)main() -> for i in range(1, 4): 22 / 107
  12. Comment faire Example 6 import pdb def do_something(): raise Exception("bug")

    def main(): try: do_something() except Exception: pdb.post_mortem() if __name__ == '__main__': main() 23 / 107
  13. Comment faire Example 6 import pdb def do_something(): raise Exception("bug")

    def main(): try: do_something() except Exception: pdb.post_mortem() if __name__ == '__main__': main() > example6.py(4)do_something() -> raise Exception("bug") (Pdb) bt example6.py(8)main() -> do_something() > example6.py(4)do_something() -> raise Exception("bug") (Pdb) ll 3 def do_something(): 4 -> raise Exception("bug") (Pdb) 24 / 107
  14. Comment faire On peut aussi charger pdb comme un module

    à la ligne de commande. $ python -m pdb <file.py> $ python -m pdb -c "command" <file.py> 25 / 107
  15. Prenons un exemple lisant un .csv import pendulum import csv

    class Event: def __init__(self, name, date): self.name = name self.date = pendulum.parse(date) def get_events(stream): for row in csv.reader(stream): yield Event(*row) def main(): with open('events.csv') as stream: for event in get_events(stream): print(event.name, event.date) if __name__ == '__main__': main() 27 / 107
  16. Prenons un exemple lisant un .csv import pendulum import csv

    class Event: def __init__(self, name, date): self.name = name self.date = pendulum.parse(date) def get_events(stream): for row in csv.reader(stream): yield Event(*row) def main(): with open('events.csv') as stream: for event in get_events(stream): print(event.name, event.date) if __name__ == '__main__': main() PyCon France,2017-09-21 PyCon Canada,2017-11-18 PyCon US,2017-05-17 Python FOSDEM,2018-02-03 Python TOTO,x 28 / 107
  17. Prenons un exemple lisant un .csv $ python events.py Non

    d'un petit Python, CRASH avec deux exceptions :/ PyCon France 2017-09-21T00:00:00+00:00 PyCon Canada 2017-11-18T00:00:00+00:00 PyCon US 2017-05-17T00:00:00+00:00 Python FOSDEM 2018-02-03T00:00:00+00:00 File "/home/stephane/.virtualenvs/flask/lib/python3.6/site-packages/dateutil/par raise ValueError("Unknown string format") ValueError: Unknown string format During handling of the above exception, another exception occurred: File "/home/stephane/.virtualenvs/flask/lib/python3.6/site-packages/pendulum/par raise ParserError('Invalid date string: {}'.format(text)) pendulum.parsing.exceptions.ParserError: Invalid date string: x 29 / 107
  18. Je liste mon code avec l ou ll (Pdb) ll

    1 -> import pendulum 2 import csv 3 4 class Event: 5 def __init__(self, name, date): 6 self.name = name 7 self.date = pendulum.parse(date) 8 9 def get_events(stream): 10 for row in csv.reader(stream): 11 yield Event(*row) 12 13 def main(): 14 with open('events.csv') as stream: 15 for event in get_events(stream): 16 print(event.name, event.date) 17 18 if __name__ == '__main__': 19 main() 31 / 107
  19. J'avance d'une ligne (Pdb) next > events.py(2)<module>() -> import csv

    (Pdb) ll 1 import pendulum 2 -> import csv 3 4 class Event: 5 def __init__(self, name, date): 6 self.name = name 7 self.date = pendulum.parse(date) 8 9 def get_events(stream): 10 for row in csv.reader(stream): 11 yield Event(*row) 12 13 def main(): 14 with open('events.csv') as stream: 15 for event in get_events(stream): 16 print(event.name, event.date) 17 18 if __name__ == '__main__': 19 main() 33 / 107
  20. On saute dans le code -> def main(): (Pdb) ll

    15 -> def main(): 16 with open('events.csv') as stream: 17 for event in get_events(stream): 18 print(event.name, event.date) 19 print(Event.id) (Pdb) step > events.py(16)main() -> with open('events.csv') as stream: (Pdb) ll 15 def main(): 16 -> with open('events.csv') as stream: 17 for event in get_events(stream): 18 print(event.name, event.date) 19 print(Event.id) 34 / 107
  21. On saute dans le code (Pdb) next > events.py(17)main() ->

    for event in get_events(stream): (Pdb) ll 15 def main(): 16 with open('events.csv') as stream: 17 -> for event in get_events(stream): 18 print(event.name, event.date) 19 print(Event.id) (Pdb) step --Call-- > events.py(11)get_events() -> def get_events(stream): (Pdb) ll 11 -> def get_events(stream): 12 for row in csv.reader(stream): 13 yield Event(*row) (Pdb) next > events.py(12)get_events() -> for row in csv.reader(stream): (Pdb) ll 11 def get_events(stream): 12 -> for row in csv.reader(stream): 13 yield Event(*row) (Pdb) 35 / 107
  22. Breakpoints (Pdb) ll 1 import pendulum 2 import csv 3

    4 class Event: 5 def __init__(self, name, date): 6 self.name = name 7 self.date = pendulum.parse(date) 8 9 def get_events(stream): 10 for row in csv.reader(stream): 11 yield Event(*row) 12 13 def main(): 14 with open('events.csv') as stream: 15 for event in get_events(stream): 16 print(event.name, event.date) 17 18 if __name__ == '__main__': 19 main() 37 / 107
  23. Breakpoints Ajouter > events.py(1)<module>() -> import pendulum (Pdb) break 9

    Breakpoint 1 at events.py:9 (Pdb) l 4 class Event: 5 id = 0 6 def __init__(self, name, date): 7 self.id += 1 8 self.name = name 9 B-> self.date = pendulum.parse(date) 10 11 def get_events(stream): 12 for row in csv.reader(stream): 13 yield Event(*row) 39 / 107
  24. Breakpoints Ajouter > events.py(1)<module>() -> import pendulum (Pdb) break 9

    Breakpoint 1 at events.py:9 (Pdb) l 4 class Event: 5 id = 0 6 def __init__(self, name, date): 7 self.id += 1 8 self.name = name 9 B-> self.date = pendulum.parse(date) 10 11 def get_events(stream): 12 for row in csv.reader(stream): 13 yield Event(*row) (Pdb) break Num Type Disp Enb Where 1 breakpoint keep yes at events.py:9 40 / 107
  25. Breakpoints Désactiver un breakpoint (Pdb) disable 1 Disabled breakpoint 1

    at events.py:9 (Pdb) break Num Type Disp Enb Where 1 breakpoint keep no at events.py:9 42 / 107
  26. Breakpoints Activer un breakpoint (Pdb) enable 1 Enabled breakpoint 1

    at events.py:9 (Pdb) break Num Type Disp Enb Where 1 breakpoint keep yes at events.py:9 44 / 107
  27. J'examine (Pdb) args self = <__main__.Event object at 0x7f23cf8cc908> name

    = 'PyCon France' date = '2017-09-21' (Pdb) whatis name <class 'str'> (Pdb) whatis self <class '__main__.Event'> 47 / 107
  28. Nouvelles commandes avec alias alias d pp dir(%1) (Pdb) b

    9 Breakpoint 1 at events.py:9 (Pdb) c > events.py(9)__init__() -> self.date = pendulum.parse(date) 49 / 107
  29. Nouvelles commandes avec alias alias d pp dir(%1) (Pdb) b

    9 Breakpoint 1 at events.py:9 (Pdb) c > events.py(9)__init__() -> self.date = pendulum.parse(date) (Pdb) d self.__dict__ ['__class__', '__contains__', '__delattr__', ... ] 50 / 107
  30. Nouvelles commandes avec alias alias printdict for key, value in

    %1.items(): print(f"{key}:{value}") 53 / 107
  31. Nouvelles commandes avec alias alias printdict for key, value in

    %1.items(): print(f"{key}:{value}") (Pdb) printdict self.__dict__ id:1 name:PyCon France date:2017-09-21T00:00:00+00:00 54 / 107
  32. Réutilisation d'alias alias pd printdict %1 (Pdb) pd self.__dict__ id:1

    name:PyCon France date:2017-09-21T00:00:00+00:00 56 / 107
  33. Breakpoint et une action Possibilité de définir une action lors

    d'un breakpoint commands [bpnumber] p self.__dict__ printdict self.__dict__ end 57 / 107
  34. Les importantes Commande Description h(elp) Affiche l'aide h(elp) command Affiche

    l'aide d'une commande q(uit) Quitte le debuggeur 60 / 107
  35. Examiner Commande Description p(rint) expr Affiche la valeur de l'expression

    pp expr Pretty-print de la valeur de l'expression l(ist) Affiche une partie du code ll Affiche toute le code de la fonction a(rgs) Affiche la liste des arguments de la fonction whatis arg Affiche le type de l'argument where Mais ou se trouve l'exception source Affiche la source d'une fonction 61 / 107
  36. Naviguer Commande Description n(ext) Exécute l'instruction s(tep) Exécute et rentre

    dans la fonction/méthode r(eturn) Continue l'exécution jusqu'au return c(ontinue) Continue l'exécution jusqu'à un breakpoint u(p) Remonte d'un niveau dans la stack trace d(own) Descend d'un niveau dans la stack trace j(ump) lineno Saute a la ligne 62 / 107
  37. Breakpoints Commande Description b(reak) lineno Place un breakpoint à lineno

    b(reak) lineno, cond Place un breakpoint à lineo si cond est True b(reak) file:lineno Place un breakpoint dans le fichier file à la ligne lineno b(reak) func Place un breakpoint à la première ligne de la fonction func tbreak lineno Place un breakpoint et l'enleve dès que l'on y est 64 / 107
  38. Breakpoints Commande Description clear bpnumber Supprime un breakpoint disable bpnumber

    Désactive un breakpoint enable bpnumber Active un breakpoint ignore bpnumber count Ignore le breakpoint X fois condition bpnumber condition Permet de modifer le breakpoint 65 / 107
  39. Celles que vous ne connaissez pas ;-) Commande Description !expr

    Execute une expression dans l'interpreteur Python alias Créer une nouvelle commande PDB unalias Supprime un alias commands [bpnumber] Assigne des commandes à un breakpoint restart or run Redémarre le script 66 / 107
  40. Fichier de con guration alias d pp dir(%1) alias ps

    d self.__dict__ alias loc !list(locals().keys()) alias printdict for key, value in %1.items(): print(f"{key}:{value}") Voir $HOME/.pdbrc ou ./.pdbrc 67 / 107
  41. Les alternatives - ipdb Auto-completion Coloration syntaxique Meilleurs tracebacks Meilleur

    introspection Commandes supplémentaires https://github.com/gotcha/ipdb 70 / 107
  42. Les alternatives - rdb import pdb, socket, sys # noqa

    --> Pas PEP8 class Rdb(pdb.Pdb): def __init__(self, port=4444): self.old_stdout = sys.stdout self.old_stdin = sys.stdin self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.skt.bind(('localhost', port)) self.skt.listen(1) (clientsocket, address) = self.skt.accept() handle = clientsocket.makefile('rw') pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle) sys.stdout = sys.stdin = handle def do_continue(self, arg): sys.stdout = self.old_stdout sys.stdin = self.old_stdin self.skt.close() self.set_continue() return 1 do_c = do_cont = do_continue https://dzone.com/articles/remote-debugging-python-using 72 / 107
  43. Les alternatives - rdb (2) # Example usage - connect

    with 'telnet 4444' if __name__=='__main__': def buggy_method(): x = 3 remote_debug = Rdb() remote_debug.set_trace() print(x) buggy_method() https://dzone.com/articles/remote-debugging-python-using 73 / 107
  44. Les alternatives - rdb (3) telnet localhost 4444 Trying 127.0.0.1...

    Connected to localhost. Escape character is '^]'. > rdb.py(32)buggy_method() -> print(x) (Pdb) ll 28 def buggy_method(): 29 x = 3 30 remote_debug = Rdb() 31 remote_debug.set_trace() 32 -> print(x) (Pdb) p x 3 (Pdb) quit Connection closed by foreign host. Meilleure implémentation dans https://github.com/celery/celery/blob/master/celery/contrib/rdb.py https://dzone.com/articles/remote-debugging-python-using 74 / 107
  45. pdb ou ipdb avec du dev Web Par exemple, Django

    django-pdb avec --ipdb ou --pdb ou simplement python -m pdb ./manage.py test python -m ipdb ./manage.py test python -m pudb ./manage.py test 77 / 107
  46. GNU Debugger Également nommé gdb, GNU Debugger, est le débuggeur

    standard du projet GNU, portable et fonctionne pour plusieurs langages, comme le C, le C++ et aussi le Python ;-) 78 / 107
  47. GNU Debugger Avoir les symboles de debug de Python $

    dnf install python3-debug 80 / 107
  48. GNU Debugger Avoir les symboles de debug de Python $

    dnf install python3-debug ou $ ./configure --prefix=$PWD-build --with-pydebug $ make $ make install 81 / 107
  49. GNU Debugger Python + gdb = ? 1. il y

    a une librairie pour debugger votre script Python depuis gdb 83 / 107
  50. GNU Debugger Python + gdb = ? 1. il y

    a une librairie pour debugger votre script Python depuis gdb 2. gdb est extensible via Python 84 / 107
  51. Que faire avec gdb ? Commande Description py-bt Montre la

    stack trace Python py-bt-full py-bt mais en plus verbeux py-list Liste le code Python py-print Affiche le contenu d'une variable Python py-locals Affiche les locals py-up Remonte d'une frame py-down Descend d'une frame 85 / 107
  52. Le code import time import os def next(i): print(f"{os.getpid()} {time.strftime('%H:%M:%S')}")

    time.sleep(10) i = 1 - i i = 1 while True: next(i) $ python ex_loop1.py 10619 06:14:24 10619 06:14:34 10619 06:14:44 10619 06:14:54 10619 06:15:04 10619 06:15:14 PID 10619 87 / 107
  53. Comment faire ? $ gdb -p 10619 GNU gdb (GDB)

    Fedora 8.0.1-26.fc26 ... For help, type "help". Type "apropos word" to search for commands related to "word". Attaching to process 10619 Reading symbols from /usr/bin/python3.6dm...Reading symbols from /usr/lib/debug/us Reading symbols from /lib64/libpython3.6dm.so.1.0...Reading symbols from /usr/lib/d Reading symbols from /lib64/libpthread.so.0...Reading symbols from /usr/lib/debug/ [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". Reading symbols from /lib64/libdl.so.2...Reading symbols from /usr/lib/debug/usr/li Reading symbols from /lib64/libutil.so.1...Reading symbols from /usr/lib/debug/usr Reading symbols from /lib64/libm.so.6...Reading symbols from /usr/lib/debug/usr/li Reading symbols from /lib64/libc.so.6...Reading symbols from /usr/lib/debug/usr/li Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/de 0x00007f40dc696ce3 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:84 84 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) (gdb) 88 / 107
  54. A cher le code (gdb) py-list 1 import time 2

    import os 3 4 def next(i): 5 print(f"{os.getpid()} {time.strftime('%H:%M:%S')}") >6 time.sleep(10) 7 i = 1 - i 8 9 i = 1 10 while True: 11 next(i) (gdb) 89 / 107
  55. A cher la traceback (gdb) py-bt Traceback (most recent call

    first): <built-in method sleep of module object at remote 0x7f40d590fd58> File "ex_loop1.py", line 6, in next time.sleep(10) File "ex_loop1.py", line 11, in <module> next(i) 90 / 107
  56. A cher la traceback avec plus d'infos (gdb) py-bt-full #3

    <built-in method sleep of module object at remote 0x7f40d590fd58> #7 Frame 0x55c890804748, for file ex_loop1.py, line 6, in next (i=1) time.sleep(10) #12 Frame 0x55c890804458, for file ex_loop1.py, line 11, in <module> () next(i) 91 / 107
  57. A cher la traceback avec plus d'infos (gdb) py-bt-full #3

    <built-in method sleep of module object at remote 0x7f40d590fd58> #7 Frame 0x55c890804748, for file ex_loop1.py, line 6, in next (i=1) time.sleep(10) #12 Frame 0x55c890804458, for file ex_loop1.py, line 11, in <module> () next(i) (gdb) select-frame 7 (gdb) info locals tstate = 0x55c8907a3cf0 (gdb) select-frame 3 (gdb) info locals func = 0x7f40d5974418 meth = 0x7f40dd57c6c6 <time_sleep> self = <module at remote 0x7f40d590fd58> result = 'sleep' flags = 8 __PRETTY_FUNCTION__ = "_PyCFunction_FastCallDict" 92 / 107
  58. Examiner une frame (gdb) py-bt-full #3 <built-in method sleep of

    module object at remote 0x7fc8e71bad58> #7 Frame 0x55706036f748, for file ex_loop1.py, line 6, in next (i=1) time.sleep(10) #12 Frame 0x55706036f458, for file ex_loop1.py, line 11, in <module> () next(i) (gdb) select-frame 7 (gdb) py-list 1 import time 2 import os 3 4 def next(i): 5 print(f"{os.getpid()} {time.strftime('%H:%M:%S')}") >6 time.sleep(10) 7 i = 1 - i 8 9 i = 1 10 while True: 11 next(i) (gdb) py-print i local 'i' = 1 (gdb) py-locals i = 1 93 / 107
  59. Navigation (gdb) py-down Unable to find a newer python frame

    (gdb) py-up #7 Frame 0x55c890804748, for file ex_loop1.py, line 6, in next (i=1) time.sleep(10) (gdb) py-down #3 <built-in method sleep of module object at remote 0x7f40d590fd58> 94 / 107
  60. Backtrace de CPython (gdb) bt #0 0x00007f40dc696ce3 in __select_nocancel ()

    at ../sysdeps/unix/syscall-template #1 0x00007f40dd57e7f8 in pysleep (secs=10000000000) at /usr/src/debug/Python-3.6.2/Modules/timemodule.c:1438 #2 0x00007f40dd57c73d in time_sleep (self=<module at remote 0x7f40d590fd58>, obj= at /usr/src/debug/Python-3.6.2/Modules/timemodule.c:235 #3 0x00007f40dd39a9af in _PyCFunction_FastCallDict ( func_obj=<built-in method sleep of module object at remote 0x7f40d590fd58>, args=0x55c8908048e0, nargs=1, kwargs=0x0) at /usr/src/debug/Python-3.6.2/Objects/methodobject.c:209 #4 0x00007f40dd39ada6 in _PyCFunction_FastCallKeywords ( func=<built-in method sleep of module object at remote 0x7f40d590fd58>, stack=0x55c8908048e0, nargs=1, kwnames=0x0) at /usr/src/debug/Python-3.6.2/Objects/methodobject.c:294 #5 0x00007f40dd4b046e in call_function (pp_stack=0x7ffd7bcab7b8, oparg=1, kwnames= at /usr/src/debug/Python-3.6.2/Python/ceval.c:4809 #6 0x00007f40dd4a8046 in _PyEval_EvalFrameDefault ( f=Frame 0x55c890804748, for file ex_loop1.py, line 6, in next (i=1), throwflag= at /usr/src/debug/Python-3.6.2/Python/ceval.c:3295 #7 0x00007f40dd494118 in PyEval_EvalFrameEx ( f=Frame 0x55c890804748, for file ex_loop1.py, line 6, in next (i=1), throwflag= at /usr/src/debug/Python-3.6.2/Python/ceval.c:718 95 / 107
  61. Threads (gdb) info threads Id Target Id Frame 1 Thread

    0x7f40ddaef700 (LWP 10619) "python3-debug" 0x00007f40dd39a9af in _PyC args=0x55c8908048e0, nargs=1, kwargs=0x0) at /usr/src/debug/Python-3.6.2/Objects/methodobject.c:209 96 / 107
  62. Dumper les stack traces des threads (gdb) thread apply all

    py-bt Thread 1 (Thread 0x7f40ddaef700 (LWP 10619)): Traceback (most recent call first): <built-in method sleep of module object at remote 0x7f40d590fd58> File "ex_loop1.py", line 6, in next time.sleep(10) File "ex_loop1.py", line 11, in <module> next(i) (gdb) 97 / 107
  63. Mode Remote - gdbserver $ gdbserver --multi host:2345 # #

    ou ces modes-ci # $ gdbserver host:2345 process_name $ gdbserver host:2345 --attach PID 99 / 107
  64. Mode Remote - gdbserver $ gdbserver --multi host:2345 # #

    ou ces modes-ci # $ gdbserver host:2345 process_name $ gdbserver host:2345 --attach PID $ gdb (gdb) target extended-remote host:2345 (gdb) attach PID 100 / 107
  65. Mode Remote - gdbserver $ gdbserver --multi host:2345 # #

    ou ces modes-ci # $ gdbserver host:2345 process_name $ gdbserver host:2345 --attach PID $ gdb (gdb) target extended-remote host:2345 (gdb) attach PID (gdb) py-list 1 import time 2 import os 3 4 def next(i): 5 print(f"{os.getpid()} {time.strftime('%H:%M:%S')}") >6 time.sleep(10) 7 i = 1 - i 8 9 i = 1 10 while True: 11 next(i) (gdb) 101 / 107
  66. Prochaines talks pour 2018 SystemTap et DTrace pour mon Python

    Que fait mon débuggeur Python? GDB en profondeur 106 / 107