Regarding the result of “decoration”, there are two types of decorators. Those that wrap the decorated function and those that only register the function in some way, but leave it unchanged.
IMO the difference should be visible. Currently it isn’t:
from somepkg import deco1
@deco1
def function():
do_something()
Now it is impossible to say if a call of function() will execute some additional code or not.
A decorator is a “syntax sugar” for doing func=decorator(func) immediately after the defintion.
I’d like to discuss if it makes sense to you having also a non-assigning variation of the decorator syntax which guarantees that the original function remains untouched.
It seems to me that “will execute some additional code” is only one aspect of how the decorator might behave. For example, some decorators conditionally call the function be decorated. There won’t be a way to use new syntax to capture the full variety of what decorators might do.
To understand what a decorator does, you need to do the same thing you do for any function or class: read the documentation that explains the thing.
Just check the hints of the decorator. If it is some def deco(callable: T) -> T: … , that should indicate that we return the exact same object, (having a Union would return the exact same object or something else), and anything else would be ‘it could return the same object, don’t be 100% sure about that though’.
does not indicate that deco(callable) is callable, only that type(deco(callable)) is type(callable), i.e. T.
For instance, if you have a decorator @with_logging that adds logging to a function, the function’s type / signature (argument and return types) remain the same, but the function is changed (different identity and code) by adding the logging.
TypeVars (like T in this case) can be bound, and that means that I can say that Tmust be callable. Therefore deco(something_that_works_for_T)will return something callable (and more specifically, T’s Type itself).
In the case of decorators, “read the docs” sometimes turns into “read the source code” for the following information:
Is the decorator passing through the incoming callable, returning the identical callable object as is?
If the decorator wraps, does it wrap the passed-in callable nicely (are the __name__, __qualname__, etc. set properly?)
Does it expect a class? If so, exactly what class does it return? Can I expect isinstance and alike to still work properly afterwards?
There are many other issues to be aware of, and not all of this information can realistically be addressed in the current type system or explained in detail in the docs while being general. This is a consequence of decorators being a convenience shorthand for a call-and-reassign syntax: since functions can just about do anything, decorators can do anything as well. The concept of an API boundary doesn’t apply as cleanly in the case of decorators because the input domain is user-written outside of the decorator designer’s control. And that the decorator syntax looks like that of simple annotations in other languages certainly doesn’t help make its metaprogramming power apparent.
The workings of decorators is one of the few cases where I consider “please consult the course code” to be a satisfactory answer.
Decorators are a general purpose metaprogramming mechanism. Wrapping and registering are two use cases, but far from the only ones. Take @property for example. It sort of wraps the function, but does a whole lot more by returning a descriptor that is not directly callable that implements attribute access. Or @overload that registers it, but not in any way that is meaningful during execution and doesn’t wrap it but instead raises an exception. While both of these sort of wrap or sort of register, they demonstrate decorators do not cleanly fit into the two categories you are trying to draw a clear distinction between.
The dividing line is a very clear IMO. The decorator either replaces the original definition with a different object or not. Nothing in between.
If the decorator isn’t replacing the decorated function, we know for sure the code will be executed as we see it. My idea was to make this case visible in the code, i.e. that the decoration was applied without assignment. The purpose was to make code reading easier.
If the decorated function was replaced, we need to read the docs, what the decorator does. So, no change here. I find it irrelevant that this case can be further sub-divided into more categories.
Not mentioned yet is what you will do with this guarantee if you could get it? Is this for people, or type checkers, or something else?
We’ve covered a lot of nuances here, so I still believe there isn’t a strong line to divide decorators into two kinds. But even if we could, I don’t understand what would be useful about the division.