How to bypass python function definition with decorator?
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")
I wrote my code before read other answers. After I finished my code, I found @Todd's code is the best answer. Anyway I post my answer because I felt fun while I was solving this problem. I learned new things thanks to this good question. The drawback of my code is that there exists overhead to retrieve dictionaries every time functions are called.
from collections import defaultdict
import inspect
import os
class PlatformFunction(object):
mod_funcs = defaultdict(dict)
@classmethod
def get_function(cls, mod, func_name):
return cls.mod_funcs[mod][func_name]
@classmethod
def set_function(cls, mod, func_name, func):
cls.mod_funcs[mod][func_name] = func
def linux(func):
frame_info = inspect.stack()[1]
mod = inspect.getmodule(frame_info.frame)
if os.environ['OS'] == 'linux':
PlatformFunction.set_function(mod, func.__name__, func)
def call(*args, **kwargs):
return PlatformFunction.get_function(mod, func.__name__)(*args,
**kwargs)
return call
def windows(func):
frame_info = inspect.stack()[1]
mod = inspect.getmodule(frame_info.frame)
if os.environ['OS'] == 'windows':
PlatformFunction.set_function(mod, func.__name__, func)
def call(*args, **kwargs):
return PlatformFunction.get_function(mod, func.__name__)(*args,
**kwargs)
return call
@linux
def myfunc(a, b):
print('linux', a, b)
@windows
def myfunc(a, b):
print('windows', a, b)
if __name__ == '__main__':
myfunc(1, 2)
The code below works by conditionally defining a decorated function based on the value of platform.system
. If platform.system
matches a chosen string, the function will be passed through as-is. But when platform.system
doesn't match up, and if no valid definition has been given yet, the function gets replaced by one that raises a NotImplemented
error.
I've only tested this code on Linux systems, so be sure to test it yourself before using it on a different platform.
import platform
from functools import wraps
from typing import Callable, Optional
def implement_for_os(os_name: str):
"""
Produce a decorator that defines a function only if the
platform returned by `platform.system` matches the given `os_name`.
Otherwise, replace the function with one that raises `NotImplementedError`.
"""
def decorator(previous_definition: Optional[Callable]):
def _decorator(func: Callable):
if previous_definition and hasattr(previous_definition, '_implemented_for_os'):
# This function was already implemented for this platform. Leave it unchanged.
return previous_definition
elif platform.system() == os_name:
# The current function is the correct impementation for this platform.
# Mark it as such, and return it unchanged.
func._implemented_for_os = True
return func
else:
# This function has not yet been implemented for the current platform
@wraps(func)
def _not_implemented(*args, **kwargs):
raise NotImplementedError(
f"The function {func.__name__} is not defined"
f" for the platform {platform.system()}"
)
return _not_implemented
return _decorator
return decorator
implement_linux = implement_for_os('Linux')
implement_windows = implement_for_os('Windows')
Note that implement_for_os
isn't a decorator itself. Its job is to build decorators when given a string matching the platform you want to the decorator to respond to.
A complete example looks like the following:
@implement_linux(None)
def some_function():
print('Linux')
@implement_windows(some_function)
def some_function():
print('Windows')
implement_other_platform = implement_for_os('OtherPlatform')
@implement_other_platform(some_function)
def some_function():
print('Other platform')