Python 3에서의 생활
저는 Python을 배우기 시작했고 Conway의 삶의 게임을 제 첫 프로그램으로 선택했습니다. 좀 더 관용적 인 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하고 s max를 사용 하여 정리할 수 있다고 생각합니다 range.
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 목록 또는 살아있는 셀을 나타내는 튜플 집합을 사용합니다. 부울 목록 버전의 경우 셀이 살아 있으면 참이고 죽으면 거짓입니다. 세트 버전의 경우 셀이 세트에 있으면 살아 있고 그렇지 않으면 죽은 것입니다.
나는 바닥에있는 모든 것을 main함수에 집어 넣을 것이다 . 단순히 파일을로드했기 때문에 모든 것이 항상 실행되는 것을 원하지는 않습니다.
효율성을 위해 매 세대마다 새로운 필드 사본을 지속적으로 생성하는 대신 일반적인 트릭은 처음에 두 개를 생성 한 다음 매 세대마다 교체하는 것입니다.
내가하는 방식은 하나의 필드 write_field이고 하나는 read_field. 이름에서 알 수 있듯이 모든 쓰기는에서 발생 write_field하고 모든 읽기는 read_field. 각 "틱"후에는 간단히 교체 할 수 있습니다. read_field새로운됩니다 write_field및 write_field된다 read_field. 이렇게하면 deepcopy틱당 한 번 값 비싼 전화를받지 않아도됩니다.
이 스왑 은 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'):
이 함수는 두 개의 매개 변수를 받지만 호출은 매개 변수를 전달하지 않습니다. 따라서 실제로는 내부 변수 일 뿐이며 함수 정의에 없어야합니다.
댓글 4
field_string = field_string.replace('.', dead_cells)
field_string = field_string.replace('o', living_cells)
print(field_string)
이것은 불필요한 반복이며 읽기 어렵습니다. 차라리 그 세 줄을 하나로 묶고 싶습니다
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 좌표의 언어를 바꾸는 생사 세포를 대표하는 두 가지 방법을 갖고, 그 사이에서 전환 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 세대를 변경할 수 있습니다 (디버깅 및 테스트에 편리함). 또한 프로그램의 알고리즘 부분과 인쇄 및 프리젠 테이션을 처리하는 프로그램 부분을 엄격하게 분리하고 싶습니다. 해당 경로에서 시작하는 한 가지 방법은 다음과 같습니다.
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.