LEBEN in Python 3
Ich habe angefangen, Python zu lernen und habe Conways Spiel des Lebens als mein erstes Programm gewählt. Ich würde gerne lesen, wie man mehr idiomatisches Python schreibt. Was mich auch für einige Zeit abschreckte, war, dass alles als Referenz übergeben wird und die Zuweisung einer Liste nicht ihre Werte kopiert, sondern die Referenz kopiert. Daher habe ich die Deepcopy-Funktion verwendet, aber ich denke, dass Listen in diesem Fall die falsche Wahl sein könnten. Was wäre eine bessere Wahl in Python?
""" Implementation of LIFE """
import copy
# PARAMETERS
# Number of generations to simulate
N_GENERATIONS = 10
# Define the field. Dots (.) are dead cells, the letter "o" represents living cells
INITIAL_FIELD = \
"""
...................
...................
...................
...................
.ooooo.ooooo.ooooo.
...................
...................
...................
...................
"""
# FUNCTIONS
def print_field(field_copy, dead_cells=' ', living_cells='x'):
"""Pretty-print the current field."""
field_string = "\n".join(["".join(x) for x in field_copy])
field_string = field_string.replace('.', dead_cells)
field_string = field_string.replace('o', living_cells)
print(field_string)
def get_neighbours(field_copy, x, y):
"""Get all neighbours around a cell with position x and y
and return them in a list."""
n_rows = len(field_copy)
n_cols = len(field_copy[0])
if y == 0:
y_idx = [y, y+1]
elif y == n_rows - 1:
y_idx = [y-1, y]
else:
y_idx = [y-1, y, y+1]
if x == 0:
x_idx = [x, x+1]
elif x == n_cols - 1:
x_idx = [x-1, x]
else:
x_idx = [x-1, x, x+1]
neigbours = [field_copy[row][col] for row in y_idx for col in x_idx if (row, col) != (y, x)]
return neigbours
def count_living_cells(cell_list):
"""Count the living cells."""
accu = 0
for cell in cell_list:
if cell == 'o':
accu = accu + 1
return accu
def update_field(field_copy):
"""Update the field to the next generation."""
new_field = copy.deepcopy(field_copy)
for row in range(len(field_copy)):
for col in range(len(field_copy[0])):
living_neighbours = count_living_cells(get_neighbours(field_copy, col, row))
if living_neighbours < 2 or living_neighbours > 3:
new_field[row][col] = '.'
elif living_neighbours == 3:
new_field[row][col] = 'o'
return new_field
# MAIN
# Convert the initial playfield to an array
field = str.splitlines(INITIAL_FIELD)
field = field[1:] # Getting rid of the empty first element due to the multiline string
field = [list(x) for x in field]
print("Generation 0")
print_field(field)
for generation in range(1, N_GENERATIONS+1):
field = update_field(field)
print(f"Generation {generation}")
print("")
print_field(field)
print("")
Antworten
Ich denke, Ihre get_neighborFunktion kann mit minund maxund unter Verwendung von ranges bereinigt werden :
def get_neighbours(field_copy, x, y):
"""Get all neighbours around a cell with position x and y
and return them in a list."""
n_rows = len(field_copy)
n_cols = len(field_copy[0])
min_x = max(0, x - 1)
max_x = min(x + 1, n_cols - 1)
min_y = max(0, y - 1)
max_y = min(y + 1, n_rows - 1)
return [field_copy[row][col]
for row in range(min_y, max_y + 1)
for col in range(min_x, max_x + 1)
if (row, col) != (y, x)]
Es ist immer noch ziemlich lang, aber es beseitigt all das unordentliche ifVersenden an fest codierte Indexlisten. Ich habe auch das Listenverständnis über ein paar Zeilen aufgeteilt. Immer wenn mein Verständnis etwas länger wird, breche ich es so auf. Ich finde, dass es die Lesbarkeit erheblich verbessert.
Zum
"\n".join(["".join(x) for x in field_copy])
Du brauchst nicht das []:
"\n".join("".join(x) for x in field_copy)
Ohne die eckigen Klammern ist es ein Generatorausdruck anstelle eines Listenverständnisses. Sie sind faul, was Sie davon abhält, eine Liste zu erstellen, damit sie eingespeist werden kann join. Der Unterschied ist hier nicht groß, aber für lange Listen, die Speicher sparen können.
Ich würde das Board nicht als 2D-Liste von Zeichenfolgen darstellen. Dies verbraucht wahrscheinlich mehr Speicher als nötig, und vor allem, wenn Sie es jetzt haben, müssen Sie sich merken, welches Zeichenfolgensymbol was darstellt. Darüber hinaus haben Sie zwei Sätze von Zeichenfolgensymbolen: einen intern für Logik ( 'o'und '.') und einen für den Ausdruck ( ' 'und 'x'). Das ist verwirrender als es sein muss.
Wenn Sie wirklich Zeichenfolgen verwenden möchten, sollten Sie oben eine globale Konstante haben, die klar definiert, welche Zeichenfolge was ist:
DEAD_CELL = '.' # At the very top somewhere
ALIVE_CELL = 'o'
. . .
if living_neighbours < 2 or living_neighbours > 3: # Later on in a function
new_field[row][col] = DEAD_CELL
elif living_neighbours == 3:
new_field[row][col] = ALIVE_CELL
Zeichenfolgen wie das '.'Herumschweben fallen in die Kategorie "magische Zahlen": Werte, die lose in einem Programm verwendet werden, das keine selbsterklärende Bedeutung hat. Wenn der Zweck eines Werts nicht selbstverständlich ist, speichern Sie ihn in einer Variablen mit einem beschreibenden Namen, damit Sie und Ihre Leser genau wissen, was im Code vor sich geht.
Persönlich verwende ich beim Schreiben von GoL-Implementierungen eine 1D- oder 2D-Liste von Booleschen Werten oder eine Reihe von Tupeln, die lebende Zellen darstellen. Wenn für die booleschen Listenversionen eine Zelle lebt, ist sie wahr, und wenn sie tot ist, ist sie falsch. In der Set-Version lebt eine Zelle, wenn sie sich im Set befindet, andernfalls ist sie tot.
Ich würde das ganze Zeug unten in eine mainFunktion stecken . Sie möchten nicht unbedingt immer, dass all das ausgeführt wird, nur weil Sie die Datei geladen haben.
Aus Gründen der Effizienz besteht ein häufiger Trick darin, gleich zu Beginn zwei neue Feldkopien zu erstellen und diese dann bei jeder Generation auszutauschen, anstatt ständig neue Feldkopien zu erstellen.
So wie ich es mache, ist ein Feld das write_fieldund eines das read_field. Wie die Namen andeuten, passieren alle Schreibvorgänge dem write_fieldund alle lesen aus read_field. Nach jedem "Häkchen" tauschen Sie sie einfach aus. read_fieldwird das Neue write_fieldund write_fieldwird read_field. Dies erspart Ihnen den teuren deepcopyAnruf einmal pro Tick.
Sie können diesen Tausch ganz einfach in Python durchführen :
write_field, read_field = read_field, write_field
Kommentar 1
Für den Druck der Generation 0 ist kein Sonderfall erforderlich.
Lassen Sie Ihren Bereich einfach bei 0 beginnen und drucken Sie, bevor Sie aktualisieren.
for generation in range(N_GENERATIONS+1):
print(f"Generation {generation}")
print("")
print_field(field)
print("")
field = update_field(field)
Kommentar 2
Es sieht auch so aus, als würden Sie Ihren Code ziemlich INITIAL_FIELDgenau an die Art und Weise anpassen, wie Sie ihn als mehrzeilige Zeichenfolge definieren, nur weil er im Codefenster so gut aussieht. Das ist rückwärts.
Sie sollten es lieber als eine Liste von Zeichenfolgen definieren, damit Sie vor dem Starten des Programms keine Splitlines und andere Dinge ausführen müssen. Wenn Sie es dennoch lesbar machen möchten, können Sie einige Zeilenumbrüche verwenden \ (falls erforderlich), aber ich denke, die Syntax wird auch ohne diese in Ordnung sein.
INITIAL_FIELD = [
"...................",
"...................",
etc
]
Kommentar 3
def print_field(field_copy, dead_cells=' ', living_cells='x'):
Diese Funktion akzeptiert zwei Parameter, aber kein Aufruf gibt sie jemals weiter. Sie sind also nur interne Variablen und sollten nicht in der Funktionsdefinition enthalten sein.
Kommentar 4
field_string = field_string.replace('.', dead_cells)
field_string = field_string.replace('o', living_cells)
print(field_string)
Dies ist unnötige Wiederholung und schwer zu lesen. Ich würde diese 3 Zeilen lieber zu einer verketten
print(field_string.replace('.', dead_cells).replace('o', living_cells))
Kommentar 5
def count_living_cells(cell_list):
"""Count the living cells."""
accu = 0
for cell in cell_list:
if cell == 'o':
accu = accu + 1
return accu
Dies ist auch rückwärts, da Sie Ihre Zellen als Zeichen und Zeichenfolgen darstellen.
Ich denke, es wäre sinnvoller, einfache Programmlogik zu priorisieren und die Druckfunktionen nach Bedarf anpassen zu lassen. Wenn Sie lebende Zellen als Nummer 1 und tote Zellen als Nummer 0 darstellen, sieht eine Zellenliste so aus, [0,1,1,0,0,1,0]und diese Funktion könnte wie folgt geschrieben werden
return sum(cell_list)
Eigentlich würden Sie nicht einmal mehr eine Funktion brauchen, da diese so kurz ist.
In Ihrer Druckfunktion können Sie dann vor dem Drucken 1 durch ein anderes Zeichen und 0 durch ein anderes Zeichen ersetzen.
Der von Ihnen veröffentlichte Code bietet ein gutes Beispiel für die Vorteile, die sich aus einer größeren Investition in konzeptionelle und Namenskonsistenz ergeben können. Wie geschrieben, hat der Code zwei verschiedene Möglichkeiten, lebende oder tote Zellen darzustellen. Er wechselt zwischen der Sprache der Zeilen / Spalten und der Sprache der x / y-Koordinaten hin und her und wechselt zwischen fieldund field_copy.
Wenn Sie diesen Punkt in der Entwicklung eines Programms erreichen, ist es hilfreich, einen Schritt zurückzutreten und sich zu einer gewissen Konsistenz zu verpflichten. Zum Beispiel:
field : list of rows
row : list of cells
cell : either 'x' (alive) or space (dead)
r : row index
c : column index
Beginnen wir auch auf einer soliden Grundlage, indem wir den gesamten Code in Funktionen einfügen und der Verwendung ein wenig Flexibilität hinzufügen, damit wir die Anzahl der Generationen in der Befehlszeile variieren können (praktisch zum Debuggen und Testen). Darüber hinaus möchten wir eine strikte Trennung zwischen den algorithmischen Teilen des Programms und den Teilen des Programms, die sich mit Drucken und Präsentieren befassen, beibehalten. Hier ist eine Möglichkeit, auf diesem Weg zu beginnen:
import sys
ALIVE = 'x'
DEAD = ' '
INITIAL_FIELD_TEMPLATE = [
' ',
' ',
' ',
' ',
' xxxxx xxxxx xxxxx ',
' ',
' ',
' ',
' ',
]
DEFAULT_GENERATIONS = 10
def main(args):
# Setup: initial field and N of generations.
init = [list(row) for row in INITIAL_FIELD_TEMPLATE]
args.append(DEFAULT_GENERATIONS)
n_generations = int(args[0])
# Run Conway: we now have the fields for all generations.
fields = list(conway(n_generations, init))
# Analyze, report, whatever.
for i, f in enumerate(fields):
s = field_as_str(f)
print(f'\nGeneration {i}:\n{s}')
def conway(n, field):
for _ in range(n + 1):
yield field # Temporary implementation.
def field_as_str(field):
return '\n'.join(''.join(row) for row in field)
if __name__ == '__main__':
main(sys.argv[1:])
Ausgehend von dieser Grundlage besteht der nächste Schritt darin, conway()etwas Interessantes zu tun - nämlich das Feld für die nächste Generation zu berechnen. Die new_field()Implementierung ist einfach, wenn wir einige Bereichskonstanten definieren.
RNG_R = range(len(INITIAL_FIELD_TEMPLATE))
RNG_C = range(len(INITIAL_FIELD_TEMPLATE[0]))
def new_field(field):
return [
[new_cell_value(field, r, c) for c in RNG_C]
for r in RNG_R
]
def new_cell_value(field, r, c):
return field[r][c] # Temporary implementation.
Und dann ist der nächste Schritt die Implementierung eines Real new_cell_value(), von dem wir wissen, dass es uns dazu bringt, über benachbarte Zellen nachzudenken. In diesen 2D-Rastersituationen kann die Nachbarlogik häufig vereinfacht werden, indem die Nachbarn (R, C)in einer einfachen Datenstruktur relativ ausgedrückt werden:
NEIGHBOR_SHIFTS = [
(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1),
]
def new_cell_value(field, r, c):
n_living = sum(
cell == ALIVE
for cell in neighbor_cells(field, r, c)
)
return (
field[r][c] if n_living == 2 else
ALIVE if n_living == 3 else
DEAD
)
def neighbor_cells(field, r, c):
return [
field[r + dr][c + dc]
for dr, dc in NEIGHBOR_SHIFTS
if (r + dr) in RNG_R and (c + dc) in RNG_C
]
Ein letzter Hinweis: Indem wir eine konsistente Namenskonvention anwenden und das Problem in relativ kleine Funktionen zerlegen, können wir mit vielen kurzen Variablennamen davonkommen, was das visuelle Gewicht des Codes erleichtert und die Lesbarkeit verbessert. In kleinen Bereichen und in einem klaren Kontext (beide sind entscheidend) erhöhen kurze Variablennamen tendenziell die Lesbarkeit. Bedenken Sie neighbor_cells(): rund carbeiten Sie, weil unsere Konvention überall befolgt wird; RNG_Rund RNG_Carbeiten, weil sie auf dieser Konvention aufbauen; drund dcarbeiten teilweise aus dem gleichen Grund und teilweise, weil sie den Kontext eines explizit benannten Containers haben NEIGHBOR_SHIFTS.