So, why doesn't it work? Let's add some type annotations and run a type checker like MyPy, those help me personally figure out a lot of bugs:
def __bacteria(mass_list: list[int], count: int, option_list: list[int]) -> list[list[int]]:
if len(mass_list) == count:
return option_list # error: list[int] given but list[list[int]] expected
return __bacteria(mass_list, count + 1,
option_list.append(mass_list[count]) # error: None given but list[int] expected
) or __bacteria(mass_list , count + 1, option_list)
Alright, so it shows 2 errors so far. What's the first one? Well, you return the current option, rather than a list of the current option.
As for the second one, list.append
modifies the list in place, and returns None
. Neither of those are what you want. What you want is, as suggested by @Michael Butscher: option_list + [mass_list[count]]
. So now we have:
def __bacteria(mass_list: list[int], count: int, option_list: list[int]) -> list[list[int]]:
if len(mass_list) == count:
return [option_list]
return (__bacteria(mass_list, count + 1, option_list + [mass_list[count]]) or
__bacteria(mass_list, count + 1, option_list))
That should not give any errors when running a type checker or when running the code. But I bet it prints out the wrong thing. How could that happen?
The base case (if len(mass_list) == count
) should be fine. The first recursive call seems fine too, and the second recursive call is fine as well. What's left? Only the or
.
Now what does or
do when given lists as arguments? Try it out on the interactive interpreter. What is the result of [] or []
? And the result of [1] or [2]
? And the result of [] or [0]
? What is the logical rule that or
implements when given two lists?
So what do we need here? Well, we know that __bacteria
returns a list of lists (list[list[int]]
to be precise) and we need to return a list of lists that contains the items of the list returned by the first recursive call and the items of the list returned by the second recursive call. That's what +
does! So now we have:
def __bacteria(mass_list: list[int], count: int, option_list: list[int]) -> list[list[int]]:
if len(mass_list) == count:
return [option_list]
return (__bacteria(mass_list, count + 1, option_list + [mass_list[count]]) +
__bacteria(mass_list, count + 1, option_list))
We can do better than this. For one, we're building up a list by creating a lot of smaller lists and copying them to concatenate them multiple times. Try giving a larger mass_list
, and see how your program slows down.
One way to make it faster and use less memory is by using a generator as a helper function. I'll rename the function and arguments to make things clearer:
from typing import Iterator
def powerset(items: list[int]) -> list[list[int]]:
return list(powerset_helper(items, 0, []))
def powerset_helper(items: list[int], index: int, to_include: list[int]) -> Iterator[list[int]]:
if index == len(items):
yield to_include
return
yield from powerset_helper(items, index + 1, to_include + [items[index]])
yield from powerset_helper(items, index + 1, to_include)
We can do even better than this, though, which is to simply use the answer by @Dalmas Otieno! :)