Python3での生活
私はPythonを学び始め、最初のプログラムとしてConwayのライフゲームを選択しました。もっと慣用的なPythonを書く方法を読みたいと思います。また、しばらくの間私を失望させたのは、すべてが参照によって渡され、リストの割り当てはその値をコピーせず、参照をコピーすることでした。そのため、ディープコピー機能を使用しましたが、この場合、リストは間違った選択かもしれないと思います。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("")
回答
とget_neighborを使用minしてmax、およびrangesを使用することで、関数をクリーンアップできると思います。
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)]
それはまだかなり長いですがif、ハードコードされたインデックスのリストへの面倒なディスパッチをすべて取り除きます。また、リスト内包表記を数行に分割しました。私の理解が少し長くなり始めるときはいつでも、私はそれらをそのように分割します。読みやすさを大幅に向上させることができます。
にとって
"\n".join(["".join(x) for x in field_copy])
あなたは必要ありません[]:
"\n".join("".join(x) for x in field_copy)
角括弧がない場合、リスト内包表記ではなくジェネレータ式になります。それらは怠惰なので、リストを作成する手間を省くことができますjoin。ここでの違いはそれほど大きくはありませんが、メモリを節約できる長いリストの場合です。
ボードを文字列の2Dリストとして表現することはしません。これは必要以上にメモリを消費する可能性があり、特に現在のメモリを使用すると、どの文字列記号が何を表すかを覚えておく必要があります。その上で、あなたが持っている二組の論理のために内部的に使用されるもの(:文字列シンボルの'o'と'.')、および他のあなたがプリントアウトしたときのために(' 'と'x')を。これは、必要以上に混乱を招きます。
本当に文字列を使用したい場合は、上部にグローバル定数を設定して、どの文字列が何であるかを明確に定義する必要があります。
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
'.'浮かんでいるような文字列は、「マジックナンバー」のカテゴリに分類されます。つまり、プログラムで緩く使用され、自明の意味を持たない値です。値の目的が自明でない場合は、わかりやすい名前の変数に格納して、コードで何が起こっているかを読者と読者が正確に把握できるようにします。
ただし、個人的には、GoL実装を作成するときは、ブール値の1Dまたは2Dリスト、または生きているセルを表すタプルのセットを使用します。ブールリストバージョンの場合、セルが生きている場合はtrueであり、死んでいる場合はfalseです。セットバージョンの場合、セルがセット内にある場合はセルは生きていますが、そうでない場合はセルは死んでいます。
一番下にあるすべてのものをmain関数に押し込みます。ファイルをロードしたからといって、必ずしもすべてを実行する必要はありません。
効率を上げるために、世代ごとに常に新しいフィールドコピーを作成するのではなく、最初に2つ作成してから、世代ごとに交換するのが一般的なトリックです。
私のやり方は、1つのフィールドがでありwrite_field、もう1つがですread_field。名前が示すように、すべての書き込みはに発生し、write_fieldすべての読み取りはread_field。から行われます。各「ティック」の後、単にそれらを交換します。read_field新しいにwrite_fieldなり、にwrite_fieldなりread_fieldます。これにより、deepcopyティックごとに1回の高額な通話から解放されます。
このスワップはPythonで非常に簡単に行うことができます:
write_field, read_field = read_field, write_field
コメント1
ジェネレーション0を印刷するための特別なケースは必要ありません。
更新する前に、範囲を0から開始し、印刷してください。
for generation in range(N_GENERATIONS+1):
print(f"Generation {generation}")
print("")
print_field(field)
print("")
field = update_field(field)
コメント2
また、INITIAL_FIELDコードウィンドウで見栄えが良いという理由だけで、コードを複数行の文字列として定義する方法にかなり調整しているように見えます。これは逆です。
プログラムを開始する前に分割線やその他のことを行う必要がないように、文字列のリストとして定義する必要があります。それでも人間が読める形式にしたい場合は、\ (必要に応じて)いくつかの改行を使用できますが、それがなくても構文は問題ないと思います。
INITIAL_FIELD = [
"...................",
"...................",
etc
]
コメント3
def print_field(field_copy, dead_cells=' ', living_cells='x'):
この関数は2つのパラメーターを受け入れますが、パラメーターを呼び出すことはありません。したがって、これらは実際には単なる内部変数であり、関数定義に含めるべきではありません。
コメント4
field_string = field_string.replace('.', dead_cells)
field_string = field_string.replace('o', living_cells)
print(field_string)
これは不必要な繰り返しであり、読みにくいです。私はむしろそれらの3行を1つにつなぎたい
print(field_string.replace('.', dead_cells).replace('o', living_cells))
コメント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
セルを文字や文字列として表現する方法が原因で、これも逆になります。
単純なプログラムロジックを優先し、必要に応じて印刷機能を調整する方が賢明だと思います。生細胞を1として、死細胞を0として表すと、セルリストは次のように[0,1,1,0,0,1,0]なり、この関数は次のように記述できます。
return sum(cell_list)
実際には、これは非常に短いので、関数はもう必要ありません。
印刷機能では、印刷する前に、1を他の文字に、0を他の文字に置き換えることができます。
あなたが投稿したコードは、概念と命名の一貫性へのより大きな投資から下流に流れることができる利点の良い例を提供します。書かれたように、コードは、生きているまたは死んだ細胞を表す2つの異なる方法があり、それは、行/列とX / Y座標の言語の言語間で前後にトグルし、それが切り替えるfieldとfield_copy。
プログラムの開発でその時点に達したときは、一歩下がって一貫性を保つことが役立ちます。例えば:
field : list of rows
row : list of cells
cell : either 'x' (alive) or space (dead)
r : row index
c : column index
また、すべてのコードを関数に配置し、使用法にわずかな柔軟性を追加して、コマンドラインで世代のNを変更できるようにすることで、強固な基盤から始めましょう(デバッグとテストに便利です)。さらに、プログラムのアルゴリズム部分と、印刷および表示を処理するプログラムの部分との間の厳密な分離を維持する必要があります。そのパスを開始する1つの方法は次のとおりです。
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:])
その基盤から始めて、次のステップはconway()何か面白いことをすることです-つまり、次世代のフィールドを計算します。new_field()我々は範囲定数のカップルを定義した場合、実装は簡単です。
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.
そして、次のステップは、実際のを実装new_cell_value()することです。これにより、隣接するセルについて考えるようになります。これらの2Dグリッドの状況では、ネイバーロジックは(R, C)、単純なデータ構造で相対的な用語でネイバーを表現することで簡略化できることがよくあります。
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
]
最後に、一貫した命名規則を採用し、問題をかなり小さな関数に分解することで、多くの短い変数名を回避できます。これにより、コードの視覚的な重みが軽くなり、読みやすくなります。小さなスコープ内および明確なコンテキスト内(両方とも重要)では、短い変数名は読みやすさを向上させる傾向があります。私たちの慣習はどこでも守られているので、neighbor_cells():rを検討してくださいc。RNG_RそしてRNG_C、彼らはその慣習に基づいているので働きます。drそしてdc、同じ理由で、部分的に働き、彼らが明示的に指定コンテナのコンテキストを持っている理由の一つNEIGHBOR_SHIFTS。