# creazione
l = [1,2,3,10,"a", -12.333, 1024, 768, "pippo"]

# concatenazione
l += ["la", "concatenazione", "della", "lista"]

# aggiunta elementi in fondo
l.append(32)
l.append(3)
print(u"la lista è {}".format(l))
l.remove(3)  # rimuove la prima occorrenza
print(u"la lista è {}".format(l))
i = l.index(10)  # restituisce l'indice della prima occorrenza del valore 10
print(u"l'indice di 10 è {}".format(i))
print(u"il valore all'indice 3 è {}".format(l[3]))
print(u"** vediamo come funziona lo SLICING delle liste")
print(u"Ecco i primi 3 valori della lista {}".format(l[:3]))
print(u"e poi i valori dal 3o al penultimo {}".format(l[3:-1]))
print(u"e poi i valori dal 3o al penultimo, ma ogni 2 {}".format(l[3:-1:2]))

print("\n***FUNZIONI RANGE e XRANGE***\n")
l2 = range(1000)  # questi sono i primi 1000 valori da 0 a 999
print(u"ecco la lista ogni 50 elementi di n <=1000: {}".format(l2[::50]))

# LA FUNZIONE xrange è comoda per ottenere un oggetto tipo (ma non = a ) un generatore 
# da cui i numeri vengono appunto generati al momento dell'accesso all'elemento stesso
# della sequenza

# Il codice di prima dà errore
try:
    l2 = xrange(1000)  # questi sono i primi 1000 valori da 0 a 999 ma senza occupare RAM
    print(u"ecco la lista ogni 50 elementi di n <= 1000: {}".format(l2[::50]))
except Exception as e:
    print("ECCEZIONE {}: {}".format(type(e), e))

# Il codice che funziona con lo slice valuta xrange in una lista quindi
# risulta inutile
l2 = list(xrange(1000))  # questi sono i primi 1000 valori da 0 a 999 ma senza occupare RAM
print(u"ecco la lista ogni 50 elementi di n <= 1000: {}\n".format(l2[::50]))

## ma si può fare direttamente con range o xrange!
print(u"[OK] lista ogni 50 elementi <= 1000: {}".format(range(0,1000,50)))

"""
Explanation: CAPITOLO 1.1: liste, dizionari e modello dati
LISTE: Operazioni e metodi
End of explanation
"""



print("***PER FARE UN CICLO FOR CON INDICE INCREMENTALE SI USA XRANGE!")

for el in xrange(1,21):
    print("numero {}".format(el))
    
print("***PER NUMERARE GLI ELEMENTI DI UNA LISTA SI USA ENUMERATE!")

for i, el in enumerate(l, start=10):  # numero partendo da 10, se start non specificato parto da 0
    print("Il contenuto {} si trova all'indice {}".format(el, i))



"""
Explanation: Iterazione nelle liste e cicli for su indice
End of explanation
"""


# -*- coding: utf-8 -*-

# definizione
d = {"nome": "Luca", 
     "cognome": "Ferroni", 
     "classe": 1980, 
     2: "figli", 
     (2017, 1, 23): "corso python",
    "classe": 1979}

print(d)
# aggiornamento
d.update({
    "professioni" : ["docente", "lavoratore autonomo"]
})

d["pranzo"] = "ritrovo degli artisti"

# recupero valore per chiave certa (__getitem__)
print(u"Il nome del personaggio è {}".format(d["nome"]))

# sfrutto il mini-formato di template per le stringhe
# https://docs.python.org/2.7/library/string.html#formatspec
print(u"Il personaggio è {nome} {cognome} nato nel {classe}".format(**d))

# Recupero di un valore per una chiave opzionale
print(u"'nome' è una chiave che esiste con valore = {}, 'codiceiban' invece non esiste = {}".format(
    d.get('nome'), d.get('codiceiban')))

print(u"Se avessi usato la __getitem__ avrei avuto un KeyError")

# rimozione di una chiave dal dizionario
print(u"Rimuovo il nome dal dizionario con d.pop('nome')")
d.pop('nome')

print(u"'nome' ora non esiste con valore = {}, come 'codiceiban' = {}".format(
    d.get('nome'), d.get('codiceiban')))

print(u"Allora, se non trovi la chiave 'nome' allora dimmi 'Pippo'. Cosa dici?")
print(d.get('nome', 'Pippo'))

"""
Explanation: DIZIONARI: Operazioni e metodi
End of explanation
"""


print("\n***PER ITERARE SU TUTTI GLI ELEMENTI DI UN DIZIONARIO SI USA .iteritems()***\n")

for key, value in d.iteritems():
    print("Alla chiave {} corrisponde il valore {}".format(key,value))

    
print("\n***DIZIONARI E ORDINAMENTO***\n")
data_input = [('a', 1), ('b', 2), ('l', 10), ('c', 3)]
d1 = dict(data_input)
    
import collections

d2_ord = collections.OrderedDict(data_input)

print("input = {}".format(data_input))
print("dizionario non ordinato = {}".format(d1))
print("dizionario ordinato = {}".format(d2_ord))
print("lista di coppie da diz NON ordinato = {}".format(d1.items()))
print("lista di coppie da diz ordinato = {}".format(d2_ord.items()))

"""
Explanation: Iterazione nei dizionari
ATTENZIONE: Il contenuto del dizionario non è ordinato! Non c'è alcuna garanzia sull'ordinamento. Per avere garanzia bisogna usare la classe collections.OrderedDict
End of explanation
"""


def foo(bar):
    bar.append(41)
    print(bar)
    
    
    # >> [41]

answer_list = []
foo(answer_list)
print(answer_list)
# >> [41]


def foo(bar):
    bar = 'new value'
    print (bar)
    # >> 'new value'

answer_list = 'old value'
foo(answer_list)
print(answer_list)
# >> 'old value'

"""
Explanation: DYI: Fibonacci optimized
Salvare i calcoli intermedi della funzione di Fibonacci in un dizionario da usare come cache.
Eseguire i test per essere sicuri di non aver rotto l'algoritmo
Caratteristiche del modello dati di Python
Tipi di dato "mutable" e "immutable"
Python Data Model
Ogni oggetto ha:
   * identità -> non cambia mai e si può pensare come l'indirizzo in memoria
   * tipo -> non cambia mai e rappresenta le operazioni che l'oggetto supporta
   * valore -> può cambiare se il tipo è mutable, non se è immutable
Tipi di dato immutable sono:

interi
stringhe
tuple
set

Tipi di dato mutable sono:

liste
dizionari

Tipizzazione forte e dinamica
Da http://stackoverflow.com/questions/11328920/is-python-strongly-typed/11328980#11328980 (v. anche i commenti)
Python is strongly, dynamically typed.

Strong typing means that the type of a value doesn't suddenly change. A string containing only digits doesn't magically become a number, as may happen in Perl. Every change of type requires an explicit conversion.
Dynamic typing means that runtime objects (values) have a type, as opposed to static typing where variables have a type.

As for example
bob = 1
bob = "bob"
This works because the variable does not have a type; it can name any object. After bob=1, you'll find that type(bob) returns int, but after bob="bob", it returns str. (Note that type is a regular function, so it evaluates its argument, then returns the type of the value.)
Passaggio di parametro per valore o riferimento?
Nessuno dei due! V. https://jeffknupp.com/blog/2012/11/13/is-python-callbyvalue-or-callbyreference-neither/
Call by object, or call by object reference. 
Concetto base: in python una variabile è solo un nome per un oggetto (= la tripla id,tipo,valore)
In sostanza il comportamento dipende dal fatto che gli oggetti nominati dalle variabili sono mutable o immutable.
Seguono esempi:
End of explanation
"""


def ciao(n, l=[], d={}):
    if n > 5:
        return
    l.append(n)
    d[n] = n
    print("la lista è {}".format(l))
    # print("il diz è {}".format(d))
    
ciao(1)  # [1]
ciao(4)  # [1, 4]
ciao(2)
print("----")

ciao(2, l=[1])
ciao(5)

"""
Explanation: WARNING / DANGER / EXPLOSION / ATTENZIONE!
Cosa succede se inizializzo liste e dizionari nei parametri nominali di una funzione?
End of explanation
"""


# -*- coding: utf-8 -*-

# This is hello_who_3.py
import sys                            # <-- importo un modulo


def compose_hello(who, force=False):   # <-- valore di default
    """
    Get the hello message.
    """ 
    try:                                     # <-- gestione eccezioni `Duck Typing`
        message = "Hello " + who + "!"
    except TypeError:                       # <-- eccezione specifica
    # except TypeError as e:                       # <-- eccezione specifica su parametro e
        print("[WARNING] Il parametro `who` dovrebbe essere una stringa")
        if force:                            # <-- controllo "if"
            message = "Hello {}!".format(who)
        else:
            raise                            # <-- solleva eccezione originale
    except Exception:
        print("Verificatasi eccezione non prevista")
    else:
        print("nessuna eccezione")
    finally:
        print("Bye")
    
    return message


def hello(who='world'):               # <-- valore di default
    print(compose_hello(who))


if __name__ == "__main__":
    hello("mamma")


hello("pippo")

hello(1)

ret = compose_hello(1, force=True)
print("Ha composto {}".format(ret))

try:
    hello(1)
except TypeError as e:
    print("{}: {}".format(type(e).__name__, e))
    print("Riprova")

"""
Explanation: Funzioni con parametri posizionali, nominali, e arbitrari
Una funzione si definisce con def &lt;nomefunzione&gt;([parametri]) dove i parametri possono essere:

posizionali. Ad es: def hello(who)
nominali. Ad es: def hello(who='') o who=None o who='default'
entrambi, ma i nominali devono essere messi dopo i posizionali. Ad es: def hello(who, say="How are you?")
arbitrari sia posizionali con il simbolo * o nominali con **. Come convenzione si utilizzano i nomi args e kw o kwargs. Ad es: def hello(who, say="How are you?", *args, **kw)

I simboli * e ** indicano rispettivamente la rappresentazione di una lista come una sequenza di elementi, e di un dizionario come una sequenza di parametri &lt;chiave&gt;=&lt;valore&gt;
Scope delle variabili
http://www.saltycrane.com/blog/2008/01/python-variable-scope-notes/
e ricordatevi che:
for i in [1,2,3]:
      print(i)
print("Sono fuori dal ciclo e posso vedere che i={}".format(i))
Namespace
I namespace in python sono raccoglitori di nomi e posson essere impliciti o espliciti. Sono impliciti lo spazio dei nomi __builtin__ e __main__. Sono espliciti, le classi, gli oggetti, le funzioni e in particolare i moduli.
Posso importare un modulo che mi va a costituire un namespace con import &lt;nomemodulo&gt; e accedere a tutti i simboli top-level inseriti nel modulo come &lt;nomemodulo&gt;.&lt;simbolo&gt;.
L'importazione di simboli singoli all'interno di un modulo in un altro namespace si può fare con from &lt;nomemodulo&gt; import &lt;simbolo&gt;. Quello che non si dovrebbe fare è importare tutti i simboli di un modulo dentro un altro nella forma: from &lt;nomemodulo&gt; import *. Non fatelo, a meno che non strettamente necessario.
Stack delle eccezioni e loro gestione
Lo stack delle eccezioni builtin, ossia già comprese nel linguaggio python sono al link: https://docs.python.org/2/library/exceptions.html#exception-hierarchy
Derivando da essere facilmente se ne possono definire di proprie.
La gestione delle eccezioni avviene in blocchi:
try:
    ...
except [eccezione] [as variabile]:
    ...
else:
    ...
finally:
    ...
Pratica del Duck Typing! 
« If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck. »
Segue esempio di composizione del saluto con la gestione delle eccezioni:
End of explanation
"""


def main():
    # Step 1. Finché l'utente non scrive STOP - si fa tutto in un while True con un break quando occorre
    # Step 2. L'utente inserisce il nome
    #         Usa raw_input("Inserisci ...") per chiedere le info all'utente
    # Step 3. L'utente inserisce la città
    # Step 4. L'utente inserisce lo stipendio
    # Step 5. Inserisci il dizionario con chiavi 
    #   'name', 'city', 'salary', 'genfibo'
    #   nella lista PEOPLE = []
    PEOPLE.append(person_d)
    # Step 6. Stampa a video PEOPLE nel modo che ti piace
    # Step 7. Riinizia da Step 1
    # FINE
    
    # ---- BONUS ----
    # Step 8. Quando l'utente smette -> scrivi i dati in un file
    #   se vuoi Step 8.1 in formato json
    #   se vuoi Step 8.2 in formato csv
    #   se vuoi Step 8.3 in formato xml
    # Step 9. Fallo anche se l'utente preme CTRL+C o CTRL+Z

"""
Explanation: DYI: Anagrafica e Fibonacci
Sapendo che l'input all'utente si chiede con la funzione raw_input(&lt;richiesta&gt;), scrivere una funzione che richieda nome, città, stipendio, e generazione di Fibonacci dei propri conigli.
End of explanation
"""


