Skip to content

Commit b3573ac

Browse files
bmerryrwgk
andauthored
feat: add .keys and .values to bind_map (#3310)
* Add `.keys` and `.values` to bind_map Both of these implement views (rather than just iterators), and `.items` is also upgraded to a view. In practical terms, this allows a view to be iterated multiple times and have its size taken, neither of which works with an iterator. The views implement `__len__`, `__iter__`, and the keys view implements `__contains__`. Testing membership also works in item and value views because Python falls back to iteration. This won't be optimal for item values since it's linear rather than O(log n) or O(1), but I didn't fancy trying to get all the corner cases to match Python behaviour (tuple of wrong types, wrong length tuple, not a tuple etc). Missing relative to Python dictionary views is `__reversed__` (only added to Python in 3.8). Implementing that could break code that binds custom map classes which don't provide `rbegin`/`rend` (at least without doing clever things with SFINAE), so I've not tried. The size increase on my system is 131072 bytes, which is rather large (5%) but also suspiciously round (2^17) and makes me suspect some quantisation effect. * bind_map: support any object in __contains__ Add extra overload of `__contains__` (for both the map itself and KeysView) which takes an arbitrary object and returns false. * Take py::object by const reference in __contains__ To keep clang-tidy happy. * Removing stray `py::` (detected via interactive testing in Google environment). Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]>
1 parent b4e1ab8 commit b3573ac

File tree

2 files changed

+104
-4
lines changed

2 files changed

+104
-4
lines changed

include/pybind11/stl_bind.h

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,13 +595,33 @@ template <typename Map, typename Class_> auto map_if_insertion_operator(Class_ &
595595
);
596596
}
597597

598+
template<typename Map>
599+
struct keys_view
600+
{
601+
Map &map;
602+
};
603+
604+
template<typename Map>
605+
struct values_view
606+
{
607+
Map &map;
608+
};
609+
610+
template<typename Map>
611+
struct items_view
612+
{
613+
Map &map;
614+
};
598615

599616
PYBIND11_NAMESPACE_END(detail)
600617

601618
template <typename Map, typename holder_type = std::unique_ptr<Map>, typename... Args>
602619
class_<Map, holder_type> bind_map(handle scope, const std::string &name, Args&&... args) {
603620
using KeyType = typename Map::key_type;
604621
using MappedType = typename Map::mapped_type;
622+
using KeysView = detail::keys_view<Map>;
623+
using ValuesView = detail::values_view<Map>;
624+
using ItemsView = detail::items_view<Map>;
605625
using Class_ = class_<Map, holder_type>;
606626

607627
// If either type is a non-module-local bound type then make the map binding non-local as well;
@@ -615,6 +635,12 @@ class_<Map, holder_type> bind_map(handle scope, const std::string &name, Args&&.
615635
}
616636

617637
Class_ cl(scope, name.c_str(), pybind11::module_local(local), std::forward<Args>(args)...);
638+
class_<KeysView> keys_view(
639+
scope, ("KeysView[" + name + "]").c_str(), pybind11::module_local(local));
640+
class_<ValuesView> values_view(
641+
scope, ("ValuesView[" + name + "]").c_str(), pybind11::module_local(local));
642+
class_<ItemsView> items_view(
643+
scope, ("ItemsView[" + name + "]").c_str(), pybind11::module_local(local));
618644

619645
cl.def(init<>());
620646

@@ -628,12 +654,22 @@ class_<Map, holder_type> bind_map(handle scope, const std::string &name, Args&&.
628654

629655
cl.def("__iter__",
630656
[](Map &m) { return make_key_iterator(m.begin(), m.end()); },
631-
keep_alive<0, 1>() /* Essential: keep list alive while iterator exists */
657+
keep_alive<0, 1>() /* Essential: keep map alive while iterator exists */
658+
);
659+
660+
cl.def("keys",
661+
[](Map &m) { return KeysView{m}; },
662+
keep_alive<0, 1>() /* Essential: keep map alive while view exists */
663+
);
664+
665+
cl.def("values",
666+
[](Map &m) { return ValuesView{m}; },
667+
keep_alive<0, 1>() /* Essential: keep map alive while view exists */
632668
);
633669

634670
cl.def("items",
635-
[](Map &m) { return make_iterator(m.begin(), m.end()); },
636-
keep_alive<0, 1>() /* Essential: keep list alive while iterator exists */
671+
[](Map &m) { return ItemsView{m}; },
672+
keep_alive<0, 1>() /* Essential: keep map alive while view exists */
637673
);
638674

639675
cl.def("__getitem__",
@@ -654,6 +690,8 @@ class_<Map, holder_type> bind_map(handle scope, const std::string &name, Args&&.
654690
return true;
655691
}
656692
);
693+
// Fallback for when the object is not of the key type
694+
cl.def("__contains__", [](Map &, const object &) -> bool { return false; });
657695

658696
// Assignment provided only if the type is copyable
659697
detail::map_assignment<Map, Class_>(cl);
@@ -669,6 +707,40 @@ class_<Map, holder_type> bind_map(handle scope, const std::string &name, Args&&.
669707

670708
cl.def("__len__", &Map::size);
671709

710+
keys_view.def("__len__", [](KeysView &view) { return view.map.size(); });
711+
keys_view.def("__iter__",
712+
[](KeysView &view) {
713+
return make_key_iterator(view.map.begin(), view.map.end());
714+
},
715+
keep_alive<0, 1>() /* Essential: keep view alive while iterator exists */
716+
);
717+
keys_view.def("__contains__",
718+
[](KeysView &view, const KeyType &k) -> bool {
719+
auto it = view.map.find(k);
720+
if (it == view.map.end())
721+
return false;
722+
return true;
723+
}
724+
);
725+
// Fallback for when the object is not of the key type
726+
keys_view.def("__contains__", [](KeysView &, const object &) -> bool { return false; });
727+
728+
values_view.def("__len__", [](ValuesView &view) { return view.map.size(); });
729+
values_view.def("__iter__",
730+
[](ValuesView &view) {
731+
return make_value_iterator(view.map.begin(), view.map.end());
732+
},
733+
keep_alive<0, 1>() /* Essential: keep view alive while iterator exists */
734+
);
735+
736+
items_view.def("__len__", [](ItemsView &view) { return view.map.size(); });
737+
items_view.def("__iter__",
738+
[](ItemsView &view) {
739+
return make_iterator(view.map.begin(), view.map.end());
740+
},
741+
keep_alive<0, 1>() /* Essential: keep view alive while iterator exists */
742+
);
743+
672744
return cl;
673745
}
674746

tests/test_stl_binders.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,43 @@ def test_map_string_double():
160160
mm["b"] = 2.5
161161

162162
assert list(mm) == ["a", "b"]
163-
assert list(mm.items()) == [("a", 1), ("b", 2.5)]
164163
assert str(mm) == "MapStringDouble{a: 1, b: 2.5}"
164+
assert "b" in mm
165+
assert "c" not in mm
166+
assert 123 not in mm
167+
168+
# Check that keys, values, items are views, not merely iterable
169+
keys = mm.keys()
170+
values = mm.values()
171+
items = mm.items()
172+
assert list(keys) == ["a", "b"]
173+
assert len(keys) == 2
174+
assert "a" in keys
175+
assert "c" not in keys
176+
assert 123 not in keys
177+
assert list(items) == [("a", 1), ("b", 2.5)]
178+
assert len(items) == 2
179+
assert ("b", 2.5) in items
180+
assert "hello" not in items
181+
assert ("b", 2.5, None) not in items
182+
assert list(values) == [1, 2.5]
183+
assert len(values) == 2
184+
assert 1 in values
185+
assert 2 not in values
186+
# Check that views update when the map is updated
187+
mm["c"] = -1
188+
assert list(keys) == ["a", "b", "c"]
189+
assert list(values) == [1, 2.5, -1]
190+
assert list(items) == [("a", 1), ("b", 2.5), ("c", -1)]
165191

166192
um = m.UnorderedMapStringDouble()
167193
um["ua"] = 1.1
168194
um["ub"] = 2.6
169195

170196
assert sorted(list(um)) == ["ua", "ub"]
197+
assert list(um.keys()) == list(um)
171198
assert sorted(list(um.items())) == [("ua", 1.1), ("ub", 2.6)]
199+
assert list(zip(um.keys(), um.values())) == list(um.items())
172200
assert "UnorderedMapStringDouble" in str(um)
173201

174202

0 commit comments

Comments
 (0)