Specification
The decorator operator, as the name implies, enables the use of decorators inline. The syntax is as follows:
value @func_or_decorator
Comparing it with the traditional decorator, it works as follows:
@decorator
def func():
...
# Using decorator operator:
def func():
...
func = func @decorator # or func @= decorator
This diverges from the traditional decorator in that value
does not necessarily have to be a function (although it can be). Essentially, value @func_or_decorator
is equivalent to func_or_decorator(value)
.
The code of val = func(val)
(or val = val @func
) can be substituted with @=
. Special treatment using __imatmul__
is not necessary.
val = map(str, 10 @range)
val @= list # same with `val = list(val)`
print(val)
# output: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
Applications
When Multiple Functions Are Overlapped
Flat is better than nested. However, there are many cases where multiple functions are overlapped. In this case, using the decorator operator can be an intuitive alternative.
def joiner(delimiter: str):
return lambda iterable: delimiter.join(str(i) for i in iterable)
print(joiner("|")(list(range(10))))
# Using decorator operator:
10 @range @list @joiner("|") @print
# output: 0|1|2|3|4|5|6|7|8|9
When Argument Variable and Assigned Variable Are Overlapped
The decorator operator can also be used very effectively by applicating @=
when the var = func(var)
form is used.
# from https://discuss.python.org/t/apply-operator-f-x-means-f-x/32834/7
import numpy as np
arr = np.array(...)
arr = np.mean(arr)
arr = np.sqrt(arr)
arr = np.log(arr)
# Using decorator operator:
arr = np.array(...)
arr = arr @np.mean # same with arr = arr @np.mean
arr = arr @np.sqrt
arr = arr @np.log
# Using in-place decorator operator:
arr = np.array(...)
arr @= np.mean
arr @= np.sqrt
arr @= np.log
Complex Function Execution
In complex function execution, you can largely eliminate parentheses and reduce the chance of getting lost in the swamp of parentheses.
# from https://discuss.python.org/t/apply-operator-f-x-means-f-x/32834/19
result = collections.OrderedDict(
map(
lambda x: (x[0],x),
self.get_buy_operations_with_adjusted(sorted(items))
)
)
# Using decorator operator:
result = map(
lambda x: (x[0], x),
items @sorted @self.get_buy_operations_with_adjusted
) @collections.OrderedDict
Pipelining
# from https://discuss.python.org/t/apply-operator-f-x-means-f-x/32834/6
result = data
result @= filterpipe(lambda age: age >= 25)
result @= selectpipe("name", "score")
result @= arrangepipe("score", order="desc")
Decorating Functions Already Defined
You can also use it when the place where the function is declared and the place where the decorator can be applied are different, or when you want to apply the decorator only once.
# from https://stackoverflow.com/questions/28245450/statement-decorators
import requests
def retry(count: int):
def inner(f):
def wrapper(*args, **kwargs):
for i in range(count):
try:
return f(*args, **kwargs)
except Exception:
if i + 1 >= count:
raise
return wrapper
return inner
# post-define decorating
requests.get @= retry(3)
res = requests.get(unstable_source)
# instant decorating
res = (requests.get @retry(3))(unstable_source)
Broadcasting
In numpy.array
or pandas.Series
etc, broadcasting is used for operators. The disadvantage is that broadcasting does not apply to functions, but the decorator operator is an operator and can therefore be overloaded.
Callables and arrays are completely separate types, so this feature can be clearly distinguished from matrix multiplication.
import numpy as np
arr = np.array(10 @range @list)
print(arr - 1) # [-1 0 1 2 3 4 5 6 7 8] (broadcasted)
print(str(arr)) # [0 1 2 3 4 5 6 7 8 9] (Not broadcasted)
print(arr @str) # will be ['0' '1' '2' '3' '4' '5' '6' '7' '8' '9'], same with np.array([str(i) for i in arr])
Of course, to support this, agreement with the library developers is necessary.
Features
Very Easy to Implement
The entire implementation is as follows:
from typing import Callable
class DecoratorOperatorMeta(type):
__matmul__ = lambda cls, other: other(cls)
__rmatmul__ = lambda cls, other: cls(other)
class pipeline[T, U, V](metaclass=DecoratorOperatorMeta):
"""A decorator for implementing decorator operator on Python."""
def __init__(self, f: Callable[[U], T]) -> None:
self.func = f
def __call__(self, *args, **kwargs) -> T:
return self.func(*args, **kwargs)
def __matmul__(self, other: Callable[[Callable[[U], T]], V]) -> V:
"""__matmul__ is not needed if decorator operator is being default.
pipeline_func @no_pipeline_func
==> no_pipeline_func(pipeline_func)
"""
return other(self.func)
def __rmatmul__(self, other: U) -> T:
"""
argument @pipeline_func
==> pipeline_func(argument)
"""
return self.func(other)
It might seem complex, but essentially, only the following one line[1] needs to be added to the implementation of a callable.
__rmatmul__ = lambda self, other: self(other)
Python already has an implementation for the @
operator, so there is no need to change the parser, implement a new magic method, or even reorder the operators.
Intuitive Understanding
Being an extension of the decorator, if you understand decorators, you can readily grasp it.
Adequate Mixing with Regular Function Notation
The decorator operator can blend well with the regular function notation.
"|".join(map(str, list(range(100))))
# Using decorator operator
map(str, 10 @range @list) @"|".join @print
# output: 0|1|2|3|4|5|6|7|8|9
Traditional Decorator Will Be More Explainable
A decorator is a fairly advanced feature that requires a very good understanding of Pythonâs features, such as closures.
If the decorator operator is implemented, since it can be easily understood even by beginners who have just learned functions, it can reduce the psychological resistance or the barriers to understanding when learning the decorator declaration later.
@deco
def hello():
...
# above code is just syntactic sugar for following code:
def hello():
...
hello = hello @deco
Alter Implementation of Traditional Decorator
Itâs not a monumental alteration, as it fundamentally remains identical, however, the implementation of the traditional decorator needs to be changed from func = decorator(func)
to func @= decorator
.
Notation
The decorator operator can have two types of notation.
func @decorator
: operator and right operand are attached.
- It can help users who know about decorators to understand intuitively.
- It can be effectively distinguished from the case where it is used as matrix multiply.
- You need to explain why there is an exception in the style guideline to newcomers (disadvantage).
func @ decorator
: Operator and operand are uniformly spaced.
- Itâs the same usage as other general operators.
- Itâs consistent.
More Than One Way to Do It
This would be the biggest disadvantage of the decorator operator.
It does not offer anything that a regular function cannot do. Itâs a kind of syntactic sugar for func(val)
, just like traditional decorator.
However, PEP 584 explains as follows:
The âZen of Pythonâ merely expresses a preference for âonly one obvious wayâ:
There should be one-- and preferably only one --obvious way to do it.
The emphasis here is that there should be an obvious way to do âitâ.
I dare to think that according to the argument of PEP 584, the decorator operator can be implemented without violating the Zen of Python through a style guideline.
Style Guideline
Here is my proposed style guideline.
Decorator should be usedâŚ
- When it needs one positional parameter.
- When the output is meaningful (unlike
print
). However, some instances of pipelining are excluded.
Decorator operator should not be usedâŚ
- With
functools.partial
only for utilizing the decorator operator. - When the traditional decorator can be used.
Examples:
- All decorators When the traditional decorator is not applicable.
- Many iterable-based functions (list, iter, sum, max) and many others.
- Many inner functions (
operator.itemgetter
,operator.attrgetter
etc.)
Try It
You can experience the decorator operator by copying the code in the Very Easy to Implement
part and attaching callable @= pipeline
to the callable you will use (you can also use DecoratorOperatorMeta
in the case of classes). Donât forget to attach the pipeline decorator to the inner function in the case of inner functions.
str @= pipeline
range @= pipeline
len @= pipeline
print @= pipeline
list @= pipeline
repr @= pipeline
10 @range @list @repr @print
# output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
map(repr, 10 @range @list) @list
# output: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
More Considerations
The two syntaxes below are some features that I think could be useful when Python starts to focus on implementing functional programming features.
However, you can use the decorator operator effectively without having to implement the features below.
Syntactic Sugar for Partial Function
functools.partial(map, str)
map?(str)
Starred Decorator
map(*(str, range(100)))
(str, range(100)) @@map
Similar Disscussions
Apply operator ( f @ x means f(x) ) - Ideas - Discussions on Python.org
python - Statement decorators - Stack Overflow
Addition
Iâm sure this feature has a deep relationship with functional programming language concepts. However, Iâm not an expert in functional programming or pipelining. If thereâs anything you think I should refer to, please let me know.
Thank you
Of course, the actual implementation would indeed be more intricate âŠď¸