HIDUP dengan Python 3

Aug 27 2020

Saya sudah mulai belajar Python dan memilih permainan hidup Conway sebagai program pertama saya. Saya akan tertarik untuk membaca cara menulis Python yang lebih idiomatis. Juga, apa yang membuat saya kesal untuk beberapa waktu adalah bahwa semuanya dilewatkan oleh referensi dan tugas daftar tidak menyalin nilainya tetapi menyalin referensi. Oleh karena itu, saya telah menggunakan fungsi deepcopy, tetapi saya berpikir bahwa daftar mungkin menjadi pilihan yang salah dalam kasus ini. Apa pilihan yang lebih baik dengan 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("")

Jawaban

5 Carcigenicate Aug 27 2020 at 22:05

Saya pikir get_neighborfungsi Anda dapat dibersihkan menggunakan mindan max, dan dengan memanfaatkan 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)]

Ini masih cukup panjang, tetapi menghapus semua ifpengiriman yang berantakan ke daftar indeks berkode keras. Saya juga memecah pemahaman daftar menjadi beberapa baris. Kapanpun pemahaman saya mulai menjadi agak panjang, saya memecahnya seperti itu. Saya merasa sangat membantu keterbacaan.


Untuk

"\n".join(["".join(x) for x in field_copy])

Anda tidak membutuhkan []:

"\n".join("".join(x) for x in field_copy)

Tanpa tanda kurung siku, ini adalah ekspresi generator, bukan pemahaman daftar. Mereka malas, yang menyelamatkan Anda dari membuat daftar hanya agar bisa dimasukkan ke dalamnya join. Perbedaannya di sini tidak besar, tetapi untuk daftar panjang yang dapat menghemat memori.


Saya tidak akan mewakili papan sebagai daftar string 2D. Ini mungkin menggunakan lebih banyak memori daripada yang diperlukan, dan terutama dengan cara Anda memilikinya sekarang, Anda dipaksa untuk mengingat simbol string yang mewakili apa. Selain itu, Anda memiliki dua set simbol string: satu digunakan secara internal untuk logika ( 'o'dan '.'), dan yang lainnya untuk saat Anda mencetak ( ' 'dan 'x'). Ini lebih membingungkan daripada yang seharusnya.

Jika Anda benar - benar ingin menggunakan string, Anda harus memiliki konstanta global di bagian atas yang dengan jelas mendefinisikan apa string itu:

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

String seperti '.'floating around termasuk dalam kategori "angka ajaib": nilai yang digunakan secara longgar dalam program yang tidak memiliki makna penjelasan sendiri. Jika tujuan nilai tidak terbukti dengan sendirinya, simpan dalam variabel dengan nama deskriptif sehingga Anda dan pembaca Anda tahu persis apa yang terjadi dalam kode.

Secara pribadi, ketika saya menulis implementasi GoL, saya menggunakan daftar nilai Boolean 1D atau 2D, atau satu set tupel yang mewakili sel hidup. Untuk versi daftar Boolean, jika sel hidup, itu benar, dan jika mati itu salah. Untuk versi set, sel akan hidup jika ada di set, jika tidak, sudah mati.


Saya akan menyelipkan semua barang di bagian bawah ke dalam sebuah mainfungsi. Anda tidak selalu ingin semua itu berjalan hanya karena Anda memuat file.


Demi efisiensi, alih-alih terus-menerus membuat salinan bidang baru setiap generasi, trik yang umum dilakukan adalah membuat dua salinan langsung di awal, lalu menukarnya setiap generasi.

Cara saya melakukannya adalah satu bidang write_fielddan satu bidang read_field. Seperti namanya, semua penulisan terjadi pada write_field, dan semua pembacaan dari read_field. Setelah setiap "centang", Anda cukup menukarnya; read_fieldmenjadi yang baru write_fielddan write_fieldmenjadi read_field. Ini menyelamatkan Anda dari deepcopypanggilan mahal sekali per centang.

Anda dapat melakukan pertukaran ini dengan cukup sederhana dengan Python :

write_field, read_field = read_field, write_field
4 user985366 Aug 27 2020 at 21:10

Komentar 1

Tidak perlu memiliki wadah khusus untuk mencetak Generasi 0.

Biarkan rentang Anda mulai dari 0 dan cetak sebelum Anda memperbarui.

for generation in range(N_GENERATIONS+1):
    print(f"Generation {generation}")
    print("")
    print_field(field)
    print("")
    field = update_field(field)

Komentar 2

Juga, sepertinya Anda menyesuaikan kode Anda sedikit dengan cara Anda mendefinisikan INITIAL_FIELDsebagai string multiline, hanya karena terlihat bagus seperti itu di jendela kode. Ini mundur.

Anda sebaiknya mendefinisikannya sebagai daftar string sehingga Anda tidak perlu melakukan garis terpisah dan hal-hal lain di dalamnya sebelum memulai program. Jika Anda masih ingin membuatnya dapat dibaca manusia, Anda dapat menggunakan beberapa jeda baris \ (jika perlu), tetapi menurut saya sintaksnya akan baik-baik saja bahkan tanpa itu.

INITIAL_FIELD = [
    "...................",
    "...................",
    etc
    ]

Komentar 3

def print_field(field_copy, dead_cells=' ', living_cells='x'):

Fungsi ini menerima dua parameter tetapi tidak ada panggilan untuk meneruskannya. Jadi sebenarnya mereka hanyalah variabel internal dan tidak boleh ada dalam definisi fungsi.

Komentar 4

field_string = field_string.replace('.', dead_cells)
field_string = field_string.replace('o', living_cells)
print(field_string)

Ini adalah pengulangan yang tidak perlu dan sulit dibaca. Saya lebih suka merangkai 3 baris itu menjadi satu

print(field_string.replace('.', dead_cells).replace('o', living_cells))

Komentar 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

Ini juga mundur, karena bagaimana Anda merepresentasikan sel Anda sebagai karakter dan string.

Akan lebih bijaksana menurut saya untuk memprioritaskan logika program sederhana dan membiarkan fungsi cetak menyesuaikan sesuai kebutuhan. Jika Anda merepresentasikan sel hidup sebagai angka 1 dan sel mati sebagai angka 0, maka daftar sel akan terlihat [0,1,1,0,0,1,0]dan fungsi ini dapat ditulis sebagai

return sum(cell_list)

Sebenarnya, Anda bahkan tidak memerlukan fungsi lagi, karena ini sangat singkat.

Dalam fungsi cetak, Anda dapat mengganti 1 dengan karakter lain dan 0 dengan karakter lain sebelum mencetak.

3 FMc Aug 29 2020 at 01:12

Kode yang Anda posting menawarkan contoh yang baik tentang manfaat yang dapat mengalir dari awal investasi yang lebih besar dalam konsistensi konseptual dan penamaan. Seperti yang tertulis, kode memiliki dua cara berbeda untuk merepresentasikan sel hidup atau mati, itu bolak-balik antara bahasa baris / kolom dan bahasa koordinat x / y, dan beralih antara fielddan field_copy.

Ketika Anda mencapai titik itu dalam pengembangan program, ada gunanya untuk mundur dan berkomitmen pada beberapa konsistensi. Sebagai contoh:

field : list of rows
row   : list of cells
cell  : either 'x' (alive) or space (dead)

r     : row index
c     : column index

Dan mari kita juga mulai dengan dasar yang kokoh dengan meletakkan semua kode dalam fungsi, menambahkan sedikit fleksibilitas pada penggunaan sehingga kita dapat memvariasikan N generasi pada baris perintah (berguna untuk debugging dan pengujian). Selain itu, kami ingin mempertahankan pemisahan yang tegas antara bagian algoritme program dan bagian program yang berhubungan dengan pencetakan dan presentasi. Inilah salah satu cara untuk memulai di jalur itu:

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:])

Berawal dari fondasi itu, langkah selanjutnya adalah membuat conway()sesuatu yang menarik - yaitu menghitung bidang untuk generasi berikutnya. The new_field()implementasi adalah mudah jika kita mendefinisikan beberapa konstanta jangkauan.

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.

Dan kemudian langkah selanjutnya adalah menerapkan yang nyata new_cell_value(), yang kita tahu akan menuntun kita untuk berpikir tentang sel tetangga. Dalam situasi kisi 2D ini, logika tetangga sering kali dapat disederhanakan dengan mengekspresikan tetangga dalam (R, C)istilah relatif dalam struktur data sederhana:

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
    ]

Satu catatan terakhir: dengan mengadopsi konvensi penamaan yang konsisten dan dengan menguraikan masalah menjadi fungsi yang cukup kecil, kita bisa lolos dengan banyak nama variabel pendek, yang meringankan bobot visual kode dan membantu keterbacaan. Dalam lingkup kecil dan dalam konteks yang jelas (keduanya penting), nama variabel pendek cenderung meningkatkan keterbacaan. Pertimbangkan neighbor_cells(): rdan cbekerja karena konvensi kita diikuti di mana-mana; RNG_Rdan RNG_Cbekerja karena mereka membangun konvensi itu; drdan dcbekerja sebagian karena alasan yang sama dan sebagian karena keduanya memiliki konteks wadah yang diberi nama secara eksplisit NEIGHBOR_SHIFTS,.