whether this selection sort algorithm implementation for number lists could be considered clean code (in the sense of being modular and maintainable)
The only style mistake you've made is that top-level items (function definitions, class definitions, etc.) in modules (source files) should be separated by two blank lines each, instead of one.
I think it's good to practise writing Python source files with the assumption that they will be modules in someone's project in the future. That sounds like your aim too. So mark "private members" (that you don't intend users to need to directly access) by prepending an underscore, and move your scripting stuff to the inside of a if __name__ == '__main__'
block if you don't want it to run when a user uses import your_module
:
def selection_sort(the_list):
for sorted_sublist_end_position in range(0, len(the_list) - 1):
smallest_item_position = _find_smallest_item_position(the_list, sorted_sublist_end_position)
_swap(the_list, sorted_sublist_end_position, smallest_item_position)
return the_list
def _find_smallest_item_position(the_list, position_before_unsorted_sublist):
current_smallest_item_position = position_before_unsorted_sublist
sublist_start_position = position_before_unsorted_sublist + 1
for unsorted_sublist_position in range(sublist_start_position, len(the_list)):
if the_list[unsorted_sublist_position] < the_list[current_smallest_item_position]:
current_smallest_item_position = unsorted_sublist_position
return current_smallest_item_position
def _swap(a_list, i, j):
aux = a_list[i]
a_list[i] = a_list[j]
a_list[j] = aux
if __name__ == '__main__':
print(selection_sort([2, 5, 4]))
print(selection_sort([5, 4, 2]))
print(selection_sort([]))
Add some docstrings and doctests too:
def selection_sort(the_list):
r'''Sort the_list in-place using selection sort.
:param the_list: a list of comparable items
:return: the sorted list
>>> a = [2, 5, 4]
>>> selection_sort(a)
[2, 4, 5]
>>> a
[2, 4, 5]
>>> selection_sort([]) # don't crash!
[]
'''
for sorted_sublist_end_position in range(0, len(the_list) - 1):
smallest_item_position = _find_smallest_item_position(the_list, sorted_sublist_end_position)
_swap(the_list, sorted_sublist_end_position, smallest_item_position)
return the_list
:param ...: ...
and :return: ...
belong to the Sphinx style docstring format, you could use any other of course.
The lines that look like REPL snippets are doctests. They provide example usage and testing facilities at the same time: run python -m doctest your_module.py
to see if all the lines starting >>>
produce the expected outputs.
It's also a good idea to use typehints wherever you can, especially in function signatures:
from typing import Protocol, TypeVar
class Comparable(Protocol):
def __lt__(self, other):
return NotImplemented
T = TypeVar('T', bound=Comparable)
def selection_sort(the_list: list[T]) -> list[T]:
...
def _find_smallest_item_position(the_list: list[T], position_before_unsorted_sublist: int) -> int:
...
def _swap(i: int, j: int) -> None:
...
Building a TypeVar and Protocol just to specify what types are sensible to pass into this self-evident usecase might be overkill, (the_list: list) -> list
is a bare minimum that in this case I'd think is good enough.
how should I go about making it more Pythonic if necessary
Use the idiomatic name lst
for generic list
instances, instead of the_list
(which I think is a great and suitable name btw, there just exists a standard one which Pythoneers are used to)
Don't specify the default 0
lower-bound in range
: range(0, len(lst) - 1)
and range(len(lst) - 1)
are the same, but the second is the idiomatic one.
Swap items using normal Python assignment syntax:
def _swap(lst: list[T], i: int, j: int) -> None:
lst[i], lst[j] = lst[j], lst[i]
- Find every excuse you can to use slices, comprehensions, and Python's built-in functions instead of explicit procedural loops. In this use a slice and a
min
:
def _find_smallest_item_position(lst: list[T], position_before_unsorted_sublist: int) -> int:
rest_of_list = lst[position_before_unsorted_sublist:]
smallest_item = min(rest_of_list)
return lst.index(smallest_item, position_before_unsorted_sublist)
Variable names are very long
I agree, your identifiers are too long which makes the code fatiguing on the eyes and mind. It's a pain to decipher code when you can't ingest an identifier at a glance.
Some of the meanings are wrong or not obvious too:
position_before_unsorted_sublist
is actually the index of the first item of the unsorted sublist, not the item before that one.
sorted_sublist_end_position
is actually the index of the first item that has not been sorted yet, not the index of the last item in the sorted-so-far sublist
sublist_start_position
is not the start position of the unsorted sublist (as its context would imply it is), instead it is the index of the second item of that sublist.
unsorted_sublist_position
is not an index relative to the unsorted sublist, instead it is an index relative to the original list. A better name is simply i
, readers can see that it is constrained to the unsorted sublist because of the lower bound of the range range(sublist_start_position, ...)
Overthinking variable names can actually obfuscate them, just use familiar identifiers like i
, start
, stop
etc. where appropriate. If you feel a need to use verbose variable names, it may also be a clue that your logic is unnecessarily complex too.
I would use these names instead:
def selection_sort(lst: list[T]) -> list[T]:
r'''Sort lst in-place using selection sort.
:param lst: a list of comparable items
:return: the sorted list
>>> a = [2, 5, 4]
>>> selection_sort(a)
[2, 4, 5]
>>> a
[2, 4, 5]
>>> selection_sort([])
[]
'''
for i in range(len(lst) - 1):
next_smallest = _locate_smallest_item(lst, i)
_swap(lst, i, next_smallest)
return lst
def _locate_smallest_item(lst: list[T], start: int) -> int:
smallest_item = min(lst[start:])
return lst.index(smallest_item, start)
I guess I could pass a single sublist to the second function, as the sublist start position is implicit, instead of two arguments. Would that make the code more Pythonic?
That would look something like this:
def selection_sort(the_list):
for i in range(len(the_list) - 1):
smallest_item_position = find_smallest_item_position(the_list[i:]) + i
swap(the_list, i, smallest_item_position)
return the_list
def find_smallest_item_position(sublist):
current_smallest_item_position = 0
for unsorted_sublist_position in enumerate(sublist):
if sublist[unsorted_sublist_position] < sublist[current_smallest_item_position]:
current_smallest_item_position = unsorted_sublist_position
return current_smallest_item_position
It might be considered more Pythonic because you're using list slicing, avoiding typically low-level language implementation details like passing indexes, and you get a chance to use enumerate
, but I don't think it's more readable in the end so I'd use your original version. I think the original function signature is more sensible because it's more similar to existing language APIs (like list.index
, range
, etc. - everything with start
parameters), more in the spirit of selection sort, and avoids the awkward + i
necessity - the meaning of which is not obvious at a glance IMO.
I don't think performance should ever be much of a consideration when writing Python. Readability, algorithmic clarity, adherence to Pythonic norms first and foremost. But generally loops are the slowest things you can do in Python, and builtins are the fastest. So a solution using slicing (which does copy the list - the snippet directly above is a fast O(n) slower than yours) and min
is way faster than one written yourself with less Pythonic tools like procedural-style loops.
Clean Code says functions should do one thing / be responsible by one thing. So I expect a function for instance to perform a single for. The second function performs both a for and a nested if, however if I extract a sub-function out of it the sub-function name will be just a rephrasing of what the sub-function does, which is not good. Should I go about doing that or is the function good enough as it is?
You may have already read these: Should a method do one thing and be good at it? and its parent duplicate Should I extract specific functionality into a function and why?
You already know it yourself, it's bad to make functions for tasks that are too small. I think the use-a-for-loop-and-variable-to-find-the-most-X-in-a-list pattern that your find_smallest_item_position
encapsulates is such an idiomatic and obvious conceptual unit that it would be a very bad idea to split it up further. You've already made correct choices about what deserves its own function.