LIFE trong Python 3

Aug 27 2020

Tôi đã bắt đầu học Python và đã chọn trò chơi cuộc sống của Conway làm chương trình đầu tiên của mình. Tôi muốn đọc cách viết Python thành ngữ hơn. Ngoài ra, điều khiến tôi khó chịu trong một thời gian là mọi thứ được truyền bằng tham chiếu và việc gán danh sách không sao chép giá trị của nó mà sao chép tham chiếu. Do đó, tôi đã sử dụng hàm deepcopy, nhưng tôi nghĩ rằng danh sách có thể là lựa chọn sai trong trường hợp này. Đâu sẽ là lựa chọn tốt hơn trong 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("")

Trả lời

5 Carcigenicate Aug 27 2020 at 22:05

Tôi nghĩ rằng get_neighborchức năng của bạn có thể được làm sạch bằng cách sử dụng minmaxvà bằng cách sử dụng 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)]

Nó vẫn còn khá dài, nhưng nó sẽ biến mất với tất cả các ifdanh sách chỉ số được mã hóa cứng nhắc. Tôi cũng đã chia nhỏ phần hiểu danh sách qua một vài dòng. Bất cứ khi nào sự hiểu biết của tôi bắt đầu dài ra một chút, tôi sẽ chia nhỏ chúng ra như vậy. Tôi thấy nó giúp dễ đọc một cách đáng kể.


Đối với

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

Bạn không cần []:

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

Không có dấu ngoặc vuông, đó là một biểu thức trình tạo thay vì một danh sách hiểu. Họ lười biếng, điều này giúp bạn không phải tạo một danh sách chỉ để nó có thể được đưa vào join. Sự khác biệt ở đây không lớn, nhưng đối với danh sách dài có thể tiết kiệm bộ nhớ.


Tôi sẽ không trình bày bảng dưới dạng danh sách các chuỗi 2D. Điều này có thể sử dụng nhiều bộ nhớ hơn mức cần thiết, và đặc biệt là với cách bạn có nó bây giờ, bạn buộc phải nhớ ký hiệu chuỗi đại diện cho cái gì. Trên hết, bạn có hai bộ ký hiệu chuỗi: một bộ được sử dụng nội bộ cho logic ( 'o''.') và bộ kia dùng khi bạn in ra ( ' ''x'). Điều này là khó hiểu hơn nó cần phải được.

Nếu bạn thực sự muốn sử dụng chuỗi, bạn nên có một hằng số toàn cục ở trên cùng để xác định rõ ràng chuỗi là gì:

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

Các chuỗi như '.'trôi nổi xung quanh thuộc loại "số ma thuật": các giá trị được sử dụng lỏng lẻo trong một chương trình không có ý nghĩa tự giải thích. Nếu mục đích của một giá trị không tự rõ ràng, hãy lưu trữ nó trong một biến có tên mô tả để bạn và người đọc của bạn biết chính xác những gì đang xảy ra trong mã.

Mặc dù vậy, theo cá nhân tôi, khi tôi viết triển khai GoL, tôi sử dụng danh sách giá trị Boolean 1D hoặc 2D hoặc một tập hợp các bộ giá trị đại diện cho các ô còn sống. Đối với các phiên bản danh sách Boolean, nếu một ô còn sống, nó đúng, và nếu nó chết, nó sai. Đối với phiên bản đã thiết lập, một ô còn sống nếu nó nằm trong tập hợp, nếu không nó sẽ chết.


Tôi sẽ nhét tất cả những thứ ở dưới cùng vào một mainhàm. Bạn không nhất thiết phải luôn muốn tất cả những thứ đó chạy đơn giản chỉ vì bạn đã tải tệp.


Để đạt hiệu quả cao, thay vì liên tục tạo các bản sao trường mới mỗi thế hệ, một mẹo phổ biến là tạo hai bản sao ngay từ đầu, sau đó hoán đổi chúng mỗi thế hệ.

Cách tôi làm là một lĩnh vực là write_fieldvà một là read_field. Như tên gợi ý, tất cả các lần viết xảy ra với write_field, và tất cả các lần đọc từ read_field. Sau mỗi "tích tắc", bạn chỉ cần hoán đổi chúng; read_fieldtrở thành cái mới write_fieldwrite_fieldtrở thành read_field. Điều này giúp bạn tiết kiệm khỏi deepcopycuộc gọi đắt đỏ một lần cho mỗi tích tắc.

Bạn có thể thực hiện việc hoán đổi này khá đơn giản bằng Python :

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

Bình luận 1

Không cần phải có trường hợp đặc biệt để in Thế hệ 0.

Chỉ cần để phạm vi của bạn bắt đầu từ 0 và in trước khi bạn cập nhật.

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

Bình luận 2

Ngoài ra, có vẻ như bạn đang điều chỉnh mã của mình khá nhiều theo cách bạn xác định INITIAL_FIELDlà một chuỗi nhiều dòng, chỉ vì nó trông đẹp theo cách đó trong cửa sổ mã. Điều này là ngược.

Thay vào đó, bạn nên xác định nó như một danh sách các chuỗi để bạn không phải thực hiện phân tách và những thứ khác trên đó trước khi bắt đầu chương trình. Nếu bạn vẫn muốn làm cho nó có thể đọc được, bạn có thể sử dụng một số ngắt dòng \ (nếu cần), nhưng tôi nghĩ rằng cú pháp sẽ ổn ngay cả khi không có điều đó.

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

Bình luận 3

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

Hàm này chấp nhận hai tham số nhưng không có lệnh gọi nào chuyển chúng vào. Vì vậy, chúng thực chất chỉ là các biến bên trong và không nên có trong định nghĩa hàm.

Bình luận 4

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

Đây là sự lặp lại không cần thiết và khó đọc. Tôi thà xâu chuỗi 3 dòng đó thành một

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

Bình luận 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

Điều này cũng ngược lại, do cách bạn biểu diễn các ô của mình dưới dạng ký tự và chuỗi.

Tôi nghĩ sẽ hợp lý hơn nếu ưu tiên logic chương trình đơn giản và để các chức năng in điều chỉnh khi cần thiết. Nếu bạn biểu diễn các ô sống là số 1 và các ô chết là số 0, thì danh sách ô sẽ trông giống như thế [0,1,1,0,0,1,0]và hàm này có thể được viết dưới dạng

return sum(cell_list)

Trên thực tế, bạn thậm chí sẽ không cần một hàm nữa, vì nó quá ngắn.

Trong hàm in, bạn có thể thay thế 1 bằng một số ký tự khác và 0 bằng một số ký tự khác trước khi in.

3 FMc Aug 29 2020 at 01:12

Đoạn mã bạn đã đăng cung cấp một ví dụ điển hình về những lợi ích có thể chảy xuống từ khoản đầu tư lớn hơn trả trước về tính nhất quán về khái niệm và đặt tên. Như đã viết, mã có hai cách khác nhau để biểu thị các tế bào sống hoặc chết, nó chuyển đổi qua lại giữa ngôn ngữ của hàng / cột và ngôn ngữ của tọa độ x / y, và nó chuyển đổi giữa fieldfield_copy.

Khi bạn đạt đến điểm đó trong quá trình phát triển một chương trình, sẽ rất hữu ích nếu bạn lùi lại và cam kết với bản thân về sự nhất quán nhất định. Ví dụ:

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

r     : row index
c     : column index

Và chúng ta cũng hãy bắt đầu trên một nền tảng vững chắc bằng cách đưa tất cả mã vào các hàm, thêm một chút linh hoạt vào việc sử dụng để chúng ta có thể thay đổi N của các thế hệ trên dòng lệnh (tiện cho việc gỡ lỗi và thử nghiệm). Ngoài ra, chúng tôi muốn duy trì sự tách biệt chặt chẽ giữa các phần thuật toán của chương trình và các phần của chương trình liên quan đến in ấn và trình bày. Đây là một cách để bắt đầu trên con đường đó:

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

Bắt đầu từ nền tảng đó, bước tiếp theo là làm conway()điều gì đó thú vị - cụ thể là tính toán trường cho thế hệ tiếp theo. Việc new_field()thực hiện dễ dàng nếu chúng ta xác định một vài hằng số phạm vi.

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.

Và sau đó, bước tiếp theo là thực hiện một thực new_cell_value(), mà chúng ta biết sẽ dẫn chúng ta đến suy nghĩ về các ô lân cận. Trong các tình huống lưới 2D này, logic lân cận thường có thể được đơn giản hóa bằng cách biểu thị các lân cận theo nghĩa tương đối (R, C)trong một cấu trúc dữ liệu đơn giản:

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
    ]

Một lưu ý cuối cùng: bằng cách áp dụng một quy ước đặt tên nhất quán và bằng cách phân tách vấn đề thành các hàm khá nhỏ, chúng ta có thể loại bỏ nhiều tên biến ngắn, điều này làm giảm trọng lượng trực quan của mã và giúp dễ đọc. Trong phạm vi nhỏ và trong bối cảnh rõ ràng (cả hai đều quan trọng), các tên biến ngắn có xu hướng tăng khả năng đọc. Hãy cân nhắc neighbor_cells(): rclàm việc vì quy ước của chúng ta được tuân thủ ở khắp mọi nơi; RNG_RRNG_Clàm việc bởi vì họ xây dựng trên quy ước đó; drdchoạt động một phần vì lý do tương tự và một phần vì chúng có ngữ cảnh là vùng chứa được đặt tên rõ ràng NEIGHBOR_SHIFTS,.