# Getting values from a given level of a nested collection

This question is a supplement to my previous question: Counting 'absolute' nesting levels of an iterable.

I would like to expand functionality of counting nests in order to get values from given nest. I created 2 examples:

dct_test = {
"a": {1: 2, 2: 3, 3: 4},
"b": {"x": "y", "z": [1, 2, 3, {7, 8}]},
"c": {"l": (2, 3, 5), "j": {"a": {"k": 1, "l": [1, 3, (2, 7, {"x": 1})]}}},
"d": [1, (2, 3)],
"e": 5
}

tpl_test = (1, [3, 4, {5, 6, 7}], {"a": 5, "b": [9, 8]}, 2)


Picture below shows an algorithm of counting with values that should be returned from each level:

Here's code:

from collections.abc import Iterable
from itertools import chain

# EXAMPLES
dct_test = {
"a": {1: 2, 2: 3, 3: 4},
"b": {"x": "y", "z": [1, 2, 3, {7, 8}]},
"c": {"l": (2, 3, 5), "j": {"a": {"k": 1, "l": [1, 3, (2, 7, {"x": 1})]}}},
"d": [1, (2, 3)],
"e": 5
}

tpl_test = (1, [3, 4, {5, 6, 7}], {"a": 5, "b": [9, 8]}, 2)

# FUNCTIONS
# converting a collection to a list
def ConvertIterableToList(
coll: Iterable,
):
# check if argument is a collection
if isinstance(coll, (tuple, list, set, dict)):

# if the argument is a tuple or set - convert to list
if isinstance(coll, (tuple, set)):
return list(coll)

# if the argument is a dictionary - get values of a dictionary and convert them to list
elif isinstance(coll, dict):
return list(coll.values())

# if the argument is a list - return collection
elif isinstance(coll, list):
return coll
else:
raise TypeError("Given argument is not a list, tuple, set or dictionary")

# author: FMc
# https://codereview.stackexchange.com/a/270335/252081
def GetIterDepth(
coll: Iterable,
):

# returning coll if it's a tuple, list or set
# returning values of dictionary if coll is dict
# returning None if coll is not a collection
iter_to_inspect = (
coll if isinstance(coll, (tuple, list, set)) else
coll.values() if isinstance(coll, dict) else
None
)

# # 0 means coll is not a collection at all
# # 1 means coll has no nests
# # 1 + max(...) means coll has nested collections
# iterate over iter_to_inspect in order to get one level deeper
# recursion is needed to inspect deeper nests
# max() stores result for each case of a collections inside a collection
# 1 is a kind incrementator/counter for looping over collections inside a collection

return (
0 if iter_to_inspect is None else
1 if not iter_to_inspect else
1 + max(GetIterDepth(i) for i in iter_to_inspect)
)

# returns a list of collections extracted from a collection
def PeelIter(
coll: Iterable
):

if isinstance(coll, (tuple, list, set, dict)):
return [i for i in ConvertIterableToList(coll) if isinstance(i, (tuple, list, set, dict))]
else:
raise TypeError("Given argument is not a list, tuple, set or dictionary")

# returns a collection from a given level
def GetItersFromLevel(
coll: Iterable,
level: int
):

# argument must be a collection - error otherwise
if not isinstance(coll, (tuple, list, set, dict)):
raise TypeError("Given argument is not a list, tuple, set or dictionary")

# maximum number of nests (levels) inside a collection
n_levels = GetIterDepth(coll)

# level cannot be equal to 0 or less
if level < 1:
raise ValueError(f"Given level ({level}) is not greater or equal 1.")

# level cannot be greater than maximum number of levels
if level > n_levels:
raise ValueError(f"Given level ({level}) is bigger than maximum number of levels \
in a collection ({n_levels}).")

# if level is 1 - no computation is needed - simply return a collection
if level == 1:

# if coll is dict return only values
if isinstance(coll, dict):
return list(coll.values())
else:
return coll

# if level if greater than one
else:

# assuming function needs to return at least 2nd level
# we have to "peel" a collection at start
iter_to_inspect = PeelIter(coll)

# looping as many times as deep is a collection
# in order to avoid recursion or indefinite while loop
# we can start from 2 as there's no need to inspect 1st level
# we have to add 1 to n_levels because range(from, to) ends at (to - 1)
for i in range(2, n_levels + 1):

# if i is less than given level we have to continue peeling
if i < level:

# peeling iter_to_inspect as well as j is needed because:
# 1. peeling iter_to inspect returns list of collections inside iter_to_inspect
# 2. peeling j returns collections from elements inside list of collections
# # from peeled iter_to_inspect

# iter_to_inspect returns list of lists containing single collections
# flattening is needed because otherwise function would constantly iterate
# # over the same peeled collection
iter_to_inspect = \
list(chain.from_iterable([PeelIter(j) for j in PeelIter(iter_to_inspect)]))

else:
return iter_to_inspect

print("1st level: ", GetItersFromLevel(dct_test, 1)) # returns dct_test.values() so it's correct
print("2nd level: ", GetItersFromLevel(dct_test, 2))
print("3rd level: ", GetItersFromLevel(dct_test, 3))
print("4th level: ", GetItersFromLevel(dct_test, 4))
print("5th level: ", GetItersFromLevel(dct_test, 5))
print("6th level: ", GetItersFromLevel(dct_test, 6))
print("7th level: ", GetItersFromLevel(dct_test, 7))
print("8th level: ", GetItersFromLevel(dct_test, 8), "\n") # error

print("1st level: ", GetItersFromLevel(tpl_test, 1))
print("2nd level: ", GetItersFromLevel(tpl_test, 2))
print("3rd level: ", GetItersFromLevel(tpl_test, 3))
print("4th level: ", GetItersFromLevel(tpl_test, 4)) #error


Output:

1st level:
[{1: 2, 2: 3, 3: 4}, {'x': 'y', 'z': [1, 2, 3, {8, 7}]}, {'l': (2, 3, 5), 'j': {'a': {'k': 1, 'l': [1, 3, (2, 7, {'x': 1})]}}}, [1, (2, 3)], 5]
2nd level:
[{1: 2, 2: 3, 3: 4}, {'x': 'y', 'z': [1, 2, 3, {8, 7}]}, {'l': (2, 3, 5), 'j': {'a': {'k': 1, 'l': [1, 3, (2, 7, {'x': 1})]}}}, [1, (2, 3)]]
3rd level:
[[1, 2, 3, {8, 7}], (2, 3, 5), {'a': {'k': 1, 'l': [1, 3, (2, 7, {'x': 1})]}}, (2, 3)]
4th level:
[{8, 7}, {'k': 1, 'l': [1, 3, (2, 7, {'x': 1})]}]
5th level:
[[1, 3, (2, 7, {'x': 1})]]
6th level:
[(2, 7, {'x': 1})]
7th level:
[{'x': 1}]
Traceback (most recent call last):
(...)
ValueError: Given level (8) is bigger than maximum number of levels in a collection (7).

1st level:  (1, [3, 4, {5, 6, 7}], {'a': 5, 'b': [9, 8]}, 2)
2nd level:  [[3, 4, {5, 6, 7}], {'a': 5, 'b': [9, 8]}]
3rd level:  [{5, 6, 7}, [9, 8]]
Traceback (most recent call last):
(...)
ValueError: Given level (4) is bigger than maximum number of levels in a collection (3).


So, I reached my goal but I think this code, like code for counting nests, can be more neat (@FMc - big thanks). I would be grateful for any tips because I'm rather a beginner. Do you have any?

New contributor
Konrad is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
• Too short for a review. But two minor nitpicks. Your use of Pascal Case is not idomatic to python (functions should use snake_case), nor is your overuse of comments. Please explain your code through 1 modules 2 functions 3 docstring 4 variable names 5 comments. Comments should explain why not how, the algorithm (if terse) can be placed in the docstring. yesterday

Your previous question dealt with counting the number of levels in a nested data structure. This question deals with collecting values from a specific level (sort of). In both cases, your conceptualization of the problem strikes me as somewhat unintuitive, at least based on my experience with languages like Perl, Ruby, and Python.

To help clarify things, let's pretty-print your tpl_test example. I would label the levels exactly in parallel with the indentation structure. In fact, an easy (albeit sloppy) algorithm for counting the levels or determining the level of any particular value would be to JSON-ify the data structure, pretty print it, and count the spaces on the left margin. Level 0 would mean no indentation and would represent the top-level data structure. Level 1 would have an indentation of 4 spaces. And the entire data structure goes up to Level 3. By contrast, you were counting only two levels for this example. There's no "right" answer here, but the approach I'm suggesting seems more congruent with the way that software engineers typically think about the topic. Also, as we saw in my implementation of the depth() function in your previous question, this way of counting the levels composes more simply within a recursive context.

(                       # Level 0
1,                  # Level 1
[
3,              # Level 2
4,
{
5,          # Level 3
6,
7,
},
],
{
"a": 5,
"b": [
9,
8,
],
},
2,
)


At first I thought that your current question was trying to collect all of the values at a given level. But that's not what your code is doing. Consider this question: what are the values at Level 3? My answer would be [5, 6, 7, 9, 8], which is directly evident from the pretty-printed data structure above. Your code's answer is different, for two reasons. First, for the current question you are labeling each level one higher than I am (your Level 3 is my Level 2). In addition, you are not collecting all values at the requested level: all of the values at your-Level-3 (ie, my-Level-2) are [3, 4, {5,6,7}, 5, [9,8]]. But your code returns only a subset of those value -- namely, only the supported collection types at that level, or [{5,6,7}, [9,8]]. Again, neither approach is right or wrong; however, I do think that the approach I'm suggesting is a bit more idiomatic.

So how would one collect the values from a level, as I have framed the problem? It would have the same structure as my depth() implementation. First we have to distinguish between three cases: (a) tuple, list, set, (b) dict, or (c) something else. If something-else, we do nothing. If we're currently at the desired level, we want to collect the values at that level. And if we are not yet at the desired level, we want to dive deeper into the structure, using recursion. The tricky part is how to collect values from a recursively implemented function? For example, values can reside at Level 3 in multiple places within the structure, and we need to collect them all. One easy way to do that is to think of the function as emitting many values rather than returning a single unified answer from one call. That's what Python's yield can do. The function will yield/emit the desired data, and the code calling the function will glue all of that emitted data into a single list or tuple.

def get_vals_from_level(obj, level):
xs = (
obj if isinstance(obj, (tuple, list, set)) else
obj.values() if isinstance(obj, dict) else
None
)
if xs is not None:
if level == 1:
yield from xs
else:
for x in xs:
yield from get_vals_from_level(x, level - 1)

print(list(get_vals_from_level(tpl_test, 3)))  # [5, 6, 7, 9, 8]