Scusate ma... non ci siamo mica tanto, qui.
Allora, per prima cosa sgombriamo il campo da una possibilità. Questo, per caso, è un esercizio che è stato *assegnato* da qualcuno all'OP (un prof, un corso che sta seguendo, un libro...)? Perché allora i casi sono due: o il prof ha assegnato un esercizio troppo difficile, oppure l'OP non ha seguito con profitto il corso fin qui.
Questa è la possibilità facile.
Se invece questo è un esercizio che l'OP si è "inventato" da solo, tanto per provare... allora qui andiamo nel complicato.
Partiamo da questa affermazione fondamentale (quindi la scrivo in maiuscolo, tanto per essere chiaro): IMPARARE E RISOLVERE PROBLEMI SONO DUE COSE DIVERSE, al contrario di quanto molti amano dire, superficialmente.
Sì, è vero che ci sono sovrapposizioni tra le due cose. Nel corso di un processo di apprendimento si risolvono problemi: ma questa si chiama "didattica" e consiste nel proporre problemi attentamente confezionati in modo da corrispondere al livello di competenza acquisito dall'allievo in quel momento; in un certo senso, sono dei "problemi giocattolo", non dei problemi veri.
All'inverso, è vero che risolvendo problemi spesso si impara qualcosa. Ma questa si chiama "esperienza" e presuppone comunque a monte la conoscenza necessaria per risolvere il problema (e/o la capacità di acquisirne autonomamente dell'altra necessaria allo scopo).
Ora, personalmente io credo che l'OP farebbe meglio a IMPARARE, invece di dedicarsi a RISOLVERE PROBLEMI. Il mio consiglio è di lasciar perdere questo problema ("sbattendoci la testa" o no, è lo stesso) e seguire un buon libro passo-passo (il Lutz è sempre la scelta sicura). Affidarsi a un percorso didattico che in qualche modo lo sostenga... Specialmente se, oltre a essere principiante di Python, è anche principiante della programmazione, degli algoritmi, eccetera.
Ciò detto, vediamo invece la seconda cosa. Che cosa bisogna fare per RISOLVERE UN PROBLEMA?
Bisogna fare esattamente QUATTRO passaggi. Esattamente quattro, esattamente nell'ordine. Non c'è scampo: altrimenti non state risolvendo il problema, state solo pasticciando... e magari a forza di pasticciare inciampate nella soluzione, eh?... anzi, capita abbastanza spesso. Ma non vuol dire che avete "risolto il problema", vuol dire che avete "trovato la soluzione del problema". Sono due cose diverse.
Il primo passaggio consiste nel RICONOSCERE il problema, nel "dargli un nome", se volete. Questo molto spesso è il passaggio più difficile, specialmente per il principiante. Occorre fare un gigantesco sforzo di astrazione e lasciar perdere il caso specifico che si ha di fronte, per capire che in che cosa consiste il problema generale, a quale famiglia di problemi appartiene... come "si chiama" il problema.
Per esempio, nel nostro caso il caso specifico è "voglio indovinare una password!!! con la brute force!!!!!" Ma indovinare una password non è un "problema", è un "caso d'uso". E "brute force" non è... nulla, è solo un'espressione che non significa nulla.
Quindi lo sforzo di astrazione consisterebbe nel capire che questo problema (almeno per come ce lo stiamo ponendo qui adesso) appartiene alla famiglia dell'analisi combinatoria, e nello specifico consiste nel
trovare le combinazioni (con ripetizione!) di "k" elementi di un insieme di "n" elementi. Questo è il problema che cerchiamo di risolvere. Dove "k" sarà poi la lunghezza della password, nel nostro caso specifico. E "n" finiranno per essere i caratteri ammessi nel campo della password (lettere, numeri, segni speciali, quel che volete).
Se uno arriva a capire che questo è il problema da risolvere, capisce anche che per esempio non ha senso chiedersi, come fa l'OP, "come faccio ad aggiungere le maiuscole e i numeri". Il problema è formulato correttamente (ripetiamo) come
le combinazioni di "k" elementi di UN insieme di "n" elementi
e non "...nella somma di TRE insiemi...". Per il semplice fatto che la somma (l'unione) di tre insiemi è a sua volta un insieme... quindi l'OP dovrebbe ragionare in termini di "quanti/quali caratteri sono ammessi" (il numero "n" di elementi), e non ha nessun senso distinguere l'insieme delle maiuscole, l'insieme delle minuscole, l'insieme dei numeri, l'insieme dei caratteri speciali...
Ma a questa comprensione ci si può arrivare solo se ci si sforza di riconoscere qual è il problema, "dare un nome" al problema.
Quindi, una volta che abbiamo capito qual è di preciso il problema, vediamo bene che anche la soluzione di Nunzio è sbagliata per principio. Non ha senso proporre una funzione per quando k è uguale a 4, un'altra funzione per quando k è uguale a 5, e così via. Quello che occorrerà fare è una funzione che produca l'algoritmo per la soluzione generale:
>>> def combinations(n, k):
# ...
tale per cui, per esempio,
>>> combinations('abcde', 3)
['aaa', 'aab', 'aac', 'aad', 'aae', ..., 'cce', 'cdd', 'cde', 'cee', 'ddd', 'dde', 'dee', 'eee'] # 35 valori
>>> combinations('abcde', 4)
['aaaa', 'aaab', 'aaac', 'aaad', ..., 'ceee', 'dddd', 'ddde', 'ddee', 'deee', 'eeee'] # 70 valori
Solo a questo punto potremmo poi applicare questa soluzione al nostro caso specifico (finalmente! la brute force!), con un'altra funzione:
>>> import string
>>> chars = string.ascii_letters + string.digits + '#?$%&-_' # per esempio...
>>> def guess_password(password):
for guess in combinations(chars, len(password)):
if guess == password:
return 'found: ' + guess
>>> guess_password('my_secret')
found: my_secret
>>> guess_password('my_long_long_long_long_long_long_long_long_secret')
found: my_long_long_long_long_long_long_long_long_secret # dopo un po' di tempo
Ma torniamo ai nostri quattro passaggi.
Dopo aver capito qual è il nostro problema, il secondo passaggio è DOCUMENTARSI sul problema. Questo è fondamentale. Googlare, googlare, googlare come dei pazzi. Non alla ricerca di ricettine da copincollare, beninteso, come vogliono i principianti SEMPRE. Ma googlare per capire di che cosa stiamo parlando. Se sto cercando di fare un programma che mi cataloga la mia biblioteca, avrò bisogno di avere qualche nozione basilare di biblioteconomia (e già per trovare il parolone, bisogna saper googlare). Quanto ne so, davvero, di come si cataloga un libro? Se voglio tenere i conti di casa, forse mi conviene farmi un'idea di quali feature offrono, tipicamente, i centomila programmi esistenti per tenere i conti di casa. E così via. Meglio studiare, prima di cominciare a pasticciare con la tastiera.
Di solito il primo e il secondo passaggio si rimbalzano tra loro più volte. Documentandosi uno finisce per arrivare a definizioni sempre più precise del problema. Definire più precisamente il problema consente di raffinare le ricerche di documentazione. Per esempio, nel nostro caso possiamo partire da una vaga idea di "combinazione" e poi, leggendo in giro, scoprire che è diverso dire "con ripetizioni" o "senza ripetizioni". Quindi ci pensiamo sopra, e arriviamo a definire meglio il nostro problema: a noi servono le "combinazioni con ripetizione" perché uno può usare tante volte lo stesso carattere, in una password. E così via.
Documentandosi si finisce sempre per scoprire anche già qualche algoritmo pronto, perché ovviamente! non saremo i primi a porci questo problema nella storia dell'umanità. Questa è una BUONA cosa, a patto di non limitarsi a copincollare la ricettina. La cosa migliore poi è trovare l'algoritmo implementato in un linguaggio che non è il nostro (in Java, in C++)... lo sforzo di capire produrrà risultati sorprendenti nella nostra testa.
Per esempio, nel nostro caso potremmo anche imbatterci nel curioso piccolo fatto... che Python ha già una funzione nella libreria standard che trova esattamente le combinazioni con ripetizione. Eh già, guarda un po'. E se uno guarda la documentazione di questa funzione, trova anche una implementazione in Python dell'algoritmo che ci serve... ed è una implementazione molto elegante, ma anche molto difficile da capire... su cui davvero uno, se ha voglia, può "sbatterci la testa" per un po'.
Il terzo passaggio consiste nel FORMALIZZARE la soluzione. Questo si fa SEMPRE con carta e penna, e può voler dire diverse cose. Per i problemi più complessi e compositi potrebbe essere un elenco preciso degli attori; magari un po' di diagrammi UML; magari uno schema delle tabelle di un database con le loro relazioni; va a sapere. In ogni caso, qualcosa che fissi con chiarezza come intendiamo procedere.
Nel caso degli algoritmi semplici, come il nostro, non resta che...
scrivere l'algoritmo in italiano (pseudo-codice) riga per riga. Questo è l'esercizio noioso che il principiante schiva come la morte. Però è anche la cosa che serve: altrimenti si sta solo pasticciando sulla tastiera. Talvolta può essere facile, talvolta più difficile. La rete è piena di esempi di algoritmi in pseudo-codice. Bisogna abituarsi a farseli a manina... non c'è santo. Spesso aiutano dei disegni, dei diagrammi... quello che volete. L'importante è che sia chiaro nel momento in cui poi si scriverà il codice.
Ora, il problema di questo algoritmo particolare è che è un po' complicato da visualizzare correttamente. Un problema è che ci sono due varibili libere ("n" e "k"), per cui è un po' più difficile immaginare una tabella... Tra l'altro è uno di quegli algoritmi che si "pensano" più facilmente immaginando una struttura ricorsiva... e questo è tutto dire, visto che di solito la ricorsione è difficile da pensare.
Non è che non lo si possa studiare, eh... e sicuramente in giro per la rete qualche aiuto si trova. Però non sarebbe uno di quelli che proporrei in un percorso didattico, almeno non per studenti alle prime armi.
Il quarto passaggio, finalmente, è SCRIVERE IL CODICE. Cercare di tradurre l'algoritmo visualizzato in codice. Anche questo non è proprio automatico. Per esempio, nel nostro caso si può ricorrere a cicli "for" oppure a dei "while"... è una scelta. Oppure, dicevamo, alla ricorsione.
Anche qui, il terzo e il quarto passaggio si rimbalzano spesso tra di loro. Uno può cominciare con una piccola parte dell'algoritmo, scrivere quel pezzo di codice, poi tornare indietro... fare delle prove e verificare la correttezza delle sue assunzioni...
Però bisogna chiarire bene una cosa: questo "avanti e indietro" tra il terzo e il quarto passaggio, è una cosa comune e ammessa dalla pratica di programmazione. A seconda dei contesti si può chiamare in vari modi (fattorizzare il codice, approccio bottom-up... test-driven development... chi più ne ha più ne metta); ma non vuol dire "mettersi a pasticciare e vediamo che cosa esce fuori". La cosa importante è che non si dovrebbe comunque scrivere una riga di codice senza sapere che cosa si sta facendo (magari poi ci si sbaglia... ma questo fa parte del gioco).
Dico questo perché molto spesso c'è una confusione alla base... questo incentivo a "pasticciare" è tipico in effetti di molti corsi didattici, che propongono di impare "mettendoci le mani" subito... però questi sono appunto dei percorsi didattici, che ti dicono di mettere le mani "a caso" su esercizi molto ben calibrati e progressivi (si spera!), non si problemi reali scelti un po' a caso. Di nuovo, bisogna stare attenti a distinguere tra "la didattica" e "la soluzione di problemi".
E qui chiudo perché per oggi mi sembra di aver dato anche troppo. La morale è sempre quella: Python è un linguaggio che può essere facile per chi già ha l'abitudine a programmare, ma per i principianti è molto più complesso di quello che si dice. E la programmazione è ancora più complessa. In generale, non bisogna sorprendersi di trovarsi di fronte a dei gradini difficili. L'ideale è imparare affidandosi a un percorso didattico valido, invece di avere la pretesa di "sperimentare" e "farsi un programma per vedere come va". Ma mi rendo conto che non è un consiglio che viene accolto bene, in genere.