How to bypass python function definition with decorator?
I would like to know if its possible to control Python function definition based on global settings (e.g. OS). Example:
@linux
def my_callback(*args, **kwargs):
print("Doing something @ Linux")
return
@windows
def my_callback(*args, **kwargs):
print("Doing something @ Windows")
return
Then, if someone is using Linux, the first definition of my_callback
will be used and the second will be silently ignored.
Its not about determining the OS, its about function definition / decorators.
If the goal is to have the same sort of effect in your code that #ifdef WINDOWS / #endif has.. here's a way to do it (I'm on a mac btw).
Simple Case, No Chaining
>>> def _ifdef_decorator_impl(plat, func, frame):
... if platform.system() == plat:
... return func
... elif func.__name__ in frame.f_locals:
... return frame.f_locals[func.__name__]
... else:
... def _not_implemented(*args, **kwargs):
... raise NotImplementedError(
... f"Function {func.__name__} is not defined "
... f"for platform {platform.system()}.")
... return _not_implemented
...
...
>>> def windows(func):
... return _ifdef_decorator_impl('Windows', func, sys._getframe().f_back)
...
>>> def macos(func):
... return _ifdef_decorator_impl('Darwin', func, sys._getframe().f_back)
So with this implementation you get same syntax you have in your question.
>>> @macos
... def zulu():
... print("world")
...
>>> @windows
... def zulu():
... print("hello")
...
>>> zulu()
world
>>>
What the code above is doing, essentially, is assigning zulu to zulu if the platform matches. If the platform doesn't match, it'll return zulu if it was previously defined. If it wasn't defined, it returns a placeholder function that raises an exception.
Decorators are conceptually easy to figure out if you keep in mind that
@mydecorator
def foo():
pass
is analogous to:
foo = mydecorator(foo)
Here's an implementation using a parameterized decorator:
>>> def ifdef(plat):
... frame = sys._getframe().f_back
... def _ifdef(func):
... return _ifdef_decorator_impl(plat, func, frame)
... return _ifdef
...
>>> @ifdef('Darwin')
... def ice9():
... print("nonsense")
Parameterized decorators are analogous to foo = mydecorator(param)(foo)
.
I've updated the answer quite a bit. In response to comments, I've expanded its original scope to include application to class methods and to cover functions defined in other modules. In this last update, I've been able to greatly reduce the complexity involved in determining if a function has already been defined.
[A little update here... I just couldn't put this down - it's been a fun exercise] I've been doing some more testing of this, and found it works generally on callables - not just ordinary functions; you could also decorate class declarations whether callable or not. And it supports inner functions of functions, so things like this are possible (although probably not good style - this is just test code):
>>> @macos
... class CallableClass:
...
... @macos
... def __call__(self):
... print("CallableClass.__call__() invoked.")
...
... @macos
... def func_with_inner(self):
... print("Defining inner function.")
...
... @macos
... def inner():
... print("Inner function defined for Darwin called.")
...
... @windows
... def inner():
... print("Inner function for Windows called.")
...
... inner()
...
... @macos
... class InnerClass:
...
... @macos
... def inner_class_function(self):
... print("Called inner_class_function() Mac.")
...
... @windows
... def inner_class_function(self):
... print("Called inner_class_function() for windows.")
The above demonstrates the basic mechanism of decorators, how to access the caller's scope, and how to simplify multiple decorators that have similar behavior by having an internal function containing the common algorithm defined.
Chaining Support
To support chaining these decorators indicating whether a function applies to more than one platform, the decorator could be implemented like so:
>>> class IfDefDecoratorPlaceholder:
... def __init__(self, func):
... self.__name__ = func.__name__
... self._func = func
...
... def __call__(self, *args, **kwargs):
... raise NotImplementedError(
... f"Function {self._func.__name__} is not defined for "
... f"platform {platform.system()}.")
...
>>> def _ifdef_decorator_impl(plat, func, frame):
... if platform.system() == plat:
... if type(func) == IfDefDecoratorPlaceholder:
... func = func._func
... frame.f_locals[func.__name__] = func
... return func
... elif func.__name__ in frame.f_locals:
... return frame.f_locals[func.__name__]
... elif type(func) == IfDefDecoratorPlaceholder:
... return func
... else:
... return IfDefDecoratorPlaceholder(func)
...
>>> def linux(func):
... return _ifdef_decorator_impl('Linux', func, sys._getframe().f_back)
That way you support chaining:
>>> @macos
... @linux
... def foo():
... print("works!")
...
>>> foo()
works!
The comments below don't really apply to this solution in its present state. They were made during the first iterations on finding a solution and no longer apply. For instance the statement, "Note that this only works if macos and windows are defined in the same module as zulu." (upvoted 4 times) applied to the earliest version, but has been addressed in the current version; which is the case for most of the statements below. It's curious that the comments that validated the current solution have been removed.
While @decorator
syntax looks nice, you get the exact same behaviour as desired with a simple if
.
linux = platform.system() == "Linux"
windows = platform.system() == "Windows"
macos = platform.system() == "Darwin"
if linux:
def my_callback(*args, **kwargs):
print("Doing something @ Linux")
return
if windows:
def my_callback(*args, **kwargs):
print("Doing something @ Windows")
return
If required, this also allows to easily enforce that some case did match.
if linux:
def my_callback(*args, **kwargs):
print("Doing something @ Linux")
return
elif windows:
def my_callback(*args, **kwargs):
print("Doing something @ Windows")
return
else:
raise NotImplementedError("This platform is not supported")