Representing lists as a head and a tail is a common method, but honestly not the most Pythonic way to do things. Additionally, everything you're doing is appending lists together - this is generally inefficient due to the way memory has to be reallocated and the data has to be moved. This StackOverflow question has some interesting details. (Although I talk about efficiency, generally recursion and deeply nested generators may not be the most efficient either. Anytime you're considering performance, make sure you understand the requirements of your functionality and measure your own performance before attempting to optimize too much)
Ultimately, I think the cleanest solution would be to create a generator:
def flatten(iterable):
try:
iterator = iter(iterable)
except TypeError:
# this is potentially controversial, depending on your use case
# you may want a mutually recursive function, or a keyword argument, to
# indicate if you want to yield the input vs raise an error
yield iterable
else:
first_value = next(iterator)
if first_value is iterable:
yield first_value
else:
yield from flatten(first_value)
for value in iterator:
yield from flatten(value)
def get_flat_list(iterable, constructor=list):
return constructor(flatten(iterable))
A few things to call out - explicit isinstance
checks are not always a Pythonic solution. Many libraries and functions in the Python ecosystem make heavy use of duck typing - if it iterates like a list, then it is a list as far as that functionality is concerned. You should re-evaluate whether it is a strict requirement that lists are supported, or iterables. I suspect that you want most/all iterables to be supported, and thus see if there are other ways to do this. There are generally two ways to do this:
- Just try, and handle the error
- Direct inspection of properties to see if it can be used (e.g.
hasattr(iterable, "__getitem__") or hasattr(iterable, "__iter__")
)
In this case, I'm just try
ing. You might also notice the first_value is iterable
stuff - this is to handle cases like a string, where both "abc"
and "a"
are iterable, and you would hit infinite recursion. In this case, I'm simply making sure that if the first item of an object is itself we don't recurse forever. This probably isn't comprehensive, but it handles at least the string case.
I've also stopped restricting us to returning lists - what if you want a tuple? Some other type?
Lastly, I've renamed things to match Python's PEP8 naming conventions
As Peilonrayz pointed out in the comments, isinstance
has become more "accepted" outside of core Python development recently (generally, strict types are more in vogue overall).
For example, instead of using a try/except
, you could do isinstance(foo, collections.abc.Iterable)
instead of catching the TypeError
when we do iterator = iter(iterable)
. Note - collections.abc.Iterable
will only work for objects that define the __iter__
dunder-method, so __getitem__
based iteration won't be usable with this approach. This is another good reason to avoid using isinstance
if you have a good alternative (in our case, checking for iter(iterable)
).
Alternatively, some new proposed functionality in Python is isinstance
based - see PEP 622. Theoretically, you could come up with something like this (using sequence patterns):
match iterable:
case ((inner_head, *inner_tail), *tail):
yield inner_head
yield from flatten(inner_tail)
yield from flatten(tail)
case ():
return
case _:
yield iterable
flatten_2
? Or, what do the names of your functions signify? \$\endgroup\$