Python 3'te YAŞAM
Python öğrenmeye başladım ve ilk programım olarak Conway'in hayat oyununu seçtim. Daha deyimsel Python nasıl yazılacağını okumakla ilgilenirim. Ayrıca, beni bir süredir kaçıran şey, her şeyin referansla aktarılması ve bir listenin atanmasının değerlerini kopyalamaması, referansı kopyalamasıydı. Bu nedenle, deepcopy işlevini kullandım, ancak listelerin bu durumda yanlış seçim olabileceğini düşünüyorum. Python'da daha iyi bir seçim ne olabilir?
""" 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("")
Yanıtlar
Ben senin düşünüyorum get_neighborfonksiyonu kullanılarak temizlenebilir minve maxve yararlanarak 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)]
Hala oldukça uzun, ancak ifkodlanmış indeks listelerine gönderilen tüm dağınıklığı ortadan kaldırıyor. Ayrıca liste anlayışını birkaç satıra böldüm. Anlayışım biraz uzamaya başladığında, onları böyle ayırıyorum. Okunabilirliğe önemli ölçüde yardımcı olduğunu düşünüyorum.
İçin
"\n".join(["".join(x) for x in field_copy])
Şunlara ihtiyacınız yok []:
"\n".join("".join(x) for x in field_copy)
Köşeli parantezler olmadan, bu bir liste anlayışı yerine bir üretici ifadesidir. Tembeldirler, bu da sizi sadece beslenebilmesi için bir liste oluşturmaktan kurtarır join. Buradaki fark çok büyük değil, ancak bellekten tasarruf sağlayan uzun listeler için.
Tahtayı 2B dizeler listesi olarak göstermezdim. Bu muhtemelen gerekenden daha fazla bellek kullanır ve özellikle şimdi nasıl sahip olduğunuzla, hangi dize sembolünün neyi temsil ettiğini hatırlamak zorunda kalırsınız. Bunun da ötesinde, iki dizi dize sembolünüz var: biri dahili olarak mantık için ( 'o've '.'), diğeri çıktı aldığınızda ( ' 've 'x') kullanılır. Bu olması gerekenden daha kafa karıştırıcı.
Eğer varsa gerçekten kullanımı dizeleri istediğini, ne dize açıkça tanımlar ne olduğunu üstündeki Genel bir sabit olmalıdır:
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
'.'Yüzen gibi dizeler "sihirli sayılar" kategorisine girer: bir programda gevşek olarak kullanılan ve kendi kendini açıklayıcı bir anlamı olmayan değerler. Bir değerin amacı açık değilse, onu açıklayıcı bir adla bir değişkende saklayın, böylece siz ve okuyucularınız kodda tam olarak ne olduğunu bilirsiniz.
Kişisel olarak, GoL uygulamalarını yazarken, 1D veya 2D Boole değerleri listesi veya canlı hücreleri temsil eden bir dizi tuple kullanıyorum. Boolean listesi versiyonları için, eğer bir hücre canlıysa, doğrudur ve ölüyse yanlıştır. Set versiyonu için, bir hücre sette ise canlıdır, aksi takdirde ölür.
Alt kısımdaki her şeyi bir mainişleve sokardım. Dosyayı yüklediğiniz için her zaman tüm bunların çalışmasını istemezsiniz.
Verimlilik uğruna, her nesilde sürekli olarak yeni alan kopyaları oluşturmak yerine, ortak bir numara, başlangıçta iki tane oluşturmak ve ardından bunları her nesilde değiştirmektir.
Bunu yapma şeklim, bir alan write_fieldve bir read_field. Adlardan da anlaşılacağı gibi, tüm yazılar 'a olur write_fieldve hepsi' den okur read_field. Her "işaretten" sonra, onları değiştirirsiniz; read_fieldyeni olur write_fieldve write_fieldolur read_field. Bu sizi deepcopyher tık için bir kez pahalı aramadan kurtarır .
Bu takas işlemini Python'da oldukça basit bir şekilde yapabilirsiniz :
write_field, read_field = read_field, write_field
Yorum 1
Generation 0 yazdırmak için özel bir duruma gerek yoktur.
Aralığınızın 0'dan başlamasına izin verin ve güncellemeden önce yazdırın.
for generation in range(N_GENERATIONS+1):
print(f"Generation {generation}")
print("")
print_field(field)
print("")
field = update_field(field)
Yorum 2
Ayrıca, INITIAL_FIELDkod penceresinde çok satırlı bir dize olarak tanımladığınız şekilde kodunuzu biraz değiştiriyorsunuz gibi görünüyor, çünkü kod penceresinde bu şekilde güzel görünüyor. Bu geriye doğru.
Bunu bir dizeler listesi olarak tanımlamalısınız, böylece programı başlatmadan önce bölme çizgileri ve diğer şeyler yapmak zorunda kalmazsınız. Hala insan tarafından okunabilir hale getirmek istiyorsanız, \ (gerekirse) bazı satır sonları kullanabilirsiniz , ancak sözdiziminin bu olmadan bile iyi olacağını düşünüyorum.
INITIAL_FIELD = [
"...................",
"...................",
etc
]
Yorum 3
def print_field(field_copy, dead_cells=' ', living_cells='x'):
Bu işlev iki parametreyi kabul eder, ancak hiçbir çağrı onları içeri aktarmaz. Dolayısıyla bunlar aslında yalnızca dahili değişkenlerdir ve işlev tanımında olmamalıdır.
Yorum 4
field_string = field_string.replace('.', dead_cells)
field_string = field_string.replace('o', living_cells)
print(field_string)
Bu gereksiz bir tekrar ve okunması zor. Bu 3 satırı tek bir
print(field_string.replace('.', dead_cells).replace('o', living_cells))
Yorum 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
Bu, hücrelerinizi karakterler ve dizeler olarak nasıl temsil ettiğinizden dolayı da geriye doğrudur.
Basit program mantığına öncelik vermek ve yazdırma işlevlerinin gerektiği gibi ayarlanmasına izin vermek daha mantıklı olur. Canlı hücreleri 1 sayısı ve ölü hücreleri 0 sayısı olarak temsil ederseniz, bir hücre listesi gibi görünür [0,1,1,0,0,1,0]ve bu işlev şu şekilde yazılabilir:
return sum(cell_list)
Aslında, bu çok kısa olduğu için artık bir işleve bile ihtiyacınız olmayacak.
Yazdırma işlevinizde, yazdırmadan önce 1'i başka bir karakterle ve 0'ı başka bir karakterle değiştirebilirsiniz.
Gönderdiğiniz kod, kavramsal ve adlandırma tutarlılığı açısından daha büyük bir yatırımdan aşağıya doğru akabilecek avantajlara iyi bir örnek sunar. Yazılı olarak, kod geri ve ileri satır / sütun dil ve x / y koordinatlarının dil arasındaki geçiş yapar, canlı ya da ölü hücreleri temsil etmek için iki farklı yol vardır ve o arasında geçiş fieldve field_copy.
Bir programın geliştirilmesinde bu noktaya geldiğinizde, geri adım atmak ve kendinizi bir miktar tutarlılığa adamak yararlıdır. Örneğin:
field : list of rows
row : list of cells
cell : either 'x' (alive) or space (dead)
r : row index
c : column index
Ayrıca, tüm kodu işlevlere yerleştirerek, kullanıma küçük bir esneklik ekleyerek sağlam bir temelde başlayalım, böylece komut satırındaki N neslini değiştirebiliriz (hata ayıklama ve test için kullanışlıdır). Ek olarak, programın algoritmik bölümleri ile programın yazdırma ve sunumla ilgili bölümleri arasında kesin bir ayrım olmasını istiyoruz. İşte bu yola başlamanın bir yolu:
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:])
Bu temelden başlayarak, bir sonraki adım conway()ilginç bir şey yapmaktır - yani gelecek nesil için alanı hesaplamak. new_field()Biz aralık sabitleri birkaç tanımlarsanız uygulanması oldukça kolaydır.
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.
Ve sonraki adım, new_cell_value()bizi komşu hücreler hakkında düşünmeye götüreceğini bildiğimiz bir gerçek uygulamaktır . Bu 2B grid durumlarında, komşu mantığı genellikle (R, C)basit bir veri yapısında komşuları göreceli terimlerle ifade ederek basitleştirilebilir :
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
]
Son bir not: Tutarlı bir adlandırma kuralı benimseyerek ve problemi oldukça küçük işlevlere ayırarak, kodun görsel ağırlığını hafifleten ve okunabilirliğe yardımcı olan birçok kısa değişken adından kurtulabiliriz. Küçük kapsamlar içinde ve net bir bağlam içinde (her ikisi de çok önemlidir), kısa değişken adları okunabilirliği artırma eğilimindedir . Düşünün neighbor_cells(): rve cçalışın çünkü sözleşmemiz her yerde takip ediliyor; RNG_Rve RNG_Cbu kongre üzerine inşa ettikleri için çalışırlar; drve dckısmen aynı nedenle ve kısmen de açıkça adlandırılmış bir kapsayıcı bağlamına sahip oldukları için çalışırlar NEIGHBOR_SHIFTS.