Macro for decorator (wrapper) in Python

Greetings,

What I’m trying to do can be expressed as a Python code like this:

def is_convex_repr(is_convex):
    def wrapper(func):
        func.is_convex = is_convex
        return func
    return wrapper
    @is_convex_repr(False)
    def my_obj_path(x, u):
        return 0.5*(x.T@x + u.T@u)

Then, f = my_obj_path will immediately give me that f.is_convex = False.

I found that macro in Julia can be used to do the same thing, but I’m not sure how to do it exactly with macro. Can anybody help me?

The cleanest thing is probably to use a “trait”-like approach, you don’t even need a macro:

my_obj_path(x, y) = (x+y)/2

is_convex(::Any) = false
is_convex(::typeof(my_obj_path)) = true

is_convex(my_obj_path) # returns true
is_convex(some_other_function) # returns false

A cool thing is that since the trait is now known at compile-time, some extra optimizations may happen, eg branches that depend on is_convex will be optimized away entirely:

foo(func) = is_convex(func) ? 100 : 200

julia> @code_llvm foo(my_obj_path)

;  @ REPL[4]:1 within `foo'
define i64 @julia_foo_2435() {
top:
  ret i64 100
}

You could then also write a macro that rewrites

@is_convex_repr true my_obj_path(x, y) = (x+y)/2

into something like the above code, if that’s the syntax you wanted.

1 Like

this is dangerous right? I can define a function that operates on different types and is not convex.

Yea, here convexity would be defined per-function rather than per-method (which is how I took the original question, since Python doesn’t have methods).

@marius311 Thanks for your kind explanation, but in my example code, is_convex_repr does not act to determine whether a function is convex or not. Instead, it is more close to add a method like

@is_convex_repr(False)
def my_obj_path(x, u):
    return 0.5*(x.T@x + u.T@u)

my_obj_path.is_convex == False  # true

In my case, I have a bunch of functions and wanna initialise each function easily by decorating them (if I were using Python).

Sorry should have called it just is_convex to more closely match your original question, I edited my post to reflect that. The only difference is that its a function is_convex(my_obj_path) instead of a property, my_obj_path.is_convex. If this isn’t what you’re asking for then I may be missing something.

exactly. hence it’s safe to do it in python but not so in Julia

you assign a value to something os you can detect whether it’s convex right? Not to store value in the function, that’s not your aim.

This would be kinda weird to do in Julia but is kinda what u are looking for


struct IsConvexRepr{T}
    fn
    arg_types::T
    is_convex::Bool
end

function (icr::IsConvexRepr)(args...)
    @assert all(typeof.(args) .== icr.arg_types)
    icr.fn(args...)
end

using MacroTools
macro is_convex_repr(b, ex)
    @capture(ex, fn_(args__))
    :(
        IsConvexRepr($fn, typeof.($args), $b)
    )
end


abc(x,y) = x+y

# you can define if it's convex by supplying an example of what to 
# after all a method in julia is completely determined by function name and argument types
abc1 = @is_convex_repr false abc(1, 1)
abc2 = @is_convex_repr true abc(1.0, 1.0)

abc1(1, 1)
abc1(1.0, 1.0) # will error cos types don't match


abc2(1.0, 1.0)
abc2(1, 1) #  will error cos types don't match

is_convex(x) = x.is_convex

is_convex(abc1) # false
abc1.is_convex # false

is_convex(abc2) # true
abc2.is_convex # true

A decorator in Python is just syntactic sugar for a higher-order function.
Example:

def dec(f):
       def inner(*args, **kwargs):
           print("hello!")
           return f(*args, **kwargs)
       return inner

@dec
def f(x):
    return 3*x

# is equivalent to 
def f(x):
    return 3*x
f = dec(f)

Julia has also higher-order functions, thus the same behavior can be archieved, too.

What is not possible in Julia is to give functions “object properties” from outside, like you did in Python:

func.is_convex = is_convex

However, I think this is quite an unusual way to use the Python object system. An alternative approach in Python would be to use a class here:

class FunctionWithProperty:
    def __init__(self, f, is_callable):
        self.f = f
        self.is_callable = is_callable
    def __call__(self, x):
        return self.f(x)

In Julia, the same could be archived with a Struct and a functor - see https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects.