Search code examples
pythonwrapperpython-decorators

Why wrapper and wrapped function are the same for some python codes.


I was reading Ian Goodfellow's GAN source code in Github (link https://github.com/goodfeli/adversarial/blob/master/deconv.py). In particular, at line 40/41, the code is:

@functools.wraps(Model.get_lr_scalers)
def get_lr_scalers(self):

It's a rather unfamiliar way of using wraps, and it seems the goal is to replace the get_lr_scalers with a user defined function. But in that case, we don't really need a wrapper for that, right? I don't really know the purpose of wraps in this case.


Solution

  • wraps copies a number of attributes from another function onto this function—by default, __module__, __name__, __qualname__, __annotations__ and __doc__.

    The most obviously useful one to copy over is the __doc__. Consider this simpler example:1

    class Base:
        def spam(self, breakfast):
            """spam(self, breakfast) -> breakfast with added spam
    
            <29 lines of detailed information here>
            """
    
    class Child:
        @functools.wraps(Base.spam)
        def spam(self, breakfast):
            newbreakfast = breakfast.copy()
            newbreakfast.meats['spam'] + 30
            return newbreakfast
    

    Now if someone wants to use help(mychild.spam), they'll get the 29 lines of useful information. (Or, if they autocomplete mychild.spam in PyCharm, it'll pop up the overlay with the documentation, etc.) All without me having to manually copy and paste it. And, even better, if Base came from some framework that I didn't write, and my user upgrades from 1.2.3 to 1.2.4 of that framework, and there's a better docstring, they'll see that better docstring.


    In the most common case, Child would be a subclass of Base, and spam would be an override.2 But that isn't actually required—wraps doesn't care whether you're subtyping via inheritance, or duck typing by just implementing an implicit protocol; it's equally useful for both cases. As long as Child is intended to implement the spam protocol from Base, it makes sense for Child.spam to have the same docstring (and maybe other metadata attributes).


    Others attributes probably aren't quite as useful as docstrings. For example, if you're using type annotations, their benefit in reading the code is probably at least as high as their benefit in being able to run Mypy for static type checking, so just copying them over dynamically from another method often isn't all that useful. And __module__ and __qualname__ are primarily used for reflection/inspection, and are more likely to be misleading than helpful in this case (although you could probably come up with an example of a framework where you'd want people to read the code in Base instead of the code in Child, that isn't true for the default obvious example). But, unless they're actively harmful, the readability cost of using @functools.wraps(Base.spam, assigned=('__doc__',)) instead of just the defaults may not be worth it.


    1. If you're using Python 2, change these classes to inherit from object; otherwise they'll be old-style classes, which just complicates things in an irrelevant way. If Python 3, there are no old-style classes, so this issue can't even arise.

    2. Or maybe a "virtual subclass" of an ABC, declared via a register call, or via a subclass hook.