ชีวิตใน Python 3

Aug 27 2020

ฉันได้เริ่มเรียนรู้ Python และเลือกเกมแห่งชีวิตของคอนเวย์เป็นโปรแกรมแรกของฉัน ฉันสนใจที่จะอ่านวิธีการเขียน Python ที่เป็นสำนวนมากขึ้น นอกจากนี้สิ่งที่ทำให้ฉันผิดหวังในบางครั้งก็คือทุกอย่างถูกส่งผ่านโดยการอ้างอิงและการกำหนดรายการไม่ได้คัดลอกค่า แต่คัดลอกข้อมูลอ้างอิง ดังนั้นฉันจึงใช้ฟังก์ชัน deepcopy แต่ฉันคิดว่ารายการอาจเป็นตัวเลือกที่ผิดในกรณีนี้ อะไรจะเป็นทางเลือกที่ดีกว่าใน 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("")

คำตอบ

5 Carcigenicate Aug 27 2020 at 22:05

ฉันคิดว่า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

สตริงที่'.'ลอยไปมาจะอยู่ในหมวดหมู่ของ "magic numbers": ค่าที่ใช้แบบหลวม ๆ ในโปรแกรมที่ไม่มีความหมายอธิบายตัวเอง หากจุดประสงค์ของค่าไม่ชัดเจนในตัวเองให้เก็บไว้ในตัวแปรที่มีชื่อที่สื่อความหมายเพื่อให้คุณและผู้อ่านทราบว่าเกิดอะไรขึ้นในโค้ด

โดยส่วนตัวแล้วเมื่อฉันเขียนการใช้งาน GoL ฉันใช้รายการค่าบูลีน 1D หรือ 2D หรือชุดของสิ่งที่เป็นตัวแทนของเซลล์ที่มีชีวิต สำหรับเวอร์ชันรายการบูลีนหากเซลล์ยังมีชีวิตอยู่จะเป็นจริงและถ้าเซลล์ตายแล้วก็เป็นเท็จ สำหรับเวอร์ชันที่กำหนดเซลล์จะมีชีวิตอยู่หากอยู่ในชุดมิฉะนั้นเซลล์จะตาย


ฉันจะเก็บทุกอย่างที่ด้านล่างลงในmainฟังก์ชัน คุณไม่จำเป็นต้องทำงานทั้งหมดเพียงเพราะคุณโหลดไฟล์


เพื่อประสิทธิภาพแทนที่จะสร้างสำเนาฟิลด์ใหม่ทุกรุ่นอย่างต่อเนื่องเคล็ดลับทั่วไปคือการสร้างสองรายการที่ถูกต้องตั้งแต่เริ่มต้นจากนั้นสลับทุกรุ่น

วิธีที่ฉันทำคือฟิลด์หนึ่งคือและอีกฟิลด์write_fieldหนึ่งคือread_field. ในฐานะที่เป็นชื่อแนะนำทั้งหมดเขียนเกิดขึ้นกับและทั้งหมดอ่านจากwrite_field read_fieldหลังจาก "ติ๊ก" แต่ละครั้งคุณก็แค่สลับมัน read_fieldกลายเป็นใหม่write_fieldและกลายเป็นwrite_field read_fieldวิธีนี้ช่วยให้คุณประหยัดค่าdeepcopyโทรราคาแพงหนึ่งครั้งต่อครั้ง

คุณสามารถทำการ swap ได้อย่างง่ายดายใน Python :

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

ความคิดเห็นที่ 1

ไม่จำเป็นต้องมีเคสพิเศษสำหรับการพิมพ์ Generation 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'):

ฟังก์ชั่นนี้รับพารามิเตอร์สองตัว แต่ไม่มีการเรียกใช้มันเลยดังนั้นในความเป็นจริงแล้วมันเป็นเพียงตัวแปรภายในและไม่ควรอยู่ในนิยามฟังก์ชัน

ความคิดเห็นที่ 4

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

นี่เป็นการทำซ้ำโดยไม่จำเป็นและอ่านยาก ฉันอยากจะโยงทั้ง 3 เส้นให้เป็นเส้นเดียว

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 ด้วยอักขระอื่นก่อนที่จะพิมพ์

3 FMc Aug 29 2020 at 01:12

รหัสที่คุณโพสต์นำเสนอตัวอย่างที่ดีของประโยชน์ที่สามารถไหลลงมาจากการลงทุนล่วงหน้ามากขึ้นในความสอดคล้องกันของแนวคิดและการตั้งชื่อ ตามที่เขียนรหัสมีสองวิธีที่แตกต่างกันเพื่อเป็นตัวแทนของเซลล์มีชีวิตอยู่หรือตายก็สลับไปมาระหว่างภาษาของแถว / คอลัมน์และภาษาของ x / y พิกัดและมันสลับไปมาระหว่างและfieldfield_copy

เมื่อคุณบรรลุจุดนั้นในการพัฒนาโปรแกรมการถอยกลับและมุ่งมั่นที่จะรักษาความสม่ำเสมอนั้นมีประโยชน์ ตัวอย่างเช่น:

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

r     : row index
c     : column index

และเรามาเริ่มต้นบนรากฐานที่มั่นคงด้วยการใส่โค้ดทั้งหมดลงในฟังก์ชันเพิ่มความยืดหยุ่นเล็กน้อยในการใช้งานเพื่อให้เราสามารถเปลี่ยน N ของรุ่นในบรรทัดคำสั่งได้ (สะดวกสำหรับการดีบักและการทดสอบ) นอกจากนี้เราต้องการให้มีการแยกส่วนที่เข้มงวดระหว่างส่วนอัลกอริทึมของโปรแกรมและส่วนของโปรแกรมที่เกี่ยวข้องกับการพิมพ์และการนำเสนอ วิธีหนึ่งในการเริ่มต้นเส้นทางนั้น:

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()มาใช้ซึ่งเรารู้ว่าจะทำให้เราคิดถึงเซลล์ข้างเคียง ในสถานการณ์กริด 2 มิติเหล่านี้ตรรกะเพื่อนบ้านสามารถทำให้ง่ายขึ้นโดยการแสดงเพื่อนบ้านในรูปแบบสัมพัทธ์(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และการทำงานส่วนหนึ่งด้วยเหตุผลเดียวกันและส่วนหนึ่งเป็นเพราะพวกเขามีบริบทของภาชนะแจ้งชื่อที่dcNEIGHBOR_SHIFTS