未翻訳のページ
このページはまだ日本語に翻訳されていないため、英語の原文を表示しています。 翻訳に協力する
bdb Module Complexity¶
The bdb module is the machinery under pdb. Bdb installs a trace function, receives the
events it asks for, and decides at each one whether to stop and hand control to a subclass's
user_* method. A debugged program pays per event, so which events arrive, not the size of the
program, is what the cost turns on.
Breakpoints live in two class-level registries on Breakpoint, shared by every Bdb in the
process. Each Bdb also keeps, per file, a list of the lines holding a breakpoint.
e is the trace events delivered while the debugged code runs, d is frames on the stack, p
is skip patterns, b is lines holding a breakpoint in the file being checked, k is breakpoints
at one line, L is file:line locations holding a breakpoint across all files, B is breakpoint
numbers ever assigned, deleted ones included, and c is files in the linecache cache. File
names, module names, source lines and breakpoint conditions are priced at O(1); evaluating a
condition costs whatever its expression does, and a subclass's user_* methods cost whatever
they do. One-time cache fills are left out of the bounds: linecache reading a whole file the
first time a line of it is needed, canonic() resolving a new file name, and on 3.14+
break_anywhere() recording a code object's line numbers the first time it sees it. Bounds are
for the default 'settrace' backend.
Complexity Reference¶
Bdb¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bdb.Bdb(skip=None, backend='settrace') |
O(p + L·b) | O(p + L) | Copies every breakpoint location already registered, by any instance, checking each against its file's lines so far; backend is 3.14+ |
Bdb.run(cmd, globals=None, locals=None), Bdb.runeval(expr, globals=None, locals=None), Bdb.runctx(cmd, globals, locals) |
O(c + e·(p + b + k)) | O(c) | Plus the code's own cost; a string is compiled first, and BdbQuit ends the run quietly |
Bdb.runcall(func, /, *args, **kwds) |
O(c + e·(p + b + k)) | O(c) | Plus the call's own cost; returns what func returns |
Bdb.reset() |
O(c) | O(c) | Revalidates every file in linecache; the run* methods and set_trace() call it |
Bdb.set_trace(frame=None) |
O(c + d) | O(c + d) | Hooks every frame on the stack; 3.13+ stops at once and keeps each frame's trace flags until a set_continue() with no breakpoints set |
Bdb.start_trace(), Bdb.stop_trace() |
O(1) | O(1) | 3.14+; install or remove the trace function |
Bdb.canonic(filename) |
O(1) | O(1) | Cached per name in fncache; a name in angle brackets such as <string> comes back unchanged |
Bdb event dispatch¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Bdb.trace_dispatch(frame, event, arg) |
O(p + b + k) | O(1) | One event, handed to the matching dispatch_* method |
Bdb.dispatch_line(frame) |
O(p + b + k) | O(1) | The k term and the conditions evaluated come from effective(), on a line holding breakpoints |
Bdb.dispatch_call(frame, arg) |
O(p + b) | O(1) | Returns None for a frame that cannot stop, so its lines raise no events |
Bdb.dispatch_return(frame, arg), Bdb.dispatch_exception(frame, arg) |
O(p) | O(1) | |
Bdb.stop_here(frame), Bdb.is_skipped_module(module_name) |
O(p) | O(1) | One fnmatch per skip pattern, up to the first that matches |
Bdb.break_here(frame) |
O(b + k) | O(1) | Checks the line, then the function's first line for a breakpoint set by function name |
Bdb.break_anywhere(frame) |
O(b) | O(1) | 3.14+ looks for the file's breakpoint lines in the frame's code, caching each code object's lines on first use; earlier it is O(1), and any breakpoint in the file counts |
Bdb.user_call(frame, argument_list), Bdb.user_line(frame), Bdb.user_return(frame, return_value), Bdb.user_exception(frame, exc_info) |
O(1) | O(1) | Do nothing; a subclass overrides them, and their cost is added at every stop |
Bdb.do_clear(arg) |
O(1) | O(1) | Raises NotImplementedError; a subclass supplies it to delete a temporary breakpoint once hit |
Bdb.disable_current_event(), Bdb.restart_events() |
O(1) | O(1) | 3.14+; do nothing under the 'settrace' backend |
Bdb stepping¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Bdb.set_step(), Bdb.set_next(frame), Bdb.set_return(frame), Bdb.set_until(frame, lineno=None) |
O(1) | O(1) | Record where to stop next; on 3.13+ the first of these or set_continue() after set_trace() walks the stack, O(d), to end instruction stepping |
Bdb.set_continue() |
O(1) | O(1) | With no breakpoints set it removes tracing, O(d), and the code runs on untraced |
Bdb.set_quit() |
O(1) | O(1) | The dispatch that called it raises BdbQuit |
Bdb breakpoints¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Bdb.set_break(filename, lineno, temporary=False, cond=None, funcname=None) |
O(b + d·b) | O(1) | Returns an error string for a line that does not exist; during a stop, 3.12+ also runs break_anywhere() on every frame of the stack |
Bdb.clear_break(filename, lineno) |
O(b + k²) | O(k) | Each deletion shifts the rest of that line's list |
Bdb.clear_bpbynumber(arg) |
O(b + k) | O(1) | |
Bdb.clear_all_file_breaks(filename) |
O(b·k²) | O(1) | At a line holding several breakpoints, every second one survives in Breakpoint.bplist |
Bdb.clear_all_breaks() |
O(B·k) | O(1) | Walks every number ever assigned, deleted ones included |
Bdb.get_bpbynumber(arg) |
O(1) | O(1) | Raises ValueError for a non-numeric, unknown or deleted number |
Bdb.get_break(filename, lineno) |
O(b) | O(1) | |
Bdb.get_breaks(filename, lineno) |
O(b) | O(1) | The registry's own list, not a copy |
Bdb.get_file_breaks(filename) |
O(1) | O(1) | The instance's own list of lines, not a copy |
Bdb.get_all_breaks() |
O(1) | O(1) | The instance's own dict, not a copy |
Bdb stack inspection¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Bdb.get_stack(f, t) |
O(d + t) | O(d + t) | t = traceback entries; returns (frame, lineno) pairs and the index of f |
Bdb.format_stack_entry(frame_lineno, lprefix=': ') |
O(1) | O(1) | Plus reprlib.repr() of a return value: it shortens the text, but sorts a dict's or set's keys first |
Breakpoint¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bdb.Breakpoint(file, line, temporary=False, cond=None, funcname=None) |
O(1) | O(1) | Takes the next number and joins the class-level bpbynumber and bplist |
Breakpoint.deleteMe() |
O(k) | O(1) | Leaves None at its number: numbers are never reused |
Breakpoint.enable(), Breakpoint.disable() |
O(1) | O(1) | A disabled breakpoint is passed over without counting a hit |
Breakpoint.bpformat(), Breakpoint.bpprint(out=None) |
O(1) | O(1) | bpprint() writes bpformat() to out, sys.stdout by default |
Breakpoint.file, Breakpoint.line, Breakpoint.temporary, Breakpoint.cond, Breakpoint.funcname, Breakpoint.enabled, Breakpoint.ignore, Breakpoint.hits |
O(1) | O(1) | Plain attributes; effective() updates hits and ignore |
Breakpoint.bpbynumber, Breakpoint.bplist |
O(1) | O(1) | The shared registries: a list indexed by number and a dict of lists keyed by (file, line) |
Module functions and exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bdb.effective(file, line, frame) |
O(k) | O(1) | Plus each condition it evaluates; returns the first enabled breakpoint whose condition holds and whose ignore count is spent, or whose condition raises |
bdb.checkfuncname(b, frame) |
O(1) | O(1) | |
bdb.set_trace() |
O(c + d + L·b) | O(c + d + L) | Builds a Bdb and calls its set_trace() |
bdb.BdbQuit |
O(1) | O(1) | Raised to end a run; the run* methods catch it |
Writing a Debugger¶
A subclass overrides the user_* methods and, at each stop, calls one of the set_* methods to
say where to stop next. A run starts by stopping at the first line, so a set_step() at every
stop visits every line event.
import bdb
class Recorder(bdb.Bdb):
def __init__(self):
super().__init__()
self.stops = []
def user_line(self, frame): # called at every stop
self.stops.append(frame.f_lineno - work.__code__.co_firstlineno)
self.set_step() # O(1) - stop again at the next line
def work():
total = 0
for i in range(3):
total += i
return total
debugger = Recorder()
assert debugger.runcall(work) == 3 # O(c + e·(p + b + k)), plus work's own cost
assert set(debugger.stops) == {1, 2, 3, 4} # every line of work, some of them repeatedly
What Gets Traced¶
With the 'settrace' backend, every call raises a call event, and dispatch_call() returns
None for a frame that cannot stop, so that frame raises no line events. A frame already being
traced, such as the one a stop happened in, raises one per line executed, at O(p + b) each away
from breakpoints. set_continue() with no breakpoints set removes tracing altogether.
import bdb
class Counter(bdb.Bdb):
def __init__(self):
super().__init__()
self.line_events = 0
def dispatch_line(self, frame):
self.line_events += 1
return super().dispatch_line(frame)
def user_line(self, frame):
self.set_continue() # O(1) - run on to the next breakpoint
def loop(n):
total = 0
for i in range(n):
total += i
return total
def caller(n):
return loop(n)
def line_events(func, n, with_breakpoint):
bdb.Breakpoint.clearBreakpoints() # the registry is shared by every Bdb
debugger = Counter()
if with_breakpoint:
# A breakpoint in another file, in a function that never runs
line = bdb.effective.__code__.co_firstlineno + 1
assert debugger.set_break(bdb.__file__, line) is None
debugger.runcall(func, n)
return debugger.line_events
# No breakpoints: continuing removes tracing, so only the first stop counts
assert line_events(loop, 10_000, with_breakpoint=False) == 1
# A breakpoint elsewhere: loop() cannot stop, so its lines raise no events
assert line_events(caller, 10_000, with_breakpoint=True) == 1
# The frame the stop happened in keeps raising them, once per line: O(n)
assert line_events(loop, 10_000, with_breakpoint=True) > 10_000
On 3.13 and earlier, dispatch_call() treats any breakpoint in a frame's file as a reason to
trace it, so while running on to a breakpoint every function called in that file raises line
events. From 3.14 only a function whose code holds the breakpoint does, and Bdb(backend='monitoring') goes further: it can switch off a line
event that cannot stop, rather than receive it on every execution.
Skipping Modules¶
skip takes glob patterns for module names. Stepping never stops in a frame from a matching
module, but an event can pay O(p) to match the frame's module against the patterns.
import bdb
import json
class Stepper(bdb.Bdb):
def __init__(self, skip=None):
super().__init__(skip=skip) # O(p) to store the patterns
self.modules = set()
def user_line(self, frame):
self.modules.add(frame.f_globals.get('__name__'))
self.set_step()
def work():
return json.dumps([1, 2])
stepper = Stepper()
stepper.runcall(work)
assert 'json.encoder' in stepper.modules
skipping = Stepper(skip=['json', 'json.*'])
skipping.runcall(work)
assert skipping.modules == {'__main__'}
Managing Breakpoints¶
Each Bdb keeps its breakpoint lines in one list per file, so finding a line is O(b). The
Breakpoint objects themselves live in the shared registries: a Bdb built later sees every
location registered so far, and a deleted breakpoint leaves None at its number for good.
import bdb
bdb.Breakpoint.clearBreakpoints() # start from an empty registry
first = bdb.Bdb()
filename = first.canonic(bdb.__file__) # O(1) once cached
line = bdb.effective.__code__.co_firstlineno + 1
assert first.set_break(filename, line) is None # O(b) outside a stop
assert first.set_break(filename, line, cond='False') is None
assert len(first.get_breaks(filename, line)) == 2 # O(b)
assert first.get_file_breaks(filename) == [line] # O(1)
assert first.set_break(filename, 10**6) == f'Line {filename}:1000000 does not exist'
# A Bdb built later copies every registered location - O(L·b)
second = bdb.Bdb()
assert second.get_break(filename, line) # O(b)
# Deleting by number leaves None behind: numbers are never reused
assert first.clear_bpbynumber('1') is None # O(b + k)
assert bdb.Breakpoint.bpbynumber[1] is None
try:
first.get_bpbynumber('1') # O(1)
except ValueError as error:
assert 'already deleted' in str(error)
else:
raise AssertionError('a deleted breakpoint was found')
assert first.clear_all_breaks() is None # O(B·k), deleted numbers included
assert first.get_all_breaks() == {}
Choosing the Active Breakpoint¶
effective() walks the k breakpoints at a line in the order they were set. It passes over a
disabled one without counting a hit, and one set on a function name unless this is the function's
first line; every other one it reaches counts a hit. The first whose condition holds and whose
ignore count is spent is the one that stops, and so is one whose condition raises.
import bdb
import sys
def here():
return sys._getframe()
frame = here()
file, line = frame.f_code.co_filename, frame.f_lineno
bdb.Breakpoint.clearBreakpoints()
off = bdb.Breakpoint(file, line) # O(1)
off.disable()
never = bdb.Breakpoint(file, line, cond='False')
second_hit = bdb.Breakpoint(file, line)
second_hit.ignore = 1
assert bdb.effective(file, line, frame) == (None, None) # O(k), ignore count spent
assert bdb.effective(file, line, frame) == (second_hit, True)
assert (off.hits, never.hits, second_hit.hits) == (0, 2, 2)
Clearing a File's Breakpoints¶
clear_all_file_breaks() deletes breakpoints from a line's list while iterating over it, so at a
line holding several, every second one survives. The instance forgets the file anyway, but the
survivors stay in Breakpoint.bplist, and a Bdb built later picks them up again.
clear_break() iterates over a copy and removes them all.
import bdb
bdb.Breakpoint.clearBreakpoints()
debugger = bdb.Bdb()
filename = debugger.canonic(bdb.__file__)
line = bdb.effective.__code__.co_firstlineno + 1
for _ in range(4):
debugger.set_break(filename, line)
assert debugger.clear_all_file_breaks(filename) is None # O(b·k²)
assert debugger.get_file_breaks(filename) == []
assert len(bdb.Breakpoint.bplist[filename, line]) == 2
later = bdb.Bdb() # O(L·b) - sees the survivors
assert later.clear_break(filename, line) is None # O(b + k²)
assert (filename, line) not in bdb.Breakpoint.bplist
Inspecting the Stack¶
get_stack() walks the stack and then the traceback, collecting (frame, lineno) pairs.
format_stack_entry() formats one of them; a return value is cut short by reprlib, so a long
list costs no more than a short one.
import bdb
import sys
def depth(n):
return depth(n - 1) if n else sys._getframe()
def returns_big():
__return__ = list(range(100_000))
return sys._getframe()
debugger = bdb.Bdb()
debugger.reset() # O(c)
frame = depth(50)
stack, index = debugger.get_stack(frame, None) # O(d + t)
assert stack[-1][0] is frame and index == len(stack) - 1
assert len(stack) > 50
frame = returns_big()
entry = debugger.format_stack_entry((frame, frame.f_lineno)) # O(1)
assert '->[0, 1, 2, 3, 4, 5, ...]' in entry
Common Patterns¶
Logging a Conditional Breakpoint¶
A debugger that records a value each time a conditional breakpoint fires and runs on. Only the frame the run first stopped in is traced, so each of its lines costs O(p + b), and the breakpoint line adds O(k) and its condition.
import bdb
class HitLogger(bdb.Bdb):
def __init__(self):
super().__init__()
self.seen = []
def user_line(self, frame):
if self.get_break(frame.f_code.co_filename, frame.f_lineno): # O(b)
self.seen.append(frame.f_locals['i'])
self.set_continue() # O(1) while a breakpoint is set
def work(n):
total = 0
for i in range(n):
total += i
return total
bdb.Breakpoint.clearBreakpoints()
logger = HitLogger()
line = work.__code__.co_firstlineno + 3
assert logger.set_break(__file__, line, cond='i % 25 == 0') is None
assert logger.runcall(work, 100) == 4950
assert logger.seen == [0, 25, 50, 75]
Performance Best Practices¶
✅ Do:
- Call
set_continue()when nothing is left to stop at: with no breakpoints it removes tracing and the code runs at full speed - Put breakpoints in the functions you are debugging; on 3.13 and earlier, running on to one traces every function called in its file line by line
- Use
Bdb(backend='monitoring')on 3.14+ for long runs between stops - Clear breakpoints with
clear_break()orclear_all_breaks(), which remove every one at a line
❌ Avoid:
- Stepping with
set_step()through code you do not need to see; every line event outside a skipped module reachesuser_line() - Long
skiplists: an event matches the frame's module against every pattern until one fits clear_all_file_breaks()on a line holding several breakpoints; every second one survives
Version Notes¶
- Python 3.14+:
Bdb(backend='monitoring')runs onsys.monitoring;start_trace(),stop_trace(),disable_current_event()andrestart_events()added - Python 3.14+:
break_anywhere()looks for a breakpoint in the frame's own code, so while running on to a breakpoint only a called function holding one raises line events; earlier, every function in its file did - Python 3.13+:
set_trace()stops at once, on the next instruction, rather than on the next line - Python 3.12+:
set_break()during a stop re-checks every frame on the stack