5
\$\begingroup\$

A little while ago in The 2nd Monitor, me and @Phrancis were talking about Python and debugging, and the topic of function decorators came up with this message:

Ethan Bierlein: You could even build a debug decorator. Give me a sec.

So, I built a small, simple debugging decorator, which would just print out the arguments of any method it was applied to.


I decided to go a little further though, and build two separate decorators. One for regular methods, and one built specifically for class methods. Essentially, the regular method decorator takes a function, and will print out it's arguments, and keyword arguments if debug=True. The class method decorator does the same, except it also prints out the attributes of the class it's contained in as well.

I'm wondering a couple of things though:

  • Is this a "pythonic" way to debug the arguments of a function?
  • Is there any additional data that should be debugged, that I missed?
  • Is the extra ClassMethodDecorator really needed? Or is there a simpler way to support both with one decorator?
  • I've built this to be Python 3, and Python 2.7 compatible. Did I do this correctly?

debug.py

from pprint import pprint

class MethodDebug(object):
    """Debug a normal method.

    This decorator is used for debugging a normal method,
    with normal arguments, i.e, not printing out the data
    of the class it's contained in.

    Keyword arguments:
    debug -- Whether or not you want to debug the method.
    """
    def __init__(self, debug):
        self.debug = debug

    def __call__(self, function):
        def wrapper(*args, **kwargs):
            if self.debug:
                pprint(args)
                pprint(kwargs)
            return function(*args, **kwargs)
        return wrapper


class ClassMethodDebug(object):
    """Debug a class method.

    This decorator is used for debugging a class method,
    with normal arguments, and self. When using this
    decorator, the method will print out it's arguments
    and the attributes of the class it's contained in.

    Keyword arguments:
    debug -- Whether or not you want to debug the method.
    """
    def __init__(self, debug):
        self.debug = debug

    def __call__(self, function):
        def wrapper(function_self, *args, **kwargs):
            if self.debug:
                pprint(function_self.__dict__)
                pprint(args)
                pprint(kwargs)
            return function(function_self, *args, **kwargs)
        return wrapper

test.py

from debug import MethodDebug, ClassMethodDebug

@MethodDebug(debug=True)
def normal_method(a, b):
    return a * b

print(normal_method(10, 10))


class TestClass(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    @ClassMethodDebug(debug=True)
    def class_method(self, c):
        return self.a * self.b * c

a = TestClass(10, 10)
print(a.class_method(10))
\$\endgroup\$
6
\$\begingroup\$

To address your specific questions:

  • Is it Pythonic enough? I think so.

  • Is there any additional data that should be debugged? If I was using this for my debugging, I’d want a bit more information:

    • The name of the function that’s been called – knowing which args/kwargs were supplied to an unknown function is not that useful
    • The return value of that function
    • A trace statement when the function returns

    There should also be something to distinguish trace output from regular output. That makes it much easier for me to reconstruct the call flow afterwards. Here’s an example of the sort of output I mean:

    [trace] enter my_function {
    [trace]   args: (10, 10)
    [trace]   kwargs: {}
    hello world
    [trace] return 100 } exit my_function
    

    For extra brownie points, use the traceback module to indent a function and its arguments/return value to match their level in the call tree.

  • Do I need a separate function and method decorator? It seems like it would be possible to do. I haven’t tried it, but https://stackoverflow.com/q/19314405/1558022 seems like it might have a couple of approaches.

    If applied to a method, your function decorator will always print the repr() of the object. That might be enough in most cases – perhaps rather than defining a second decorator, you add an argument debug_object to your function decorator which additionally prints the object dict on demand.

  • Is it compatible with Python 2.7 and Python 3? As far as I can tell, yes.


Two other (minor) comments:

  • I would rename the decorators. The first is misleading, because it’s described as “method debug”, but actually gets applied to functions. More generally, I’d be inclined to violate naming principles and make these lowercase instead, so they don’t look out of place next to the decorated functions. I’d probably call them something like @internaltrace.

  • The docstring for “debug” is not very useful. A better docstring would tell me exactly what debugging means in this context – here, something like “print the arguments supplied to the function” would be better.

    I’d be inclined to rename this parameter to “trace”, but I think correcting the docstring is more important.


A couple of additional ideas that came to me this morning:

  • You might want to tie this into the logging library, and direct all trace output to a dedicated file (say inttrc.log). That isolates trace statements from regular prints without putting [trace] everywhere, and is easier to search through later.

    This will get you timestamps, which are worth including, but I forgot about yesterday.

  • Although pprint is nice, I think a one-line string might be better. You can always unpack it later if you need to, but having all the associated parts of args/kwargs on the same line will be easy for parsing later.

\$\endgroup\$
3
\$\begingroup\$

I didn't entirely understand how this was meant to work, so I dove right in and ran it through the interpreter, interactively using it.

normal_method(2, 6)
>>> (2, 6)
>>> {}
>>> 12

Immediately I have no idea what's going on. Why get an empty dictionary? What's (2, 6) and what's 12? You should be distinguishing inputs from one another and checking if there even are any. A rudimentary change might be:

if self.debug:
    if args:
        print ("[args]")
        pprint(args)

    if kwargs:
        print ("[kwargs]")
        pprint(kwargs)

That's not stylistically very nice but it's more revealing than just spitting out parameters with no indication of what they are.

Also I tried out passing debug=False and it just... does nothing. Why is this an option? It's either a redundant way of allowing you to remove the debugging function or it's a poorly explained distinction. The actual decorator should be removed if you don't want to debug, not just added but entirely disabled.

\$\endgroup\$
  • 1
    \$\begingroup\$ The reasoning for the debug variable is simple. Suppose someone has a large project, and they want to debug each individual function/method. They could easily declare a global DEBUG variable, or a command line argument, and pass it to each debug decorator, easily disabling/enabling debugging for their project. \$\endgroup\$ – Ethan Bierlein Sep 9 '15 at 21:31
  • \$\begingroup\$ @EthanBierlein That makes total sense. It's not clear from the code or sample usage though, worth explaining. \$\endgroup\$ – SuperBiasedMan Sep 9 '15 at 21:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.