12
\$\begingroup\$

The goal is to flatten an array of nested arrays of integers. For example [1, [2], [3, [4]]] should return [1, 2, 3, 4]

I'm particularly looking for some feedback on the following:

  • Is usage of TypeError exception justified?
  • Did I miss some valid edge cases in my tests?

Here is my solution including unit tests:

#!/usr/bin/env python import unittest def flatten_list(array): """ Returns a flattened array of integers from arbitrarily nested arrays of integers. >>> flatten_list([[1, 2, [3]], 4]) [1, 2, 3, 4] """ flat_array = [] for element in array: if isinstance(element, int): flat_array.append(element) elif isinstance(element, list): flat_array += flatten_list(element) else: raise TypeError("Unsupported type ({})".format( type(element).__name__)) return flat_array class FlattenListTestCase(unittest.TestCase): """Tests for flatten_list""" def test_empty_list(self): self.assertEqual(flatten_list([]), []) def test_single_integer(self): self.assertEqual(flatten_list([3]), [3]) def test_already_flattened(self): self.assertEqual(flatten_list([1, 2, 3, 4]), [1, 2, 3, 4]) def test_single_element_multiple_nested(self): self.assertEqual(flatten_list([[1]]), [1]) def test_arbitary(self): self.assertEqual(flatten_list( [1, [2], [3, 4], [[5]], 6]), [1, 2, 3, 4, 5, 6]) def test_empty_sublists(self): self.assertEqual(flatten_list([1, [[], 2]]), [1, 2]) self.assertEqual(flatten_list([[]]), []) def test_invalid_input(self): with self.assertRaises(TypeError): flatten_list(3) with self.assertRaises(TypeError): flatten_list([3, "2"]) with self.assertRaises(TypeError): flatten_list(None) if __name__ == '__main__': unittest.main() 
\$\endgroup\$

    1 Answer 1

    12
    \$\begingroup\$
    • Python normally promotes duck typing, yours is more statically typed.
    • You may want to use yield, as your current code possibly uses more memory than needed.
    • You can check if an item is iterable, and iterate it. Otherwise return the item.

      You can do this in two ways either via LBYL or EAFP.

    Just using yield can get:

    def flatten_list(array): for element in array: if isinstance(element, int): yield element elif isinstance(element, list): yield from flatten_list(element) else: raise TypeError("Unsupported type ({})".format( type(element).__name__)) 

    Using LBYL and duck typing can then get:

    import collections def flatten_list(array): for element in array: if isinstance(element, collections.abc.Iterable): yield from flatten_list(element) else: yield element 

    Using EAFP and duck typing can instead get:

    def flatten_list(array): for element in array: try: element = iter(element) except TypeError: yield element else: yield from flatten_list(element) 

    Finally as a side note, if you expect the initial array to have a depth exceeding one thousand, then you may want to instead perform the stack manipulation yourself. So you don't get a stack overflow error. This can be done with something like:

    def flatten(it): to_yield = [iter(it)] sentinel = object() while to_yield: item = next(to_yield[-1], sentinel) if item is sentinel: del to_yield[-1] continue try: item = iter(item) except TypeError: yield item else: to_yield.append(item) 
    \$\endgroup\$
    5
    • 3
      \$\begingroup\$Nice answer. One small nitpick: I would use a different name for none in the last Answer as this could be confused with None. How about sentinel?\$\endgroup\$
      – magu_
      CommentedOct 23, 2017 at 18:36
    • 1
      \$\begingroup\$@magu_ I agree, it confused another too. I've changed the sentinel.\$\endgroup\$
      – Peilonrayz
      CommentedOct 23, 2017 at 19:49
    • \$\begingroup\$Is it really duck typing you are using here? My understanding of duck typing was that it doesn't involve any explicit type checking (like isinstance) and the type of an object is determined at runtime. So in our example, we should be able to call flatten_list on both int and list objects. The problem is of course that Python doesn't support the extension of built-in datatypes.\$\endgroup\$CommentedOct 24, 2017 at 8:40
    • 1
      \$\begingroup\$@NikolayDerkach Yes the above is duck typing, even the LBYL is, as that's checking if it quacks like a duck. And "The problem is of course that Python doesn't support the extension of built-in datatypes." is 100% wrong, class MyList(list):pass works fine. And you can add to it.\$\endgroup\$
      – Peilonrayz
      CommentedOct 24, 2017 at 8:50
    • \$\begingroup\$I meant extension as in "adding methods" to built-in classes (i.e. without inheritance), i.e. so that you can do something like a = 5, b = [5]; a.flatten(); b.flatten()\$\endgroup\$CommentedOct 24, 2017 at 9:31

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.