1
\$\begingroup\$

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: Algorithm

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.
\$\endgroup\$
1
  • \$\begingroup\$ 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. \$\endgroup\$ yesterday
2
\$\begingroup\$

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]
\$\endgroup\$

Your Answer

Konrad is a new contributor. Be nice, and check out our Code of Conduct.

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.