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 support for non-converting arguments #634

Merged
merged 2 commits into from Feb 5, 2017

Conversation

jagerman
Copy link
Member

@jagerman jagerman commented Feb 1, 2017

This adds support for controlling the convert flag of arguments through the py::arg annotation. This then allows arguments to be flagged as non-converting, which the type_caster<X> is able to use to implement different behaviour.

Currently, as far as I can see, convert is only used for type converters of regular pybind11-registered types (to prevent implicit conversions from recursing); all of the other core type_casters just ignore the argument. We can, however, repurpose it to control internal conversion of converters like Eigen and array: most usefully to give callers a way to disable the conversion that would otherwise occur when a Eigen::Ref<const Eigen::Matrix> argument is passed a numpy array that requires conversion (either because it has an incompatible stride or the wrong dtype).

Specifying a noconvert argument looks like one of these:

    m.def("f1", &f, "a"_a.noconvert() = "default"); // Named, default, noconvert
    m.def("f2", &f, "a"_a.noconvert()); // Named, no default, no converting
    m.def("f3", &f, py::arg().noconvert()); // Unnamed, no default, no converting

(The last one—being able to declare a py::arg without a name—is new: previously py::arg() only accepted named keyword arguments).

Such an non-convert argument is then passed convert = false by the type caster when loading the argument. Whether this has an effect is up to the type caster itself, but as mentioned above, this would be extremely helpful for the Eigen support to give a nicer way to specify a "no-copy" mode than the custom wrapper in the current PR, and moreover isn't an Eigen-specific hack.

The relevant bit of the internal Eigen type caster would then contain something like:

bool load(handle src, bool convert) {
    // ...
    bool copy_required = (stride_incompatible || wrong_dtype);
    if (copy_required && !convert) return false;
    // ...
}

and binding code that wanted to say "do not copy data" would then use:

void f(Eigen::Ref<const MatrixXd> m) { /* ... */ }

    // Binding code:
    m.def("f", &f, py::arg().noconvert());

rather than the current PR's ugly proposal of:

    m.def("f", [](py::EigenRefNoCopy<const MatrixXd> m) { return f(m); });

While that workaround works for Eigen, it's not exactly elegant, and I think this no-convert argument could be useful elsewhere, too.

Thoughts/comments/suggestions?

@aldanor
Copy link
Member

aldanor commented Feb 1, 2017

I had the same thoughts recently, see #338 (comment)

Basically, we need to be able to accept "lvalues" (e.g. NumPy arrays that are to mutated in place), which is something that's not possible with auto conversion.

Will read this in more detail and reply tomorrow :)

@jagerman
Copy link
Member Author

jagerman commented Feb 1, 2017

Yeah, your #338 comments were part of the reason to do this globally rather than just the Eigen hack.

template <typename T>
arg_v(const char *name, T &&x, const char *descr = nullptr)
: arg(name),
arg_v(arg &&base, T &&x, const char *descr = nullptr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this change -- why the rvalue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to potentially avoid a copy--that's now just a private constructor with the common code, invoked with an rvalue arg from the direct (first public) or converted-from-arg (second public) constructors.

@wjakob
Copy link
Member

wjakob commented Feb 1, 2017

I think this is a great API -- it's simple to use and leads to nice and readable binding code. Questions from my end would be:

  1. Is 1 bit enough for what we'll need to do in the future?
  2. You guessed it: does this increase .SO size? ;)

@jagerman
Copy link
Member Author

jagerman commented Feb 1, 2017

Is 1 bit enough for what we'll need to do in the future?

That depends a lot on the future, I guess. I toyed with a change to pass a custom argument info struct into load (with some clever detection of whether a type_caster has a load(str, convert) or a a load(str, argument_info) method). Unfortunately it required some pretty major changes to type caster invocation, not contained just to the argument loader. One advantage of this approach is that it doesn't break existing type casters.

You guessed it: does this increase .SO size? ;)

I measured this, and meant to include it in the PR message (but apparently forgot). Commenting out the added tests, the .so increase (under gcc) is pretty small:

------ pybind11_tests.cpython-35m-x86_64-linux-gnu.so file size: 1204192 (change of +4224 bytes = +0.35%)

@jagerman
Copy link
Member Author

jagerman commented Feb 1, 2017

One other thing that I had in mind when writing this PR (but that belongs in a future PR) is that it makes it reasonably easy to implement a two-pass function call procedure, where we first try to call the function with convert = false on all the arguments, then, if that fails, go through the chain again with convert = true (except for those with py::arg().noconvert()). That (combined with py::array_t changes to understand the convert flag) would address the issue in #631: if you bind several overloads which all work via conversion, but one works without conversion, we would still invoke the one that doesn't require conversion (then fallback to the first if none work without conversion).

Arithmetic and complex casters now only do a converting cast when
`convert=true`; previously they would convert always (e.g. when passing
an int to a float-accepting function, or a float to complex-accepting
function).
This adds support for controlling the `convert` flag of arguments
through the py::arg annotation.  This then allows arguments to be
flagged as non-converting, which the type_caster is able to use to
request different behaviour.

Currently, AFAICS `convert` is only used for type converters of regular
pybind11-registered types; all of the other core type_casters ignore it.
We can, however, repurpose it to control internal conversion of
converters like Eigen and `array`: most usefully to give callers a way
to disable the conversion that would otherwise occur when a
`Eigen::Ref<const Eigen::Matrix>` argument is passed a numpy array that
requires conversion (either because it has an incompatible stride or the
wrong dtype).

Specifying a noconvert looks like one of these:

    m.def("f1", &f, "a"_a.noconvert() = "default"); // Named, default, noconvert
    m.def("f2", &f, "a"_a.noconvert()); // Named, no default, no converting
    m.def("f3", &f, py::arg().noconvert()); // Unnamed, no default, no converting

(The last part--being able to declare a py::arg without a name--is new:
previous py::arg() only accepted named keyword arguments).

Such an non-convert argument is then passed `convert = false` by the
type caster when loading the argument.  Whether this has an effect is up
to the type caster itself, but as mentioned above, this would be
extremely helpful for the Eigen support to give a nicer way to specify
a "no-copy" mode than the custom wrapper in the current PR, and
moreover isn't an Eigen-specific hack.
@wjakob
Copy link
Member

wjakob commented Feb 5, 2017

Merged!

@wjakob wjakob merged commit 18e34cb into pybind:master Feb 5, 2017
jagerman added a commit to jagerman/pybind11 that referenced this pull request Feb 5, 2017
This adds no-convert support to the Eigen code via the new
`py::arg().noconvert()` support added in PR pybind#634.  `Eigen::Ref<const M>`
arguments bound with `.noconvert()` won't be copied.  (`Eigen::Ref<M>`
for non-const `M` continue to never allow copying).
jagerman added a commit to jagerman/pybind11 that referenced this pull request Feb 6, 2017
With PR pybind#634, `py::arg` instances don't necessarily have a name, but the
debugging output for failed casting of defaults was using `a.name`.
This fixes it to substitute "arg" if the argument doesn't have a name of
its own.
jagerman added a commit to jagerman/pybind11 that referenced this pull request Feb 6, 2017
This fixes a couple bugs with nameless py::arg() (introduced in pybind#634)
annotations:

- the argument name was being used in debug mode without checking that
  it exists (which would result in the std::string construction throwing
  an exception for being invoked with a nullptr)
- the error output says "keyword arguments", but py::arg_v() can now
  also be used for positional argument defaults.
- the debugging output "in function named 'blah'" was overly verbose:
  changed it to just "in function 'blah'".
jagerman added a commit to jagerman/pybind11 that referenced this pull request Feb 6, 2017
This fixes a couple bugs with nameless py::arg() (introduced in pybind#634)
annotations:

- the argument name was being used in debug mode without checking that
  it exists (which would result in the std::string construction throwing
  an exception for being invoked with a nullptr)
- the error output says "keyword arguments", but py::arg_v() can now
  also be used for positional argument defaults.
- the debugging output "in function named 'blah'" was overly verbose:
  changed it to just "in function 'blah'".
wjakob pushed a commit that referenced this pull request Feb 8, 2017
* Fix debugging output for nameless py::arg annotations

This fixes a couple bugs with nameless py::arg() (introduced in #634)
annotations:

- the argument name was being used in debug mode without checking that
  it exists (which would result in the std::string construction throwing
  an exception for being invoked with a nullptr)
- the error output says "keyword arguments", but py::arg_v() can now
  also be used for positional argument defaults.
- the debugging output "in function named 'blah'" was overly verbose:
  changed it to just "in function 'blah'".

* Fix missing space in debug test string

* Moved tests from issues to methods_and_attributes
jagerman added a commit to jagerman/pybind11 that referenced this pull request Feb 15, 2017
This commit largely rewrites the Eigen dense matrix support to avoid
copying in many cases: Eigen arguments can now reference numpy data, and
numpy objects can now reference Eigen data (given compatible types).

Eigen::Ref<...> arguments now also make use of the new `convert`
argument use (added in PR pybind#634) to avoid conversion, allowing
`py::arg().noconvert()` to be used when binding a function to prohibit
copying when invoking the function.  Respecting `convert` also means
Eigen overloads that avoid copying will be preferred during overload
resolution to ones that require copying.

This commit also rewrites the Eigen documentation and test suite to
explain and test the new capabilities.
jagerman added a commit to jagerman/pybind11 that referenced this pull request Feb 15, 2017
This commit largely rewrites the Eigen dense matrix support to avoid
copying in many cases: Eigen arguments can now reference numpy data, and
numpy objects can now reference Eigen data (given compatible types).

Eigen::Ref<...> arguments now also make use of the new `convert`
argument use (added in PR pybind#634) to avoid conversion, allowing
`py::arg().noconvert()` to be used when binding a function to prohibit
copying when invoking the function.  Respecting `convert` also means
Eigen overloads that avoid copying will be preferred during overload
resolution to ones that require copying.

This commit also rewrites the Eigen documentation and test suite to
explain and test the new capabilities.
wjakob pushed a commit that referenced this pull request Feb 24, 2017
This commit largely rewrites the Eigen dense matrix support to avoid
copying in many cases: Eigen arguments can now reference numpy data, and
numpy objects can now reference Eigen data (given compatible types).

Eigen::Ref<...> arguments now also make use of the new `convert`
argument use (added in PR #634) to avoid conversion, allowing
`py::arg().noconvert()` to be used when binding a function to prohibit
copying when invoking the function.  Respecting `convert` also means
Eigen overloads that avoid copying will be preferred during overload
resolution to ones that require copying.

This commit also rewrites the Eigen documentation and test suite to
explain and test the new capabilities.
@jagerman jagerman modified the milestone: v2.1 Mar 19, 2017
@rwgk rwgk mentioned this pull request Feb 9, 2023
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

Successfully merging this pull request may close these issues.

None yet

3 participants