Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Decorator (function which gets called prior to all other functions) #1798

Closed
phil-zxx opened this issue Jun 4, 2019 · 6 comments
Closed

Comments

@phil-zxx
Copy link
Contributor

phil-zxx commented Jun 4, 2019

This Python code adds a decorator to a class:

def decorate_all_methods():    
    def top_decorate(cls):    
        def sub_decorate(func):
            def deco_wrapper(*args, **kwargs):
                print('Function \'{}.{}\' was called'.format(cls.__name__, func.__name__))
                return func(*args, **kwargs)
            return deco_wrapper
        
        for attr in cls.__dict__:
            if callable(getattr(cls, attr)):
                setattr(cls, attr, sub_decorate(getattr(cls, attr)))
        return cls
    
    return top_decorate

@decorate_all_methods()   # <--- How to add a function like this in pybind11?
class MyClass(object):
    def __init__(self): pass
    def m1(self): pass


c = MyClass()   # prints: Function 'MyClass.__init__' was called
c.m1()          # prints: Function 'MyClass.m1' was called

Now my question is, is it possible to add such a decorator to a pybind class definition? Perhaps something like this?

const auto deco_func = []() { printf("function called ..."); };

py::class_<MyClass>(m, "MyClass", deco_func)
    .def(py::init<>())
    .def("m1", &MyClass::m1);

Any advice appreciated.

@eacousineau
Copy link
Contributor

eacousineau commented Jun 19, 2019

What I've done in Drake is just call the wrapping function manually.

In general, Python decorators like this:

def my_decorator(x):
    def wrapper(cls):
       return do_stuff(x, cls)
   return wrapper

@my_decorator(x)
class MyClass: ...

are equivalent to:

class MyClass: ....
MyClass = my_decorator(x)(MyClass)

So you can do this in pybind11 as well, using the Python C++ Interface:

// Let's say your decorator is defined in the module `my_project.meta`.
py::function my_decorator = py::module::import("my_project.meta").attr("my_decorator");
// Define your class normally.
py::class_<MyClass> my_class(m, "MyClass");
my_class  // BR
   .def(py::init())
   .def(...);
// Decorate - you don't really need the reassignment if your decorator mutates the original object,
// instead of returning a proxy / wrapper / whatevs.
m.attr("MyClass") = my_decorator(x)(my_class);

@eacousineau
Copy link
Contributor

Followup: Python docs link - primary sources FTW!

@phil-zxx
Copy link
Contributor Author

Hi eacousineau, I appreciate the comment. I am having trouble to compile this line though:

m.attr("MyClass") = my_decorator(x)(my_class);

What is x?

And can I make my decorator be a C++ function? (i.e. not importing it from an external python package)?

@liff-engineer
Copy link

I figure out the correct way to write decorator in pybind11 with @eacousineau comment.

use example in PEP 443 -- Single-dispatch generic functions

from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):
     if verbose:
         print("Let me just say,", end=" ")
    print(arg)

@fun.register(int)
def _(arg, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)


@fun.register(list)
def _(arg, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, e in enumerate(arg):
        print(i, e)


def nothing(arg, verbose=False):
    print("Nothing.")


fun.register(type(None), nothing)


@fun.register(float)
def fun_num(arg, verbose=False):
    if verbose:
        print("Half of your number:", end=" ")
    print(arg/2)


fun("Hello,world")
fun("test.", verbose=True)
fun(42, verbose=True)
fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
fun(None)
fun(1.23)

first , implement fun in pybind11 like this:

namespace py = pybind11;
py::object singledispatch = py::module::import("functools").attr("singledispatch");
m.def(
        "fun", [](py::object arg, bool verbose = false) {
            if (verbose)
            {
                py::print("Let me just say", py::arg("end") = " ");
            }
            py::print(arg);
        },
        py::arg("arg"), py::arg("verbose") = false);

m.attr("fun") = singledispatch(m.attr("fun"));//use singledispatch wrap fun object 

Now you can use fun as new decorator.

And if you has decorator in python like this:

@fun.register(str)
def fun_str(arg, verbose=False):
    if verbose:
        print("get string", end=" ")
    print(arg)

In pybind11 implement like this:

m.def(
        "fun_str", [](py::str arg, bool verbose = false) {
            if (verbose)
            {
                py::print("get string", py::arg("end") = " ");
            }
            py::print(arg);
        },
        py::arg("arg"), py::arg("verbose") = false);

//decorator
auto fun = m.attr("fun");
auto fun_register = fun.attr("register");

//@fun.register(str) accept  object type, you should get str object from builtins module
//and write just like function call
py::object string_ = py::module::import("builtins").attr("str");
m.attr("fun_str") = fun_register(string_)(m.attr("fun_str"));

You can get executable example from pybind11 & python decorator. If you have any questions,please contact me.

@YannickJadoul
Copy link
Collaborator

Thanks for answering, @eacousineau and @liff-engineer!

@silent-dxx
Copy link

silent-dxx commented Aug 3, 2021

I recently studied the implementation of this feature.
The following example code implements the use of a flash like

flask decorators like this:

from flask import Flask
app = Flask(__name__)

@app.route('/admin')
def hello_admin():
   return 'Hello Admin'

Implementation using pybind11 :

example.cpp

#include <iostream>
#include <string>
#include <map>

#include <pybind11/embed.h>
#include <pybind11/functional.h>

namespace py = pybind11;

class Flask {
public:
    Flask() = default;
    Flask(const Flask&) = delete;
    Flask& operator= (const Flask&) = delete;
    Flask* operator&() = delete;
    const Flask operator&() const = delete;

    static Flask *GetInstance() {
        if (_this == nullptr) {
            _this = new Flask();
        }
        return _this;
    }

    /* decorator: implementation method 2 */
    static auto decorator(const py::function &fun) {
        _this = GetInstance();

        /* call back fun or do anything */
        //fun(2);

        /* register function */
        _this->py_cb.insert(std::make_pair(_this->currRule, fun));

        return fun;
    };

    static auto route(std::string rule) {
        _this = GetInstance();
        _this->currRule = rule;

        py::function decorator = py::module::import("myFlask").attr("decorator");
        return decorator;
    }

    static void run(std::string fun, std::string name) {
        _this = GetInstance();

        if (_this->py_cb.find(fun) != _this->py_cb.end()) {
            _this->py_cb[fun](name);
        }
    }

private:
    class GC {
    public:
        explicit GC() {};
        ~GC() {
            if (_this) {
                delete _this;
                _this = nullptr;
            }
        }
    };

private:
    static Flask *_this;
    static GC gc;

    std::string currRule;
    std::map<std::string, py::object> py_cb;
};

Flask *Flask::_this = nullptr;
Flask::GC Flask::gc;

PYBIND11_EMBEDDED_MODULE(myFlask, m) {
#if 1
    /* decorator: implementation method 1 */
    m.def("decorator", [](const py::function &fun) -> py::function {
        /* call Flask::decorator */
        Flask::decorator(fun);

        return fun;
    });
#else
    /* decorator: implementation method 2 */
    m.def("decorator", &Flask::decorator);
#endif

    py::class_<Flask> fl(m, "Flask");
    fl.def(py::init(&Flask::GetInstance));
    fl.def_static("run", &Flask::run);
#if 0
    /* route: implementation method 1 */
    fl.def_static("route", &Flask::route);
#else
    static auto wrapper = m.attr("decorator");

    /* route: implementation method 2 */
    fl.def_static("route", [](std::string rule) -> auto {
        /* call Flask::route */
        Flask::route(rule);

        return wrapper;
    });
#endif
}

auto main(int argc, char **argv) -> int {
    py::scoped_interpreter python;

    py::module t = py::module::import("example_wrapper");

    auto app = py::module::import("myFlask").attr("Flask");

    app.attr("run")("/hello", "Bob");

    Flask::run("/hello", "Eve");

    Flask::run("/welcome", "This is a demo of the implementation of decorator");

    return 0;
 }

example_wrapper.py

from myFlask import Flask

app = Flask()

@app.route('/hello')
def hello(name):
    print('hello {}'.format(name))

@app.route('/welcome')
def welcome(text):
    print(text)

app.run('/hello', 'Alice')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants