Demystifying decorators: They don't need to be cryptic

(thepythoncodingstack.com)

29 points | by rbanffy 2 days ago ago

16 comments

  • adammarples 2 days ago ago

    It's absolutely baffling that an article called demystifying decorators does not actually describe decorators at all, preferring to keep them a mystery. Decorators are nothing more than a special syntax for calling a function, the meat of which can easily be explained in a single paragraph including a supporting example.

  • notepad0x90 2 days ago ago

    outside of __init__, I thought it wasn't the pythonic way to directly invoke double underscore functions like closure? I recall reading that you should implement a class that overrides/implements such functions instead? I get why the author might be doing it this way to describe the topic of the post, but would calling __closure__() be acceptable in production code?

    • zahlman 14 hours ago ago

      First, `__closure__` is a plain data attribute, not a function. It isn't callable, and isn't being "called" here.

      Further, in the cases you're talking about, the functions specifically are methods which you can implement in your own class. But you aren't doing this in order to avoid calling them directly; rather, you're doing them to implement functionality for that class (i.e., there's some other bit of syntax already which will call it indirectly for you). And `__init__` isn't much of an exception; normally you only call it explicitly where necessary for subclassing (because the corresponding syntax would create a separate base class instance instead of initializing the current base).

      But the point here is to inspect an implementation detail, for pedagogical purposes. There isn't a better way to do it in this case, exactly because you aren't ordinarily supposed to care about that detail. There's no question about whether you'd inspect an object's `__closure__` directly in production code - because not only is there no alternative, but it would be extremely rare to have any reason to do so.

      • notepad0x90 2 hours ago ago

        The naming convention is what is throwing me off. "object.closure" I understand as an attribute/property being accessed. In this case, the naming style suggests there should be a wrapper like:

        def closure(self): return self.__closure__

        I'm just talking form, not function here. In other words, if it is supposed to be accessed directly by arbitrary external code (non-class functions), it shouldn't use the double underscore syntax? If any property can have that syntax, then the syntax loses its meaning?

  • tccuuchffhfu a day ago ago

    Jdjfm df

  • sltkr 2 days ago ago

    I'm not sure who this article is for, but I think it doesn't strike at the heart of the topic.

    Half of the article is devoted to closures, but closures aren't essential for decorators. And the __closure__ attribute is an implementation detail that is really irrelevant. (For comparison, JavaScript has closures just like Python, but it doesn't expose the closed-over variables explicitly the way Python does.)

    Decorators are simply higher order functions that are used to wrap functions. The syntax is a little funky, but all you need to know is that code like:

       @foo
       @bar
       def baz(args):
           return quux(args)
    
    Is essentially equivalent to:

       baz = foo(bar(lambda args: quux(args))
    
    ...i.e. decorators are functions that take a callable argument and return a new callable (which typically does something and then calls the argument function--or not, as the case may be).

    Then there are seemingly more complex expressions like:

        @foo(42, 'blub')
        def bar(..): ...
    
    This looks like a special kind of decorator that takes arguments, but it looks more complex than it really is. Just like `foo` was an expression referencing a function `foo(42, 'blub')` is just a regular Python function call expression. That function call should then itself return a function, which takes a function argument to wrap the function being decorated. Okay, I admit that sounds pretty complex when I write it out like that, but if you implement it, it's again pretty simple:

        def foo(i, s):
            def decorator(f):
                def invoke(*args, **kwargs):
                    print('decorator', i, s)
                    f(*args, **kwargs)
                    print('done')
            return invoke
        return decorator
    
        @foo(42, 'blub')
        def hello():
            print('Hello, world!')
    
        hello()
    
        # prints:
        #    decorator 42 blub
        #    Hello, world!
        #    done
    
    This is an extra level of indirection but fundamentally still the same principle as without any arguments.

    And yes, these examples use closures, which are very convenient when implementing decorators. But they aren't essential. It's perfectly possible to declare a decorator this way:

        class Invoker:
            def __init__(self, f):
                self.f = f
        
            def __call__(self):
                print('before call')
                self.f()
                print('after call')
        
        def decorator(f):
            return Invoker(f)
            
        @decorator
        def hello():
            print('Hello, world!')
    
        hello()
        # prints:
        #     before call
        #     Hello, world!
        #     after call
    
    It's the same thing but now there are no closures whatsoever involved.

    The key point in all these examples is that functions in Python are first-class objects that can be referenced by value, invoked dynamically, passed as arguments to functions, and returned from functions. Once you understand that, it's pretty clear that a decorator is simply a wrapper that takes a function argument and returns a new function to replace it, usually adding some behavior around the original function.

    • ojii 2 days ago ago

      One tiny correction:

      > decorators are functions that take a callable argument and return a new callable

      there's nothing forcing a decorator to return a callable. A decorator _could_ return anything it wants. I don't know why you would want that, but Python won't stop you.

      • backprojection 2 days ago ago

        They also don’t have to act on callables, see @dataclass for instance.

        • ojii 2 days ago ago

          Classes are callable.

    • zahlman 14 hours ago ago

      The reference I usually offer people is https://stackoverflow.com/questions/739654/ . Admittedly the encyclopedic length answer there, while historically highly praised, barely addresses the actual question. But there's no real way to ask a suitable question for that answer - it's just way too broadly scoped. It's just one of those artifacts of the old days of Stack Overflow. Just, you know, good luck finding it if you don't already know about it.

    • ninetyninenine 2 days ago ago

      I'm getting old. Something so obvious that I thought everybody knew is getting reiterated in a blogpost by a younger generation encountering it for the first time.

      • poincaredisk 2 days ago ago

        You're certainly getting older :). You're assuming that since the author writes about something obvious, they must belong to the "younger generation" and encountered it for the first time.

        Meanwhile, the author finished their PhD in 2004, and wrote 3 books about Python.

        • ninetyninenine 2 days ago ago

          Maybe not "by", but "for" a younger generation.

        • sltkr 2 days ago ago

          Which 3 books are you talking about? I can only find 1 on Amazon (and it has a grand total of 12 reviews, so... you know, not exactly a best seller).

          Frankly it's a bit suspicious how defensive you are of this author, and combined with the blatant downvoting of my toplevel comment, makes me think there is some astro-turfing going on in this thread.

    • dijksterhuis 2 days ago ago

      the thing that bothered me most reading through it was using decorators to mutate some global state with the `data` list variable.

      like… it… just… it felt wrong reading that in the examples. felt very `def func(kw=[])` adjacent. i can see some rare uses for it, but eh. i dunno.

      (also didn’t find the closure stuff that insightful, ended up skipping past that, but then i know decorators, so… maybe useful for someone else. i dunno.).

      • t-writescode 2 days ago ago

        My "proudest" use of decorators has been in adding metrics gathering for functions in python, so you'd get automated statsd (and later prometheus) metrics just by having @tracked (or whatever I had its name be - it's been like 7 years) on the function header.

        In a sense, that was mutating a global variable by including and tracking the metrics gathering. I imagine this person's early professional exposures to it and need to create their own also came from a similar situation, so "mutating global state" and closures sorta clicked for them.

        People learn things by coming to those things from many different entry points and for many different reasons. This is another one of those instances :)