Reputation: 13
In Python sometimes I want to do something like (1)
if __debug__ and verbose: print "whatever"
If Python is run with -O, then I'd like for that whole piece of code to disappear, as it would if I just had (2)
if __debug__: print "whatever"
or even (3)
if __debug__:
if verbose: print foo
However, that doesn't seem to happen (see below). Is there a way I can get the run-time efficiency of #3 with compact code more like #1?
Here's how I tested that I'm not getting the efficient code I want:
#!/usr/bin/python2.7
from dis import dis
import sys
cmds = ["""
def func ():
if __debug__ and 1+1: sys.stdout.write('spam')""", """
def func():
if __debug__: sys.stdout.write('ham')""", """
def func():
__debug__ and sys.stdout.write('eggs')"""]
print "__debug__ is", __debug__, "\n\n\n"
for cmd in cmds:
print "*"*80, "\nSource of {}\n\ncompiles to:".format(cmd)
exec(cmd)
dis(func)
print "\n"*4
Running this gives
__debug__ is False
********************************************************************************
Source of
def func ():
if __debug__ and 1+1: sys.stdout.write('spam')
compiles to:
3 0 LOAD_GLOBAL 0 (__debug__)
3 POP_JUMP_IF_FALSE 31
6 LOAD_CONST 3 (2)
9 POP_JUMP_IF_FALSE 31
12 LOAD_GLOBAL 1 (sys)
15 LOAD_ATTR 2 (stdout)
18 LOAD_ATTR 3 (write)
21 LOAD_CONST 2 ('spam')
24 CALL_FUNCTION 1
27 POP_TOP
28 JUMP_FORWARD 0 (to 31)
>> 31 LOAD_CONST 0 (None)
34 RETURN_VALUE
********************************************************************************
Source of
def func():
if __debug__: sys.stdout.write('ham')
compiles to:
3 0 LOAD_CONST 0 (None)
3 RETURN_VALUE
********************************************************************************
Source of
def func():
__debug__ and sys.stdout.write('eggs')
compiles to:
3 0 LOAD_GLOBAL 0 (__debug__)
3 JUMP_IF_FALSE_OR_POP 21
6 LOAD_GLOBAL 1 (sys)
9 LOAD_ATTR 2 (stdout)
12 LOAD_ATTR 3 (write)
15 LOAD_CONST 1 ('eggs')
18 CALL_FUNCTION 1
>> 21 POP_TOP
22 LOAD_CONST 0 (None)
25 RETURN_VALUE
Upvotes: 1
Views: 4000
Reputation: 248
You can do this since Python 3.10+. As long as you keep the if condition from getting too complicated and put the check on __debug__
first, the newer python versions behave as you'd want it to. Here's a modified version of the original code:
from dis import dis
import sys
def func_debug_only(x):
if __debug__:
print('spam')
def func_debug_and_const(x):
if __debug__ and True:
print('spam')
def func_const_and_debug(x):
if True and __debug__: # Using const int instead of True is identical
print('spam')
def func_truth_and_debug(x):
if 1==1 and __debug__: # -O doesn't remove this
print('spam')
def func_debug_and_var(x):
if __debug__ and x: # -O removes this
print('spam')
def func_var_and_debug(x):
# -O doesn't remove this; This makes sense as using x in the if
# statement might trigger x.__bool__() or x.__len__() that might
# have unpredictable (to the interpreter side effects). Even if
# the if condition fails, order of condition evaluation would
# mean that bool(x) is evaluated.
if x and __debug__:
print('spam')
def func_debug_nested(x):
if __debug__:
if x:
print('spam')
def func_debug_nested2(x):
if x: # Doesn't completely go away; 3.12 generates slightly smaller code than 3.10
if __debug__:
print('spam')
print("__debug__ is", __debug__, "on version", sys.version_info)
for fn in [func_debug_only, func_debug_and_const, func_const_and_debug, func_truth_and_debug, func_debug_and_var, func_var_and_debug, func_debug_nested, func_debug_nested2]:
print('\n', fn.__name__)
dis(fn)
This gives you:
__debug__ is False on version sys.version_info(major=3, minor=10, micro=5, releaselevel='final', serial=0)
func_debug_only
5 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
func_debug_and_const
9 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
func_const_and_debug
13 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
func_truth_and_debug
17 0 LOAD_CONST 1 (1)
2 LOAD_CONST 1 (1)
4 COMPARE_OP 2 (==)
6 POP_JUMP_IF_FALSE 6 (to 12)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
>> 12 LOAD_CONST 0 (None)
14 RETURN_VALUE
func_debug_and_var
21 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
func_var_and_debug
25 0 LOAD_FAST 0 (x)
2 POP_JUMP_IF_FALSE 4 (to 8)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
>> 8 LOAD_CONST 0 (None)
10 RETURN_VALUE
func_debug_nested
29 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
func_debug_nested2
34 0 LOAD_FAST 0 (x)
2 POP_JUMP_IF_FALSE 4 (to 8)
35 4 LOAD_CONST 0 (None)
6 RETURN_VALUE
34 >> 8 LOAD_CONST 0 (None)
10 RETURN_VALUE
Upvotes: 1
Reputation: 1121634
No, you can't. Python's compiler is not nearly smart enough to detect in what cases it could remove the code block and if
statement.
Python would have to do a whole lot of logic inference otherwise. Compare:
if __debug__ or verbose:
with
if __debug__ and verbose:
for example. Python would have to detect the difference between these two expressions at compile time; one can be optimised away, the other cannot.
Note that the difference in runtime between code with and without if __debug__
statements is truly minute, everything else being equal. A small constant value test and jump is not anything to fuss about, really.
Upvotes: 5