def gen_all_coords():
for x in range(0, CELLWIDTH):
for y in range(0, CELLHEIGHT):
yield (x, y)
...
Si, corretto. Le parentesi attorno ad x e y non sono necessarie, ma al di la di quello, tutto bene.
Detto questo, parliamo di astrazione... vista come e' fatta quella funzione, io andrei di
def gen_all_coords(max_width, max_height)
o al limite
def gen_all_coords(max_width=CELLWIDTH, max_height=CELLWEIGHT)
Mi sembra ragionevole avere una funzione che lavora indifferentemente a prescindere dall'attuale area di gioco.
> Questo me lo segno, perché devo ancora studiare le classi.
Scusami... assumo quindi che non hai ancora finito la prima lettura di un manuale.
Beh, guardatele pure. Tutto sommato e' un concetto piu' facile dei generatori.
* up, down, etc... costanti.
> Vero. Suppongo che anche blocked_events alla linea 148 del modulo game.py posso inserirlo tra le costanti (ovviamente come una tupla). Si tratta di eventi che devono essere sempre ignorati.
Si. Insisto, finche' il progetto e' piccolo. Poi bisognerebbe capire meglio come fare a far sparire la maggior parte delle costanti e metterle solo a chi deve usarle.
> Mmm, sì, a ben vedere le named tuple sono il container ideale in questo contesto. Però sul mio manuale, il Lutz, si dice che quando si usano le named tuple si deve accettare un compromesso: in cambio di una maggiore versatilità si incorre in una penalità nelle performance. Quello che non specifica è l'entità di questa penalità...
Ora... se il Lutz evitasse questi commenti sarebbe meglio. Resta un ottimo libro, ma secondo me scrivere cose del genere porta fuori strada. Perche' si, formalmente ha ragione. Ma e' un tipo di commento che scoraggia un principiante per nessun motivo. Passera' parecchio tempo prima che tu debba porti il problema che il tuo programma e' troppo lento *perche' usa le namedtuples*. E se succede il fix e' semplice: usa gli indici numerici dove e' davvero critico. Il motivo e' semplicemente che per fare foo.bar lui va a cercare la chiave bar che gli da l'indice della tupla dove sta quell'attributo, e poi accede all'elemento. Se invece hai una tupla normale (o usi indici numerici con le tuple) vai diretto all'elemento. E se pure ti piacesse continuare a usare l'accesso per attributo ti puoi fare una classe che fa questa roba in modo efficiente. O quello che ti pare. Ma insisto, non e' un problema. Sinceramente per me non e' mai stato un problema, che e' dire qualcosa. Semmai salta fuori che e' proprio Python il problema.
Insomma, fai bene a non distrarti completamente sulle performance, ma specie in questa fase avrai piu' benefici scrivendo le cose in modo sensato, usando algoritmi e strutture dati giuste e cosi' via. Le micro-ottimizzazioni lasciale per quando avrai misurato un problema di performance (ovvero e' oggettivamente: non e' che fai misure e dici "potrei essere piu' veloce"; semplicemente ad un certo punto il programma non fa quello che deve nel tempo necessario). In molti casi questo non succede mai.
> Mmm, d'altro canto, qui non ho un problema di prestazioni e l'idea di usare le named tuple mi piace molto.
Appunto.
Da qualche parte su Stack Overflow ho letto che utilizzare import * è controindicato perché, vado a memoria, "si importa un sacco di robaccia inutile". Presumo quindi che la ragione di questo abbia qualcosa a che fare con la memoria. La parte del manuale che tratta dei moduli non l'ho ancora studiata (ehm, sì, mi sto rendendo conto del fatto che sarebbe il caso di continuare il mio studio), mi sono limitato a farmi una vaga idea leggendo il codice altrui.
Allora, il problema non e' in generale la memoria. Quando hai importato il modulo la maggior parte della memoria che doveva andare e' andata comunque. La variante * semplicemente prenderebbe un pochetto di piu' perche' ci sarebbero un numero di chiavi in piu' in qualche dizionario. Mi verrebbe da dire che e' eccezionale che questa cosa diventi davvero un problema. Mi aspetto che possa succedere solo in presenza di qualcosa di patologico.
Il problema, come quasi sempre in Python, e' di pura pulizia.
1. se tieni le cose qualificate, e' piu' semplice capire cosa sono.
2. e' piu' facile evitare conflitti nei nomi (i conflitti sui nomi dei moduli sono strettamente piu' rari dei conflitti sugli oggetti contenuti nei moduli) e risolverli e' estremamente piu' semplice.
In generale il problema e' che quando fai uno star import, non hai idea di cosa stai importando. Non solo: modifiche innocue alle tue dipendenze possono romperti.
Comunque il problema e' ben discusso qui.
https://www.python.org/dev/peps/pep-0008/#importsIn generale vale la pena conoscere *bene* cosa c'e' in quel documento. Tu mi sembra sia molto appassionato di scrivere codice "pulito" e "secondo best practice". Bene... amerai quel documento.
Però, poniamo il caso che io debba importare la costante RED dal modulo constants.py. Allora i casi sono due:
import constants
constants.RED
Oppure:
from constants import RED
RED
Quale usare quando devo importare un nome?
In quasi tutte le circostanze la prima delle due. Con possibilmente variazioni tipo:
from module import submodule
(e poi submodule.RED)
oppure
from myproject import constants
from bombastic import constants as bombastic_constants
Ma in generale cerco sempre di tenere la qualificazione a livello di modulo/sottomodulo
Aggiungo... cosa succede se per la tua libreria RED e' una stringa (che so... hai tre colori, 'red', 'black', 'blue'... ha senso usare costanti di questo tipo), mentre per una qualche libreria che usi RED e' una tupla di 3 byte (tipo rappresentazione RGB). Ad un certo punto qualche pezzo di codice usera' il RED sbagliato e un intern dovra' essere sacrificato sull'altare della type safety per riportare ordine nel cosmo.
Oh, poi ci sono tutta una serie di trucchetti che nessuno insegna... tipo os ha un modulo path, che contiene roba utile. Sfortunatamente path e' anche un nome comune per chiamare una variabile di tipo path. E non sempre ci sono nomi altrettanto efficaci (e' solo un path, non ha nulla che la differenzia dagli altri path).
Beh, per questo spesso faccio solo import os e poi os.path.dirname o quello che e'. In questo modo posso usare path liberamente, perche' il modulo path rimane os.path.
Alternativamente potrei fare from os import path as os_path, ma non mi sembra che porti vantaggi rilevanti, quindi faccio solo os. E nota: tutto questo e' solo fatto in nome della leggibilita' e della scelta dei nomi. Non ci sono considerazioni di performance o altro.
> Leggendo il codice altrui ho trovato anche cose del tipo:
Ottima scelta leggere il codice altrui. Poi suggerisco... leggi il codice, quello buono. Ora ci vuole un po' di esperienza per navigare, ma in generale piu' un pezzo di codice e' scritto da sviluppatori veri, piu' la qualita' tende ad essere alta (tutta gente con background forte di software engineering). Insomma... salvo che anche nella stdlib ci sono cose che fanno vomitare, in generale la qualita' del codice e' buona. Similarmente per librerie ad alto profilo.
Viceversa troverai che robe tipo numpy devono fare una serie di concessioni per motivi di performance. La roba di data science e' in generale scritta da persone con un diverso background, hanno scelte di API spesso discutibili.
Vai a pescare una libreria scritta da un hobbista per parlare con il suo simil-arduino... beh, se ti serve usala. Studiane il codice per capire cosa fa. Ma non prendere esempio dallo stile. E cosi' via.
Pygame a me verrebbe da dire che e' una roba che deve fare un sacco di concessioni alla pragmatica. Fra performance in Python, un modello che viene piu' dalla scrittura di giochi "alla vecchia maniera" che dal mondo del software engineering modello. Cioe'... la PyGame alla grande! Divertiti. Non ha nulla che non va.
Ma diciamo che una serie di cose che potresti vedere molto comunemente in quel contesto potrebbe non essere apprezzata tantissimo dalla wider python community.
import pygame
pg = pygame
È una buona abitudine? Perché non mi sembra che agevoli particolarmente la leggibilita... anzi, io direi che la compromette.
Sono d'accordo con te. E' una cacata pazzesca.

Denota sonora ignoranza di Python. Se il problema e' dare un nome diverso ad un modulo (per qualunque ragione), fai come dico io.
Si fa, specie per la gente che come me rifiuta gli star import.
Per dire... quando uso numpy o scipy o pandas o sta roba qui, mi trovo con librerie che hanno nel loro namespace globale un quintale di roba. E in generale nei posti in cui li uso, uso quasi solo roba che viene dalla libreria. Quindi, visto che il contesto e' decisamente chiaro quello che succede e' tipo
import numpy as np
import pandas as pd
Perche' la qualitica e' chiara (continuo a sapere chi viene da cosa), ma il codice rimane un piu' coinciso da leggere. Nota, una cosa del genere la faccio solo con librerie molto riconoscibile. Cioe' grosso modo chiunque vede una pagina piena di np (specie guardando i nomi delle funzioni) capisce che stiamo parlando di numpy. Eviterei di abusare di questa feature in caso di librerie poco note. Che se mi trovo un
prd.do_something()
non sono proprio contento.
* initialize_game contiene troppa logica. e molte delle cose che ha direttamente in pancia dovrebbero essere funzioni. inoltre fa *due* cose: inizializza il sistema dal punto di vista del software, *poi* passa il controllo all'utente per fare partire il gioco (dal punto di vista dell'utente). Questo e' gia' praticamente un "main" (o meglio, parte di). Grosso modo la stessa cosa vale per le funzioni nello stesso modulo. E vedrai anche che ci sono pezzi comuni che puoi astrarre.
Immaginavo. Devo dire che mi lascia un po' perplesso questa cosa della fattorizzazione. Voglio dire, ne capisco la logica e l'utilità, ma alle volte, quando leggo il codice scritto da altri, mi sento un po' come il cliente delle poste che viene mandato da uno sportello all'altro. Inizio col leggere la funzione main, da qui mi si rimanda ad una funzione in un altro modulo, poi da qui magari vengo mandato su una funzione che sta in un altro modulo ancora... è un po' frustrante. In alcuni casi, quando c'e' del codice duplicato, non c'è alternativa migliore. Altre volte...
Per esempio, prendiamo la funzione run_game del modulo game.py e nella fattispecie queste righe:
# check if snake's new head has hit an edge
x, y = new_head_coords
if x == -1 or x == CELLWIDTH:
return snake_coords, apple_coords
if y == -1 or y == CELLHEIGHT:
return snake_coords, apple_coords
# check if snake's new head has hit its body
for body_coords in snake_coords[1:]:
if new_head_coords == body_coords:
return snake_coords, apple_coords
Ora, questo codice lo uso una volta sola, qui. Potrei creare due funzioni distinte, che piazzerei nel modulo snake.py:
def edge_hit((x, y)):
"""Given the head coords, return True if the head hits the edge of
the screen, otherwise return False.
"""
if x == -1 or x == CELLWIDTH or y == -1 or y == CELLHEIGHT:
return True
else:
return False
def body_hit(snake_coords):
"""Given a list containing the snake's coords, return True if the
head of the snake hits its body, otherwise return False.
"""
for body_coords in snake_coords[1:]:
if snake_coords[0] == body_coords:
return True
else:
return False
Sicché, tornando alla funzione run_game:
# check game over conditions
if snake.edge_hit(snake_coords[0]) or snake.body_hit(snake_coords):
return snake_coords, apple_coords
Tralasciando il fatto che si fa così perche si fa così (e ora che l'ho messa in questi termini spero che si faccia davvero così), siamo sicuri che ne valga davvero la pena?
Se scrivessi i test per il tuo codice, non ti porresti questa domanda. Nel senso che testare due funzioni corte e' molto piu' semplice che testarne una lunga.
Detto questo... hai due funzioni per testare se hai sbattuto la testa sul muro o ti sei troncato il corpo. Mi sembra utile.
Vuoi vedere un side effect? Io il tuo codice lo avevo gia' visto... ma non mi ero accorto di un errore che fai. L'errore e' che scrivi una roba in molto complicato quando potresti semplificare. Solo che fintanto che era un coso unico, lo avevo solo preso per assunto. Quando mi e' stato chiaro che in effetti quel pezzo di codice deve solo dirmi se la testa coincide con il corpo, mi e' stato chiaro che hai molti modi piu' efficienti e/o coincisi per risolvere questo problema:
a. snake[0] in snake[1:] /
b. Counter(snake) e poi guardi se qualcosa ha 2 occorrenze e in generale ogni sistema buono per
c. snake.count(snake[0]) > 1
d. etc etc etc
Tutti questi sistemi sono molto coincisi. Ma ancora una volta non spiegano *esattamente* cosa vuoi fare. Non ti dicono perche' snake.count(snake[0]) > 1... ovvero non ti dicono come risolve il tuo problema.
Mettilo dentro un metodo body_hit ed e' immediatamente chiaro *perhe'* hai scritto quella cosa e come mai quella cosa funziona.