Access C++ hash functions from Python and vice versa (#1034)
There are two separate additions:
1. `py::hash(obj)` is equivalent to the Python `hash(obj)`.
2. `.def(hash(py::self))` registers the hash function defined by
`std::hash<T>` as the Python hash function.
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 478b7d7..7f898fe 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -243,6 +243,11 @@
* Fixed implicit conversion of `py::enum_` to integer types on Python 2.7.
`#821 <https://github.com/pybind/pybind11/pull/821>`_.
+* Added ``py::hash`` to fetch the hash value of Python objects, and
+ ``.def(hash(py::self))`` to provide the C++ ``std::hash`` as the Python
+ ``__hash__`` method.
+ `#1034 <https://github.com/pybind/pybind11/pull/1034>`_.
+
* Fixed ``__truediv__`` on Python 2 and ``__itruediv__`` on Python 3.
`#867 <https://github.com/pybind/pybind11/pull/867>`_.
diff --git a/include/pybind11/operators.h b/include/pybind11/operators.h
index 11ea248..b3dd62c 100644
--- a/include/pybind11/operators.h
+++ b/include/pybind11/operators.h
@@ -28,7 +28,7 @@
op_int, op_long, op_float, op_str, op_cmp, op_gt, op_ge, op_lt, op_le,
op_eq, op_ne, op_iadd, op_isub, op_imul, op_idiv, op_imod, op_ilshift,
op_irshift, op_iand, op_ixor, op_ior, op_complex, op_bool, op_nonzero,
- op_repr, op_truediv, op_itruediv
+ op_repr, op_truediv, op_itruediv, op_hash
};
enum op_type : int {
@@ -148,6 +148,7 @@
PYBIND11_UNARY_OPERATOR(neg, operator-, -l)
PYBIND11_UNARY_OPERATOR(pos, operator+, +l)
PYBIND11_UNARY_OPERATOR(abs, abs, std::abs(l))
+PYBIND11_UNARY_OPERATOR(hash, hash, std::hash<L>()(l))
PYBIND11_UNARY_OPERATOR(invert, operator~, (~l))
PYBIND11_UNARY_OPERATOR(bool, operator!, !!l)
PYBIND11_UNARY_OPERATOR(int, int_, (int) l)
diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h
index 9e18a01..92316c3 100644
--- a/include/pybind11/pytypes.h
+++ b/include/pybind11/pytypes.h
@@ -390,6 +390,13 @@
inline void setattr(handle obj, const char *name, handle value) {
if (PyObject_SetAttrString(obj.ptr(), name, value.ptr()) != 0) { throw error_already_set(); }
}
+
+inline ssize_t hash(handle obj) {
+ auto h = PyObject_Hash(obj.ptr());
+ if (h == -1) { throw error_already_set(); }
+ return h;
+}
+
/// @} python_builtins
NAMESPACE_BEGIN(detail)
diff --git a/tests/test_operator_overloading.cpp b/tests/test_operator_overloading.cpp
index d4d35f0..4ad34d1 100644
--- a/tests/test_operator_overloading.cpp
+++ b/tests/test_operator_overloading.cpp
@@ -10,6 +10,7 @@
#include "pybind11_tests.h"
#include "constructor_stats.h"
#include <pybind11/operators.h>
+#include <functional>
class Vector2 {
public:
@@ -53,6 +54,14 @@
int operator+(const C2 &, const C1 &) { return 21; }
int operator+(const C1 &, const C2 &) { return 12; }
+namespace std {
+ template<>
+ struct hash<Vector2> {
+ // Not a good hash function, but easy to test
+ size_t operator()(const Vector2 &) { return 4; }
+ };
+}
+
TEST_SUBMODULE(operators, m) {
// test_operator_overloading
@@ -77,6 +86,7 @@
.def(float() * py::self)
.def(float() / py::self)
.def("__str__", &Vector2::toString)
+ .def(hash(py::self))
;
m.attr("Vector") = m.attr("Vector2");
diff --git a/tests/test_operator_overloading.py b/tests/test_operator_overloading.py
index 845cedd..0d80e5e 100644
--- a/tests/test_operator_overloading.py
+++ b/tests/test_operator_overloading.py
@@ -35,6 +35,8 @@
v2 /= v1
assert str(v2) == "[2.000000, 8.000000]"
+ assert hash(v1) == 4
+
cstats = ConstructorStats.get(m.Vector2)
assert cstats.alive() == 2
del v1
diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp
index edc1e99..4742236 100644
--- a/tests/test_pytypes.cpp
+++ b/tests/test_pytypes.cpp
@@ -261,4 +261,6 @@
});
m.def("print_failure", []() { py::print(42, UnregisteredType()); });
+
+ m.def("hash_function", [](py::object obj) { return py::hash(obj); });
}
diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py
index b8dad84..2487cf6 100644
--- a/tests/test_pytypes.py
+++ b/tests/test_pytypes.py
@@ -220,3 +220,19 @@
if debug_enabled else
"arguments to Python object (compile in debug mode for details)"
)
+
+
+def test_hash():
+ class Hashable(object):
+ def __init__(self, value):
+ self.value = value
+
+ def __hash__(self):
+ return self.value
+
+ class Unhashable(object):
+ __hash__ = None
+
+ assert m.hash_function(Hashable(42)) == 42
+ with pytest.raises(TypeError):
+ m.hash_function(Unhashable())