Search code examples
pythonpython-3.xrecursionreplaceclone

How to clone an iterable and recursively replace specific values?


I'm writing a widget, that allows to specify the arrangement of its parts.

To accomplish this, I'm using the modular principle:
'Building blocks' are used to specify any order.

These 'blocks' are implemented as an enum values, where each value represents an individual component.

import enum

# The 'blocks'
class E(enum.Enum):
    A = 1
    B = 2
    C = 3

class Test():
    def __init__(self, arrangement):
        # The passed 'arrangement' is translated into the real arrangement.
        real_arrangement = []

        for a in arrangement:
            if a == E.A:
                real_arrangement.append("a_component")
            elif a == E.B:
                real_arrangement.append("b_component")
            elif a == E.C:
                real_arrangement.append("c_component")

        print(real_arrangement)


# The user can specify an arrangement...
arrangement = (E.A, E.C, E.B)

# ... and pass it to the constructor.
Test(arrangement)

# 'real_arrangement' = ("a_component", "c_component", "b_component")

Please note, that the placeholders are replaced, but the structure is the same.


However, I also like to give some freedom regarding the properties of the elements. Thus, in addition to the pure enum value, an iterable can be passed, which contains the enum value and further parameters.

# the elements are iterables themself.
arrangement = ((10, E.A),
               (20, E.C),
               (5, E.B))

# real_arrangement = ((10, "a_component"), (20, "c_component"), (5, "b_component"))

Please note, that the structure remains the same.


So I'm basically try to clone an iterable and recursively replace specific values.

Any approach I thought of is quite unreadable.
Is there perhaps already a solution that I can use?


The above code was run with Python 3.5.2.


Solution

  • This approach should work for common (container) classes.

    Parameters of the recursively_replace function:

    • original – object on which the recursively replacement should be executed.
    • replacementsdict which holds pairs of the form: value_to_replace : replacement.
    • include_original_keys - bool that determines if keys should also be replaced, in the case that original is a dict. (Default is False.)

    The function tries to use the same container classes as in the original. (Not the same container objects.)

    def recursively_replace(original, replacements, include_original_keys=False):
        """Clones an iterable and recursively replaces specific values."""
    
        # If this function would be called recursively, the parameters 'replacements' and 'include_original_keys'
        # would have to be passed each time. Therefore, a helper function with a reduced parameter list is used
        # for the recursion, which nevertheless can access the said parameters.
    
        def _recursion_helper(obj):
            #Determine if the object should be replaced. If it is not hashable, the search will throw a TypeError.
            try: 
                if obj in replacements:
                    return replacements[obj]
            except TypeError:
                pass
    
            # An iterable is recursively processed depending on its class.
            if hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, bytearray)):
                if isinstance(obj, dict):
                    contents = {}
    
                    for key, val in obj.items():
                        new_key = _recursion_helper(key) if include_original_keys else key
                        new_val = _recursion_helper(val)
    
                        contents[new_key] = new_val
    
                else:
                    contents = []
    
                    for element in obj:
                        new_element = _recursion_helper(element)
    
                        contents.append(new_element)
    
                # Use the same class as the original.
                return obj.__class__(contents)
    
            # If it is not replaced and it is not an iterable, return it.
            return obj
    
        return _recursion_helper(original)
    
    
    # Demonstration
    if __name__ == "__main__":
    
        import enum
    
        # Define an enumeration whose values should be replaced later.
        class E(enum.Enum):
            A = 1
            B = 2
            C = 3
    
        # Map the values to be replaced with their respective replacements.
        dict_with_replacements = {E.A : "a_replacement",
                                  E.B : "b_replacement",
                                  E.C : "c_replacement"}
    
        ### example 1 ###
        test = (E.A, E.C, E.B)
    
        result = recursively_replace(test, dict_with_replacements)
    
        print(result)       # ('a_component', 'c_component', 'b_component')
    
    
        ### example 2 ###
        test = ((10, E.A), (20, E.C), (5, E.B))
    
        result = recursively_replace(test, dict_with_replacements)
    
        print(result)       # ((10, 'a_component'), (20, 'c_component'), (5, 'b_component'))
    
    
        ### example 3 ###
        test = (E.A, (20, E.C), (5, E.B))
    
        result = recursively_replace(test, dict_with_replacements)
    
        print(result)       # ('a_component', (20, 'c_component'), (5, 'b_component'))
    
    
        ### example 4 & 5 ###
        test = (E.A, {20:E.C, E.B:5})
    
        result = recursively_replace(test, dict_with_replacements) 
    
        print(result)       # ('a_component', {<E.B: 2>: 5, 20: 'c_component'})
    
        result = recursively_replace(test, dict_with_replacements, True)
    
        print(result)       # ('a_component', {'b_component': 5, 20: 'c_component'})