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