Search code examples
pythonpython-3.xmultidimensional-arrayappendclass-variables

All values of multi-dimensional array change on append - Python


I am having an issue where my “all_possible_cuts” array is changing when I append another cut to it.

The specific sections of my code with the issue is:

    def append_to_all_possible_cuts(next_cuts):
        """Appends the next possible way to cut the material to the array \"all_possible_cuts\" """
        Order.all_possible_cuts.append(next_cuts[:])
        previous_cuts = next_cuts
        return(previous_cuts)
...
Print(Order.all_possible_cuts)

Output: [[[19.125, 0], [16.125, 0]], [[19.125, 0], [16.125, 0]], ...[[19.125, 0], [16.125, 0]]]

Expected Output: [[19.125, 6], [16.125, 1]], [[19.125, 6], [16.125, 0]], [[19.125, 5], [16.125, 3]],...[[19.125, 0], [16.125, 0]]

I have found articles with people having the same issue, but I tried their solutions and it doesn’t seem to help.

Enter the inputs 144, 2, 19, 15, 16, 30

Here is my full code:

class Order:
    purchase_lines = []
    all_possible_cuts = []

    def get_order_information():
        """Gathers all the starting bar length and cutting information from the order and sorts from largest cut to smallest cut."""
        Order.get_starting_material_length()
        Order.get_purchase_lines()
        Order.purchase_lines = sorted(Order.purchase_lines, reverse=True)

    def get_starting_material_length():
        """Gathers the length of the stock bar for the order.  Returns starting lenght of the stock material."""
        starting_material_length = int(input("What is the length of the stock (in inches)? "))
        Order.starting_material_length = starting_material_length
    def get_purchase_lines():
        """Gathers all the cut lengths and the quantities needed for each cut length.  Adds a tolerance of 0.125 inches to each cutting length.  Appends length + quantity needed to """
        tolerance = 0.125
        num_of_cut_lengths = int(input("How many different cut lengths are there? "))
        for cut_length in range(num_of_cut_lengths):
            length = float(input("What is the length of  the cut (in inches)? "))
            qty = int(input("How many " + str(length) + " in. cuts are needed? "))
            Order.purchase_lines.append([(length + tolerance), qty])

    def generate_all_possible_cuts():
        """Generates an array of every possible way to cut a single piece of stock material"""
        first_possible_cut = Order.generate_first_possible_cut(Order.purchase_lines)
        another_cut_possible = Order.determine_if_another_cut_possible(first_possible_cut)
        previous_cuts = first_possible_cut
        while another_cut_possible:
            next_cuts = Order.generate_next_possible_cut(previous_cuts)
            previous_cuts = Order.append_to_all_possible_cuts(next_cuts)
            another_cut_possible = Order.determine_if_another_cut_possible(previous_cuts)

    def generate_first_possible_cut(purchase_lines):
        """Using the cut lengths and quantities need, cutting the most pieces possible out of the stock material, starting with the first cut length entered, until the material has remainder of 0 or all cutting lengths have been attempted."""
        possible_cut_length_and_qtys = []
        remaining_material_length = Order.starting_material_length
        for line in purchase_lines:
            possible_cut_length = line[0]
            max_qty = line[1]
            possible_qty = int(remaining_material_length // possible_cut_length)
            if possible_qty >= 0 and remaining_material_length - (possible_cut_length * possible_qty) >= 0:
                if possible_qty > max_qty:
                    possible_cut_length_and_qtys.append([possible_cut_length, max_qty])
                    remaining_material_length -= possible_cut_length * max_qty
                else:
                    possible_cut_length_and_qtys.append([possible_cut_length, possible_qty])
                    remaining_material_length -= possible_cut_length * possible_qty
        Order.remaining_material_length = remaining_material_length
        return(possible_cut_length_and_qtys)

    def determine_if_another_cut_possible(previous_cuts):
        """Determines if another cut is possible.  Returns True if possible, False if not possible."""
        for cut in previous_cuts:
            cut_qty = cut[1]
            if cut_qty > 0:
                return(True)
                break
        else:
            return(False)
    def generate_next_possible_cut(previous_cuts):
        """Using the previous counts, generates the next possible cut."""
        index_of_lowered_digit, lowered_cuts = Order.lower_smallest_digit_possible(previous_cuts)
        Order.increase_remaining_material_length(index_of_lowered_digit)
        next_cuts = Order.use_remaining_stock(index_of_lowered_digit, lowered_cuts)
        return(next_cuts)

    def append_to_all_possible_cuts(next_cuts):
        """Appends the next possible way to cut the material to the array \"all_possible_cuts\" """
        Order.all_possible_cuts.append(next_cuts[:])
        previous_cuts = next_cuts
        return(previous_cuts)

    def lower_smallest_digit_possible(previous_cuts):
        """Using the previous counts, generates the next possible cut."""
        current_cut_index = len(previous_cuts) - 1
        while current_cut_index >= 0:
            if previous_cuts[current_cut_index][1] > 0:
                previous_cuts[current_cut_index][1] -= 1
                break
            else:
                current_cut_index -= 1
        return(current_cut_index, previous_cuts)

    def increase_remaining_material_length(index_of_lowered_digit):
        """Using the previous counts, generates the next possible cut."""
        Order.remaining_material_length += Order.purchase_lines[index_of_lowered_digit][0]


    def use_remaining_stock(index_of_lowered_digit, lowered_cuts):
        """Using the previous counts, generates the next possible cut."""
        current_index = index_of_lowered_digit + 1
        while current_index < len(lowered_cuts):
            if Order.remaining_material_length >= lowered_cuts[current_index][0]:
                qty = int(Order.remaining_material_length // lowered_cuts[current_index][0])
                lowered_cuts[current_index][1] += qty
                Order.remaining_material_length -= lowered_cuts[current_index][0] * qty
            current_index += 1
        return(lowered_cuts)
Order.get_order_information()
Order.generate_all_possible_cuts()

Solution

  • Looks like you are reusing arrays (a.k.a. Python lists) when you meant to be copying them.

    To quickly prove this, I changed every return statement in the code that was returning an array to instead return copy.deepcopy(a) where a is the name of the array. The changed code then produced the expected output.

    The full changed code is included at the bottom of this answer.

    To help explain the problem in general, consider this sample code:

    a = [0, 1, 2, 3, 4]
    b = [a, a, a, a]
    print(b)
    a[0] = 123
    print(b)
    

    The output of the sample code is:

    [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]
    [[123, 1, 2, 3, 4], [123, 1, 2, 3, 4], [123, 1, 2, 3, 4], [123, 1, 2, 3, 4]]
    

    The reason is because the a array is never copied into b. The b array only gets 4 copies of the name a, not the whole array named a. More formally, this means that a was used 'by reference'.

    Full changed code:

    import copy
    
    class Order:
        purchase_lines = []
        all_possible_cuts = []
    
        def get_order_information():
            """Gathers all the starting bar length and cutting information from the order and sorts from largest cut to smallest cut."""
            Order.get_starting_material_length()
            Order.get_purchase_lines()
            Order.purchase_lines = sorted(Order.purchase_lines, reverse=True)
    
        def get_starting_material_length():
            """Gathers the length of the stock bar for the order.  Returns starting lenght of the stock material."""
            starting_material_length = int(input("What is the length of the stock (in inches)? "))
            Order.starting_material_length = starting_material_length
        def get_purchase_lines():
            """Gathers all the cut lengths and the quantities needed for each cut length.  Adds a tolerance of 0.125 inches to each cutting length.  Appends length + quantity needed to """
            tolerance = 0.125
            num_of_cut_lengths = int(input("How many different cut lengths are there? "))
            for cut_length in range(num_of_cut_lengths):
                length = float(input("What is the length of  the cut (in inches)? "))
                qty = int(input("How many " + str(length) + " in. cuts are needed? "))
                Order.purchase_lines.append([(length + tolerance), qty])
    
        def generate_all_possible_cuts():
            """Generates an array of every possible way to cut a single piece of stock material"""
            first_possible_cut = Order.generate_first_possible_cut(Order.purchase_lines)
            another_cut_possible = Order.determine_if_another_cut_possible(first_possible_cut)
            previous_cuts = first_possible_cut
            while another_cut_possible:
                next_cuts = Order.generate_next_possible_cut(previous_cuts)
                previous_cuts = Order.append_to_all_possible_cuts(next_cuts)
                another_cut_possible = Order.determine_if_another_cut_possible(previous_cuts)
    
        def generate_first_possible_cut(purchase_lines):
            """Using the cut lengths and quantities need, cutting the most pieces possible out of the stock material, starting with the first cut length entered, until the material has remainder of 0 or all cutting lengths have been attempted."""
            possible_cut_length_and_qtys = []
            remaining_material_length = Order.starting_material_length
            for line in purchase_lines:
                possible_cut_length = line[0]
                max_qty = line[1]
                possible_qty = int(remaining_material_length // possible_cut_length)
                if possible_qty >= 0 and remaining_material_length - (possible_cut_length * possible_qty) >= 0:
                    if possible_qty > max_qty:
                        possible_cut_length_and_qtys.append([possible_cut_length, max_qty])
                        remaining_material_length -= possible_cut_length * max_qty
                    else:
                        possible_cut_length_and_qtys.append([possible_cut_length, possible_qty])
                        remaining_material_length -= possible_cut_length * possible_qty
            Order.remaining_material_length = remaining_material_length
            return copy.deepcopy(possible_cut_length_and_qtys)
    
        def determine_if_another_cut_possible(previous_cuts):
            """Determines if another cut is possible.  Returns True if possible, False if not possible."""
            for cut in previous_cuts:
                cut_qty = cut[1]
                if cut_qty > 0:
                    return(True)
                    break
            else:
                return(False)
    
        def generate_next_possible_cut(previous_cuts):
            """Using the previous counts, generates the next possible cut."""
            index_of_lowered_digit, lowered_cuts = Order.lower_smallest_digit_possible(previous_cuts)
            Order.increase_remaining_material_length(index_of_lowered_digit)
            next_cuts = Order.use_remaining_stock(index_of_lowered_digit, lowered_cuts)
            return copy.deepcopy(next_cuts)
    
        def append_to_all_possible_cuts(next_cuts):
            """Appends the next possible way to cut the material to the array \"all_possible_cuts\" """
            Order.all_possible_cuts.append(next_cuts[:])
            previous_cuts = next_cuts
            return copy.deepcopy(previous_cuts)
    
        def lower_smallest_digit_possible(previous_cuts):
            """Using the previous counts, generates the next possible cut."""
            current_cut_index = len(previous_cuts) - 1
            while current_cut_index >= 0:
                if previous_cuts[current_cut_index][1] > 0:
                    previous_cuts[current_cut_index][1] -= 1
                    break
                else:
                    current_cut_index -= 1
            return(current_cut_index, copy.deepcopy(previous_cuts))
    
        def increase_remaining_material_length(index_of_lowered_digit):
            """Using the previous counts, generates the next possible cut."""
            Order.remaining_material_length += Order.purchase_lines[index_of_lowered_digit][0]
    
    
        def use_remaining_stock(index_of_lowered_digit, lowered_cuts):
            """Using the previous counts, generates the next possible cut."""
            current_index = index_of_lowered_digit + 1
            while current_index < len(lowered_cuts):
                if Order.remaining_material_length >= lowered_cuts[current_index][0]:
                    qty = int(Order.remaining_material_length // lowered_cuts[current_index][0])
                    lowered_cuts[current_index][1] += qty
                    Order.remaining_material_length -= lowered_cuts[current_index][0] * qty
                current_index += 1
            return copy.deepcopy(lowered_cuts)
    
    Order.get_order_information()
    Order.generate_all_possible_cuts()
    print(Order.all_possible_cuts)
    # test with: 144, 2, 19, 15, 16, 30