@kwon: Anmerkungen zum ersten Modul:
Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Und die sollte nur ausgeführt werden wenn das Modul als Programm ausgeführt wird, aber nicht wenn man es nur importiert. Der Grund dafür dürfte eigentlich klar sein, insbesondere bei diesem Modul: Nur dann kann man das von einem anderen Modul aus importieren und den Inhalt von dort aus verwenden.
Namen sollten nicht kryptisch abekürzt werden. Bo ist der Name von einem Schaf auf Orson's Farm, aber kein angemessener Name für ein `board`. `num`, `pos`, `col` sollten wohl `number`, `position`, und `column` heissen.
Wobei `row` und `column` auch nicht ganz passen, denn die enthalten ja keine Reihe oder Spalte, sondern einen Index von einer Reihe oder Spalte. Das mag auf den ersten Blick wie Haarspalterei aussehen, aber der Code ist an der Stelle auch ein bisschen „unpythonisch“, weil der an Stellen mit Indexwerten hantiert wo man das normalerweise in Python nicht machen würde. Man kann *direkt* über die Werte einer Liste iterieren, ohne den Umweg über einen Laufindex. Falls man den *zusätzlich* zum Wert benötigt, gibt es die `enumerate()`-Funktion. Und wenn man über die Elemente von `board` iteriert, dann wäre für diese Listen der Name `row` sehr passend. Da wäre es dann verwirrend wenn man an einer Stelle im Programm den Namen `row` für eine Liste verwendet, die die tatsächlichen Zeilenwerte repräsentiert, und an einer anderen Stelle für den Index einer solchen Zeilenliste.
`str()` und ``+`` um Werte und Zeichenketten zusammenzustückeln ist eher BASIC als Python. Python hat dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten, und ab Python 3.6 f-Zeichenkettenliterale.
Die `print_board()`-Funktion sähe „pythonischer“ so aus:
Code: Alles auswählen
def print_board(board):
for row_index, row in enumerate(board):
if row_index % 3 == 0 and row_index != 0:
print("- " * (len(row) + 2))
for column_index, cell_value in enumerate(row):
if column_index % 3 == 0 and column_index != 0:
print("| ", end="")
print(f"{cell_value} ", end="")
print()
Ich würde da mit dem eleminieren von Indexwerten aber noch weiter gehen und das externe `more_itertools`-Modul bemühen.
Mit `itertools.intersperse()` lässt sich das auf zwei einzeilige Funktionen völlig ohne Indexwerte eindampfen:
Code: Alles auswählen
from more_itertools import intersperse
def format_row(row):
return " ".join(intersperse("|", map(str, row), 3))
def print_board(board):
print("\n".join(intersperse("- " * 11, map(format_row, board), 3)))
`valid()` ist kein guter Funktionsname, weil der keine Tätigkeit beschreibt. Wobei Prädikatsfunktionen, also solche die etwas prüfen und einen Wahrheitswert liefern, oft nach der Eigenschaft benannt werden die sie prüfen, also hier beispielsweise `is_valid_position()`, weil sich das bei einem ``if`` flüssig lesen lässt: ``if is_valid_position(…):``.
Die Funktion ist komplizierter als sie sein müsste, denn sie wird nur aufgerufen wenn klar ist, dass an der angegebenen Position keine Zahl steht. Also eine 0 und das `num` auf jeden Fall ≠0 ist, also ist der jeweilige ``and``-Teil von den ``if``-Bedingungen trivialerweise *immer wahr*, muss also nicht geprüft werden, womit zumindest beim Prüfen von Zeile und Spalte kein Index `i` mehr benötigt wird.
Man kann die drei Tests auch so schreiben, dass die Struktur sehr ähnlich ist, so dass man den Code der sich jeweils unterscheidet in eigene Funktionen herausziehen kann. Mit Funktionsnamen, die die drei Kommentare in der Funktion überflüssig machen, weil man das dann schon am Code ablesen kann.
`find` ist kein guter Name für eine Position.
Zwischenstand:
Code: Alles auswählen
#!/usr/bin/env python3
from itertools import chain
from more_itertools import intersperse
def format_row(row):
return " ".join(intersperse("|", map(str, row), 3))
def print_board(board):
print("\n".join(intersperse("- " * 11, map(format_row, board), 3)))
def find_empty_position(board):
for row_index, row in enumerate(board):
for column_index, cell in enumerate(row):
if cell == 0:
return (row_index, column_index)
return None
def iter_row(board, index):
return iter(board[index])
def iter_column(board, index):
return (row[index] for row in board)
def iter_box(board, box_position):
row_offset, column_offset = (coordinate * 3 for coordinate in box_position)
return (
board[row_index][column_index]
for row_index in range(row_offset, row_offset + 3)
for column_index in range(column_offset, column_offset + 3)
)
def is_valid_position(board, number, position):
row_index, column_index = position
return board[row_index][column_index] == 0 and number not in chain(
iter_row(board, row_index),
iter_column(board, column_index),
iter_box(board, (row_index // 3, column_index // 3)),
)
def solve(board):
empty_position = find_empty_position(board)
if not empty_position:
return True
row_index, column_index = empty_position
for number in range(1, 10):
if is_valid_position(board, number, (row_index, column_index)):
board[row_index][column_index] = number
if solve(board):
return True
board[row_index][column_index] = 0
return False
def main():
board = [
[7, 8, 0, 4, 0, 0, 1, 2, 0],
[6, 0, 0, 0, 7, 5, 0, 0, 9],
[0, 0, 0, 6, 0, 1, 0, 7, 8],
[0, 0, 7, 0, 4, 0, 2, 6, 0],
[0, 0, 1, 0, 5, 0, 9, 3, 0],
[9, 0, 4, 0, 6, 0, 0, 0, 5],
[0, 7, 0, 3, 0, 0, 0, 1, 2],
[1, 2, 0, 0, 0, 7, 4, 0, 0],
[0, 4, 9, 2, 0, 6, 0, 0, 7],
]
print_board(board)
solve(board)
print("_" * 20)
print_board(board)
if __name__ == "__main__":
main()
Ziemlich viele Funktionen bekommen das Board als erstes Argument und operieren darauf. Man könnte da sinnvoll eine Klasse draus machen.