«[Figlio dell'uomo] Porgi l'orecchio e ascolta le parole di KGB
e applica la tua mente alla SUA istruzione
» Pv. 22,17

Qui si straparla di vari argomenti:
1. Il genere dei pezzi è segnalato da varie immagini, vedi Legenda
2. Per contattarmi e istruzioni per i nuovi lettori (occasionali e non) qui
3. L'ultimo corto è questo
4. Molti articoli di questo blog fanno riferimento a definizioni e concetti che ho enunciato nella mia Epitome gratuitamente scaricabile QUI. Tali riferimenti sono identificati da una “E” fra parentesi quadre e uno o più capitoli. Per esempio: ([E] 5.1 e 5.4)

martedì 10 settembre 2019

Nomi di paesi toscani

Qualche giorno fa avevo iniziato a buttare giù delle idee per un racconto che avevo, molto tiepidamente, intenzione di scrivere. Volevo quindi ricreare anche una zona immaginaria nella Toscana completa di qualche paese fittizio e così…

...e così sono partito per la tangente e ho deciso di scrivere un programma che generasse nomi casuali di cittadine basandosi su un elenco di nomi reali: l’obiettivo era sempre il solito, ovvero impratichirmi col Python.
Lo so, apparentemente ricorda un po’ il programma PornoNomi presentato qualche settimana fa (v. Porno nomi (*1)) ma in realtà quello era un programma semplicissimo che io avevo reso arbitrariamente complicato aggiungendovi nuove funzionalità, casi particolari e usando librerie esterne (ad esempio quella per identificare aggettivi, nomi, etc…).
Era un programmino semplice perché sostanzialmente mi limitavo a scegliere un nome e un cognome a caso.

In questo caso ho invece deciso di complicare le cose: ho deciso di creare una struttura dati che, a ogni singola lettera, vi associasse tutte le possibili lettere successive (ovviamente in base alle possibilità date dalla lista di nomi di città reali). Durante la generazione di un nuovo nome, casualmente, il programma avrebbe quindi scelto la lettera successiva in base alla frequenza data dai nomi reali. Se ad esempio una “A” fosse seguita da 3 “S” e 5 “R” allora il programma sceglierebbe la “S” con probabilità 3/8 e la “R” con 5/8.
Inoltre per complicare ulteriormente le cose, ma anche per rendere i nomi generati più verosimili, ho stabilito che le parole precedute da un asterisco avrebbero dovuto essere considerate come singole lettere. Ad esempio nella lista ho inserito nomi del tipo “Montecatini *Terme” o “*Forte_dei Marmi” col risultato che il programma può generarmi “NOME-CASUALE Terme” o “Forte_dei NOME_CASUALE”.

Questo è il programma:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import random
import string

class Elemento():
    def __init__(self, nome):
        self.nome=nome
        self.seguiti=[]
        self.totSeguiti=0
    def __str__(self):
        if self.nome=="STOP":
            return ""
        return self.nome

def parsa(p):
    flag=False
    for i in range(0, len(p)):
        if p[i]=="*":
            flag=True
            blocco=""
            continue
        if flag:
            if p[i]==" ":
                flag=False
                yield blocco
                yield " "
            else:
                blocco=blocco+p[i]
                continue
        else:
            yield p[i]
    if flag:
        yield blocco

inizio=Elemento("VIA")
fine=Elemento("STOP")
base=[inizio, fine]

with open("ListaPaesi.txt") as f:
    content3 = f.readlines()
lista_paesi=[l.rstrip('\n').upper() for l in content3]

for paese in lista_paesi:
    corr=inizio
    for c in parsa(paese):
        val=list(filter(lambda x: x.nome==c , base))
        if len(val)==0:
            nuovo=Elemento(c)
            base.append(nuovo)
            corr.seguiti.append((nuovo,1))
            corr.totSeguiti=corr.totSeguiti+1
        else:
            valseg=list(filter(lambda x: x[0].nome==c , corr.seguiti))
            if len(valseg)==0:
                nuovo=val.pop()
                corr.seguiti.append((nuovo,1))
                corr.totSeguiti=corr.totSeguiti+1
            else:
                pa, nu =valseg.pop()
                corr.totSeguiti=corr.totSeguiti+1
                corr.seguiti.remove((pa, nu))
                corr.seguiti.append((pa, nu+1))
                nuovo=pa
        corr=nuovo
    val=list(filter(lambda x: x[0].nome=="STOP" , corr.seguiti))
    if len(val)==0:
        corr.seguiti.append((fine,1))
        corr.totSeguiti=corr.totSeguiti+1
    else:
        pa, nu =val.pop()
        corr.totSeguiti=corr.totSeguiti+1
        corr.seguiti.remove((pa, nu))
        corr.seguiti.append((pa, nu+1))

def genera():
    corr=inizio
    while corr.totSeguiti>0:
        num=random.randint(1,corr.totSeguiti)
        for ele in corr.seguiti:
            num=num-ele[1]
            if num<=0:
                print(ele[0],end='')
                corr=ele[0]
                break
    print()

for i in range(1,30):
    genera()

Mi limiterò a commentare brevemente le novità Python che ho introdotto e imparato a usare.

Linea 4-12 definisco un oggetto Python che sarà l’elemento basilare della struttura dati necessaria per realizzare ciò che avevo in mente. Ogni singolo elemento, oltre al proprionome/codice ha una lista che contiene tutte le possibili continuazioni e una variabile con il loro numero totale. Ridefinisco anche la funzione __str__() perché mi faceva comodo per stampare poi il nome generato…

Linea 14-32 definisco la funzione parsa() che sfrutta il comando “yield”. Difficile spiegare tale comando: è molto “pitonesco” e non è facile spiegarne il significato senza entrare in dettagli noiosi e/o complicati. Userò la funzione per spezzettare i nomi di città negli opportuni frammenti: le singole lettere oppure le parole intere precedute da * (che viene rimosso).

Linea 38-40 inizializzo la mia struttura base con due elementi (“inizio” e “fine”) e carico in memoria la lista delle città.

Linea 42-72 il ciclo principale che per ogni nome di città caricato in memoria usa la funzione parsa() per creare la meta struttura che verrà poi usata per generare i nomi. Da notare nelle linee 45, 52 e 64 l’uso del comando lambda che definisce una funzione in linea che adopero per filtrare le liste (quella generale e quella specifica di ogni elemento).
Ah! Avrei potuto essere più furbo e aggiungere un carattere speciale (tipo “#”) alla fine di ogni parola in maniera da potermi evitare le linee di codice dopo il for più interno (64-72)...

Linea 74-84 genera() è la funzione che, usando la struttura dati costruita precedentemente, la percorre casualmente creando, passo passo, il nome casuale.

Linea 86-87 semplicemente chiama la funzione genera() 30 volte per creare altrettanti nomi casuali.

L’elenco di cittadine usato è il seguente:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
Prato
Livorno
Arezzo
Pisa
Pistoia
Lucca
Grosseto
Massa
Carrara
Viareggio
Siena
Scandicci
Sesto *Fiorentino
Empoli
*Campi Bisenzio
Capannori
Cascina
Piombino
Camaiore
*San Giuliano *Terme
Rosignano *Marittimo
Pontedera
Poggibonsi
Cecina
Montelupo
*San Casciano
Matassino
Ghezzano
Calci
Osmannoro
Fiesole
Prulli
Firenze
Rosano
Reggello
Figline
Incisa
*Lido_di Camaiore
*Marina_di Pietrasanta
Capraia
Limite
*Punta Ala
Bibbona
*Forte_dei Marmi
Radda
Gaiole
Roselle
Capalbio
Cecina
Barberino
Poggibonsi
Castiglioncello
Piombino
Montespertoli
Volterra
Certaldo
Cerreto Guidi
Panzano
Antella
Mercatale
Faella
Casellina
Castiglion *Fiorentino
Cortona
Montepulciano
Chianciano *Terme
*Bagni *San Filippo
*Terme_di Saturnia
Castiglione *della Pescaia
Massa *Marittima
Montecatini *Terme
Poppi
Bibbiena
Abetone
*San Gimignano
Montalcino
Stia
Tirrenia
*San Miniato
Camaldoli
*Ponte_a Elsa

Il risultato è il seguente:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GIA
A
PI TERME
LLI
CIMO
CASSANO
MO
BORESTORANTO
CATO
SAN FIORENTINO
VIOLIENILINO
CAMPI TERME
CA
MI
SANE
CISISCCA
CO
MAPALI
TERME_DI TERME
GLLLDAINO
BIBE
PA
SAN TERME
LIMONATI
ASA
SAN CIUINORELLI
FINO
CIGINA
GIBBIA

Ad occhio un nome su 5 è decente. Un problema evidente sono i nomi cortissimi di 2 o addirittura 1 lettera: questo è inevitabile per la struttura del programma: qualsiasi finale di un nome di città può essere seguito dalla terminazione del nome. Volendo potrei semplicemente ricalcolare i nomi troppo corti (meno di 4 lettere) ma non è particolarmente interessante.
Un altro problema sono le sequenze impronunciabili di consonanti: vedi “CISISCCA” o “GLLLDAINO”…

Per limitare quest’ultimo problema ho scritto una seconda versione del programma: invece di considerare le singole lettere (o le sequenze precedute dall’asterisco) ho deciso di usare le coppie di lettere. Fortunatamente per implementare questo cambiamento non ho dovuto stravolgere il programma: me la sono cavato introducendo una nuova funzione di parsing, parsa2() che riusa la parsa(), e ho dovuto riscrivere la stampa della funzione di generazione in maniera da eliminare le lettere doppie.
In realtà c’è anche un piccolo baco nel codice: le sequenze precedute da “*” inserite nel mezzo di un nome vengono ripetute due volte: lascio al lettore il divertimento di correggere opportunamente il codice!

Ecco la versione modificata:
1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
import random
import string

class Elemento():
    def __init__(self, nome):
        self.nome=nome
        self.seguiti=[]
        self.totSeguiti=0
    def __str__(self):
        if self.nome=="STOP":
            return ""
        return self.nome

def parsa2(p):
    primo=""
    secon=""
    for cc in parsa(p):
        if primo=="":
            primo=cc
            continue
        secondo=cc
        yield primo+secondo
        primo=secondo            

def parsa(p):
    flag=False
    for i in range(0, len(p)):
        if p[i]=="*":
            flag=True
            blocco=""
            continue
        if flag:
            if p[i]==" ":
                flag=False
                yield blocco
                yield " "
            else:
                blocco=blocco+p[i]
                continue
        else:
            yield p[i]
    if flag:
        yield blocco

inizio=Elemento("VIA")
fine=Elemento("STOP")
base=[inizio, fine]

with open("ListaPaesi.txt") as f:
    content3 = f.readlines()
lista_paesi=[l.rstrip('\n').upper() for l in content3]

for paese in lista_paesi:
    corr=inizio
    for c in parsa2(paese):
        val=list(filter(lambda x: x.nome==c , base))
        if len(val)==0:
            nuovo=Elemento(c)
            base.append(nuovo)
            corr.seguiti.append((nuovo,1))
            corr.totSeguiti=corr.totSeguiti+1
        else:
            valseg=list(filter(lambda x: x[0].nome==c , corr.seguiti))
            if len(valseg)==0:
                nuovo=val.pop()
                corr.seguiti.append((nuovo,1))
                corr.totSeguiti=corr.totSeguiti+1
            else:
                pa, nu =valseg.pop()
                corr.totSeguiti=corr.totSeguiti+1
                corr.seguiti.remove((pa, nu))
                corr.seguiti.append((pa, nu+1))
                nuovo=pa
        corr=nuovo
    val=list(filter(lambda x: x[0].nome=="STOP" , corr.seguiti))
    if len(val)==0:
        corr.seguiti.append((fine,1))
        corr.totSeguiti=corr.totSeguiti+1
    else:
        pa, nu =val.pop()
        corr.totSeguiti=corr.totSeguiti+1
        corr.seguiti.remove((pa, nu))
        corr.seguiti.append((pa, nu+1))

def possibilita(lista):
    num=0
    for l,n in lista:
        print(l.nome, num, "-", num+n)
        num=num+n
    print()

def genera():
    corr=inizio
    while corr.totSeguiti>0:
        num=random.randint(1,corr.totSeguiti)
        for ele in corr.seguiti:
            num=num-ele[1]
            if num<=0:
                par=ele[0].nome
                if par!="STOP":
                    print(par[:1] if len(par)==2 else par,end='')
                    prec=par
                corr=ele[0]
                break
    if len(prec)==2:
        print(prec[-1:],end='')
    print()

for i in range(1,30):
    genera()

E un nuovo esempio di risultato:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PIENA
TINA
CATO
FAELSANZIORENIA
CORETONTERTASCAREGGIULIONE DELLADELLA  PESOLTELLO
CASTIGNA
GRO
CANO
PIO
MARINA_DI  PESCINA
VIANO
RASSA
POPPISATO
OSI
CATALDO
ROSMAIORNO FIORENTINO
SAN  GIO
CANTECIARRASTO TERME
CAMPI  BIBONTE
ARA
GROSENZANO FIORENTINO
MONTEDERARBERINO MARITTIMO
PANTELLO
CASTIA
CAMAIANDI
FAELLIOREGGIBBONE
SCISA
PI
SCAMASSA

Decisamente meglio!
Almeno le sequenze illeggibile sono sparite anche se, effettivamente, “CORETONTERTASCAREGGIULIONE DELLADELLA PESOLTELLO” (notare il baco che raddoppia il “DELLA” nel mezzo al nome) non è proprio facile da pronunciare!
Ovviamente restano i nomi cortissimi (adesso il minimo è 2 lettere) ma, come detto, questo non è un problema che mi interessa correggere.
Ah! Dalla linea 85 a 90 mi è rimasta la definizione di una funzione, possibilita(), che ho usato per sbacare il programma ma che nella sua versione finale non è usata...

Conclusione: probabilmente il nome più buffo generato dal mio programma è stato “Ano Fiorentino”: suppongo che i suoi ipotetici abitanti si chiamerebbero “stronzetti”…

Nota (*1): a dire il vero anni fa scrissi un programma ancora diverso per generare nomi casuali basato sulla scomposizione in sillabe. I nomi risultanti erano estremamente realistici e credibili ma, comunque, lo scopo di questo programma è solo prendere familiarità col Python e non che il risultato ottenuto sia particolarmente buono: per questo ho preferito cimentarmi con un metodo completamente nuovo...

Nessun commento:

Posta un commento