Skip to content

Commit a32db87

Browse files
committed
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.
1 parent 6bce3bd commit a32db87

File tree

2 files changed

+96
-4
lines changed

2 files changed

+96
-4
lines changed

include/pybind11/stl_bind.h

Lines changed: 71 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+
py::class_<KeysView> keys_view(
639+
scope, ("KeysView[" + name + "]").c_str(), pybind11::module_local(local));
640+
py::class_<ValuesView> values_view(
641+
scope, ("ValuesView[" + name + "]").c_str(), pybind11::module_local(local));
642+
py::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__",
@@ -669,6 +705,38 @@ class_<Map, holder_type> bind_map(handle scope, const std::string &name, Args&&.
669705

670706
cl.def("__len__", &Map::size);
671707

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

tests/test_stl_binders.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,39 @@ 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}"
165164

165+
# Check that keys, values, items are views, not merely iterable
166+
keys = mm.keys()
167+
values = mm.values()
168+
items = mm.items()
169+
assert list(keys) == ["a", "b"]
170+
assert len(keys) == 2
171+
assert "a" in keys
172+
assert "c" not in keys
173+
assert list(items) == [("a", 1), ("b", 2.5)]
174+
assert len(items) == 2
175+
assert ("b", 2.5) in items
176+
assert "hello" not in items
177+
assert ("b", 2.5, None) not in items
178+
assert list(values) == [1, 2.5]
179+
assert len(values) == 2
180+
assert 1 in values
181+
assert 2 not in values
182+
# Check that views update when the map is updated
183+
mm["c"] = -1
184+
assert list(keys) == ["a", "b", "c"]
185+
assert list(values) == [1, 2.5, -1]
186+
assert list(items) == [("a", 1), ("b", 2.5), ("c", -1)]
187+
166188
um = m.UnorderedMapStringDouble()
167189
um["ua"] = 1.1
168190
um["ub"] = 2.6
169191

170192
assert sorted(list(um)) == ["ua", "ub"]
193+
assert list(um.keys()) == list(um)
171194
assert sorted(list(um.items())) == [("ua", 1.1), ("ub", 2.6)]
195+
assert list(zip(um.keys(), um.values())) == list(um.items())
172196
assert "UnorderedMapStringDouble" in str(um)
173197

174198

0 commit comments

Comments
 (0)