[Python] Why Go is not good

enrico franchi enrico.franchi a gmail.com
Lun 13 Lug 2015 11:55:47 CEST


2015-07-11 16:03 GMT+01:00 Enrico Bianchi <enrico.bianchi a ymail.com>:

> On 07/03/2015 03:36 PM, enrico franchi wrote:
>
>>  Parli di override o di overload?
>>
> Oddio, confondo sempre i termini...
>

Ok, a te interessa fare overloading.


> Quello che mi interessava fare era semplicemente questo:
>
> func Sum(a, b int) int {
>   return a + b
> }
>
> func Sum(a, b float) float {
>   return a + b
> }
>
> Certo, posso usare anche SumInt e SumFloat, ma diventa molto piu`
> complesso, ovvero potrei avere un caso in cui non so il tipo di variabile e
> prima me lo devo andare a cercare (a meno di non fare evidenti cast di
> variabili).


Si, capisco. Ma explicit is better than implicit. In primo luogo, a me
verrebbe da dire che non e' del tutto normale non sapere il tipo statico di
una variabile e volerci fare sopra cose. Tipicamente finisce che qualcosa
non funziona.

Il problema con l'overloading e' che rende un sacco di cose drammaticamente
piu' complicate: in presenza dello stesso caso che menzioni (ovvero che non
sai quale tipo sia una variabile) vuole dire che effettivamente il tuo
codice compilera', ma tu non avrai idea di cosa faccia. Se non sai quale e'
il tipo della variabile, vuole dire che non sai quale variante del metodo
verra' chiamata. E quindi di fatto non sai che codice stai eseguendo. Ecco
che un'innocua seccatura in assenza di overloading e' diventata una
possibile fonte di bachi.

Ma in generale, tutte le volte che ho avuto a che fare con overloading,
avrei voluto *non* averlo. Certo, l'idea di SumInt e SumFloat da davvero
fastidio e fa brutto. Poi quando uno realizza quello che implica, dice
toglietelo. *Specie* ma non limitatamente alla combo overloading + type
conversion implicite. Troppe regole da tenere a mente, troppo facile fare
cazzate.

Ci sono chiari esempi di API design piuttosto sensati che in presenza di
overloading diventano relativamente complessi da gestire. Il primo che mi
viene in mente e' quello di slf4j (facade di logging su Java). Questo
esempio non ha nemmeno il problema dell'interazione con le type conversion
e similia.

- error(String, Object)
- error(String, Object, Object)
- error(String, Object...)
- error(String, Throwable)

In pratica quando gli passi un singolo throwable fa una cosa [case (d)],
quando passi un singolo object [case (a)] ne fa un'altra (che e' un caso
speciale di quello che succede quando gliene passi 2 [case (b)] o N
[case(c)]). E' veramente facile senza pensarci troppo prendere una chiamata
tipo:

logger.error("...", t);

e farla diventare, che so...

logger.error("...: {}", t, o);

e non accorgersi che la semantica e' cambiata parecchio (in particolare
quello che accade con lo stack-trace).



> Per inciso, ricordo un thread su di un gruppo Facebook in cui si criticava
> specificatamente la mancanza di questa caratteristica (e in cui era uscita
> anche questa cosa simpatica: http://play.golang.org/p/Isoo0CcAvr )


Questo e' un problema completamente *non* relativo a quello di la sopra. In
effetti trattano parti di linguaggio completamente distinti. Potrsti avere
overloading e avere ancora quel problema sopra menzionato.


>
>
>  Credo che tu non abbia chiara la gestione degli errori in Go.
>>
> Guarda, faccio prima a farti vedere cosa intendo: prendiamo questo caso:
>
> package main
>
> import (
>   "fmt"
> )
>
> func Sum(a int, b *int) int {
>     return a + *b;
> }
>
> func main() {
>     var first int
>     var second *int
>
>     first = 1
>     second = nil
>     fmt.Println(Sum(first, second))
> }
>
> Questo codice genera, ovviamente, un runtime error in quanto passo un
> puntatore nil come argomento della somma. Ora, per gestire questa
> situazione, posso sicuramente fare un check che le variabili siano
> valorizzate, ma questo non mi protegge da altri "imprevisti". Per capirci,
> una situazione del genere in Python la gestirei cosi`:
>
> def sum(a, b):
>     return a + b
>
> if __name__ == "__main__":
>     try:
>         print(sum(1, None))
>     except:
>         print("Argh!")
>
> (il che potrebbe essere deprecabile in alcuni contesti, mentre in altri,
> e.g. per gestire la morte prematura delle connessioni da parte di un
> client, potrebbe essere una facility non indifferente)


Scusa, non capisco. Mi fai vedere un pezzo di codice ... pezzo di codice
che ha chiaramente un baco (indipendente da Go). Cioe', quel codice non
dovrebbe essere scritto in quella maniera (e non dovrebbe essere utilizzato
in quella maniera). Tra l'altro, per come funziona Go, non ci sono estremi
buoni motivi per scriverlo in quella maniera... ora, capisco che sia un
esempio... ma non riesco a vedere come un problema il fatto che se
dereferenzi puntatori senza fare check sei nella guazza.

E il modo in cui lo risolvi in Python e', appunto, sbagliato a mio avviso.
In primo luogo il catch all except e' qualcosa che viene sconsigliato da
lungo tempo. E, per il resto, si espone un'altro problema di Python sugli
esempietti piccoli.

In Java una funzione come sum(Integer a, Integer b) la avresti
(sperabilmente) scritta come sum(@Nonnull Integer a, @Nonnull Integer b) e
i tool di analisi statica ti avrebbero flaggato degli usi potenzialmente
non safe della stessa. Alternativamente, avresti forse messo delle guardie
che controllano che i valori siano bboni. Questo si riesce a fare perche'
da un lato puoi fare analisi statica, dall'altro il compilatore e il JIT
possono fare si che suddetto costo sia trascurabile (o comunque piccolo).

In Python non hai modo di farlo: riempire tutto di null check ti fa
schiantare le performance e l'analisi statica... beh, buona fortuna.

In Go... beh, in Go non scrivi la funzione in quella maniera: se passi un
puntatore ad un intero e' perche' vuoi modificarlo. I tool di analisi
statica potrebbero esistere (ma al momento non ho idea dello stato) e il
costo dei check e' piu' piccolo che in Python, ma potenzialmente piu'
costoso che in Java (a seconda di quanto e' veramente smart il compilatore,
ma temo non abbastanza).

Ma comunque torniamo al punto chiave: per quanto mi riguarda se ti trovi
con una npe, hai un baco nel codice. E' accettabile spegnere tutto, a patto
di farlo in modo pulito (panic vs. seg fault). E' accettabile anche
un'eccezione: ma facci caso: di solito non fai catch broad di exceptions
che non puoi gestire in modo specifico (e puoi solo silenziare). Tantomeno
ho visto codice che cattura esplicitamente npe in Python: di solito se ho
un pattern tipo

a = may_return_null(...)
f(a, ...)

quello che scrivo e'

a = may_return_null(...)
if a is not None:
    f(a, ...)

-- a meno che f non sia fatta in modo tale che None e' ok --

Questa e' un'essenziale differenza rispetto al classico EAFP (che in
generale preferisco) per vari motivi. In primo luogo, se None e' un valore
non valido per a, non voglio nemmeno fare partire la computazione. Il fatto
che la funzione sopra mi abbia dato None vuole dire qualcosa di ben
preciso: di fatto e' un caso in cui may_return_null utilizza i valori di
ritorno per segnalare che qualcosa non e' andato ammodo. Possiamo discutere
sul design di may_return_null, ma se fa cosi', io cosi' lo devo gestire. E
il secondo motivo e' che probabilmente l'eccezione da f si propaga un po'
piu' tardi di quello che vorrei, per cui preferisco intervenire.


>  (o meglio, della versione castrata che e' implementata nei vari Python,
>> Java, C++ e combriccola con cui sei probabilmente familiare)
>>
> Per curiostia`, perche` sarebbero castrate?
>

Fanno sempre e solo stack unwind, non danno controllo al programmatore.


>
>  puoi usare panic.
>>
> Panic da quello che ho visto manda in traceback l'applicativo, ovvero e`
> l'equivalente di un raise in Python o di un throw in Java. Quello che
> vorrei fare io e` il catch


Loro sconsigliano di vederlo in quel modo. Il comportamento apparente e'
simile, ma lo scopo e' diverso.


>
>
>  (vedi gli esempi nella stdlib di Go)
>>
> Uhm, a quali esempi ti riferisci?


Se leggi la doc su come si usano i vari panic, recover e error-handling ti
sara' chiarissimo.
Li specificano anche che e' cattiva pratica fare propagare i panic dalle
librerie (a meno che non siano veri panic: hei, suicidiamoci gloriosamente
prima di fare altri danni). Il che vuole dire che hai l'ottima proprieta'
che devi gestire, di fatto, solo l'errore di ritorno.


>
>
>  Io personalmente trovo che la gestione degli errori di Go rende il codice
>> complessivamente molto piu' snello.
>>
> Dipende dal concetto di snello che hai Per esempio, se per creare un file,
> devo fare questo:
>
> file, err := os.Create(filename)
> if err != nil {
>   panic(err)
> }
> defer file.Close()
>
> invece di questo:
>
> try:
>     with open(filename, "w") as f:
>         # Codice
> except:
>     raise Exception("Argh!")
>
> beh, io trovo piu` snella e chiara la seconda :)
>
>
Come dicevo... quello non e' Go idiomatico a mio avviso. Qualcuno con piu'
esperienza interverra', ma io quel codice lo scriverei diversamente. E non
sono sicuro che userei panic (a meno che davvero non sono molto vicino al
top-level...).

Uno dei problemi con le eccezioni e' quando hai piu' operazioni che possono
andare male. Immagina di dovere aprire n files. E immagina che un qualunque
fallimento implica che devi uscire di li...

Immagina anche di dovere fare una serie di operazioni sui files, e di
dovere rimuovere i files in caso appunto di errore (aprendo o lavorandoci).
Ecco... questo comincia a dare un'idea del come il codice con le eccezioni
diventi bruttozzo alla svelta.

Ancora peggio quando hai codice "lineare" del tipo fai una serie di
operazioni una in fila all'altra. E hai errori vari che possono arrivare da
ognuna di queste (possibilmente con un set di eccezioni non disgiunto per
le varie chiamate).

-- 
.
..: -enrico-
-------------- parte successiva --------------
Un allegato HTML è stato rimosso...
URL: <http://lists.python.it/pipermail/python/attachments/20150713/6047cf3e/attachment-0001.html>


Maggiori informazioni sulla lista Python