# [Tutor] function return values

Cameron Simpson cs at cskk.id.au
Thu Dec 2 02:16:46 EST 2021

```On 02Dec2021 11:24, Phil <phillor9 at gmail.com> wrote:
>I was quite pleased with myself that I had reduced the complexity of a
>function until I tried to reuse the same code elsewhere. I had the
>return values set to some dummy values and that was good in this case.
>However, this idea failed when I tried to test if the pairs are in the
>same column. So, what is the correct way to handle a case where there
>isn't a valid pair, row or list value to return?
>
>    def boxPairsInSameRow(self, pairs, x, y):
>        col_list = []
>        for item, count in pairs.items():
>            if count == 2:  # then item is a pair candidate
>                for row in range(x, x + 3):
>                    col_list = []
>                    for col in range(y, y + 3):
>                        if item in self.solution[row][col]:
>                            col_list.append(col)
>                    return item, row, col_list
>        return None, None, None # item, row and col_list were
>originally dummy values
>
>This is the calling function:
>
>            item, row, column_list =
>self.boxPairsInSameRow(candidate_pairs, x , y)
>            if item is not None and row is not None and column_list is
>not None:
>                do this only if the returned values are valid. In
>other words, this is a pair in a row.
>
>Maybe the boxPairsInSameRow function is still too complex? The aim is
>to feed in the pairs, from a counter dictionary, and the row and
>column starting coordinates and have it return the pairs.item, the row
>that the item is on and a list containing the two columns for that row
>position. Once the pairs have been found on a row then there is no need
>to search the remaining row or rows.

For a function returning a valid result or no result you have two main
approaches in Python:
- return the result or None (or None-ish, as you are doing)
- return the result or raise a ValueError("no solution found")

The former requires a test by the caller. The latter means you need to
catch an exception for "no solution". Wordier, but it might mean you can
defer that to more outer pieces of the code, or not catch it at all if
this programme should always find a solution.

The exception approach means you would replace the last:

return None, None, None

with:

raise ValueError("no solution found") # better message needed

For the None-ish approach, you might:
- return None (or some other sentinel, but None is best if None is not
itself a valid return)
- return a (None,None,None) tuple as you have done to match the expected
result

The former requires code like this if you return None:

solution = self.boxPairsInSameRow(candidate_pairs, x, y)
if solution is None:
... no solution ...
else:
item, row, column_list = solution
... use the solution ...

or this for your None-ish approach:

item, row, column_list =_ self.boxPairsInSameRow(candidate_pairs, x , y)
if (item, row, column_list) == (None, None, None):
... no solution ...
else:
... use the solution ...

The exception approach looks like this:

try:
item, row, column_list =_ self.boxPairsInSameRow(candidate_pairs, x , y)
except ValueError as e:
warning("no box pairs found: %s", e)
... handle no solution here ...
else:
... use the solution ..

If you always expect a solution, this becomes:

item, row, column_list = self.boxPairsInSameRow(candidate_pairs, x , y)
... use the solution ..

which is very simple. I suppose if that were the case you would not need
your None-ish test in the first place.

A third alternative turns on your statement: "Once the pairs have been
found on a row then there is no need to search the remaining row or
rows". Is it _meaningful_ to search for more solutions, even if you
_currently_ just want the first one?  If it is you might recast this as
a generator:

def boxPairsInSameRow(self, pairs, x, y):
col_list = []
for item, count in pairs.items():
if count == 2:  # then item is a pair candidate
for row in range(x, x + 3):
col_list = []
for col in range(y, y + 3):
if item in self.solution[row][col]:
col_list.append(col)
yield item, row, col_list

This is a generator function, which walks your data structure and yields
solutions as they are found. Note that because it will continue after
the first solution you may need to clear some things after the "yield"
statement, for example maybe "col_list" should be emptied before
proceeding.

Then at the calling end you can iterate over the generator:

solutions = list(self.boxPairsInSameRow(candidate_pairs, x , y))

which would get a list of all the solutions. In your scenario, you only
want to search to the first solution:

solution = None
for solution in self.boxPairsInSameRow(candidate_pairs, x , y):
# exit the loop on the first solution found
break
if solution is None:
... no solution yielded ...
else:
... use the solution ...

This runs the generator to the first solution, but _does not_ continue
executing it because you exit the for-loop. So no further iteration
happens and the generator ceases execution (it stopped at the "yield",
and here you never resume it).

In fact Pythons for-loop has a little used feature: an else-clause:

for solution in self.boxPairsInSameRow(candidate_pairs, x , y):
# exit the loop on the first solution found
... use the solution ...
break
else:
... no solution yielded ...

The "else" clause runs after the for-loop finishes, _unless_ you "break"
out of the loop. So here we have the loop body handle the solution and
the "else:" handle the no solution path (you can leave the "else:" off
if there is nothing to do if there is no solution).

Cheers,
Cameron Simpson <cs at cskk.id.au>
```