# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + [markdown] colab_type="text" id="rwsDD6KizieW"
# ## Altres maneres d'estructurar les dades
#
# Al principi del tema hem vist una estructura de dades que organitza la informació de manera ordenada i que és
# mutable, és a dir una llista.
#
# En aquesta part del capítol veurem:
#
# * **Tuples**: molt similars a les llistes sense mutabilitat.
# * **Diccionaris**: una estructura no lineal que relaciona parells de dades.
# * **List comprehensions**: eina que crea una nova llista basada en una altra llista, en una sola línia i de manera llegible.
# * **Funcions lambda**: funcions anònimes, és a dir sense nom.
# * **Map, filter**: funcions de Python que usen les funcions anònimes i actuen sobre les llistes.
# * **Yield**: Funcions generadores de seqüències.
# -

# ### Tuples
#
# Python proporciona un altre tipus de col·lecció ordenada d'objectes, anomenada tupla.
#
# Les tuples són idèntiques a les llistes en tots els aspectes, tret de les propietats següents:
#
# * Les tuples es defineixen especificant els elements entre parèntesis, en comptes de claudàtors.
# * Les tuples **són immutables**.
#

# +
# Creació d'una tupla i us amb funcions
vocals = ('a', 'e', 'i', 'o', 'u')

# Operadors
hi_es = 'u' in vocals

print(hi_es)


long = len(vocals)
print(long)

maxim = max(vocals)
print(maxim)


# Indexacio
vocals[0] = 'A'
# -

# Les tuples es poden desempaquetar, és a dir, transformar-les en variables individuals:

# +
a, e, i, o, u = vocals

print(a + " - " +  e + " - " + i)

# +
x = 4
y = 5

print(str(x) + " - " +  str(y))
# -

aei, o , u = vocals  # ens dona un error ja que hauria de posar 5 variables i no 3
print(aei)

a = 4
b = 5
tupla_int = (a, b)
print(tupla_int)


# Això ens és molt pràctic per construir funcions _que retornen més d'un valor_ , no és una pràctica altament recomanable
# però si que pot ser molt útil en algunes situacions.

# +
def retorna2():
    a = 3
    b = 4
    return (a, b)

primer, segon = retorna2()

print(primer)
print(segon)
# -

# **Per què utilitzar una tupla en lloc d'una llista?**
#
# Quan no volem que les dades es puguin modificar. Si es pretén que els valors de la col·lecció es mantinguin constants
# durant tota la vida del programa, cal usar una tupla en lloc d'una llista, ja que protegirà les dades contra una possible
# modificació accidental.

# + [markdown] colab_type="text" id="J_9IX_vk4vTk"
# ### Diccionaris
#
# Un diccionari consisteix en una col·lecció **no ordenada** de parells de clau-valor.
#
# A diferència de les seqüències, indexades per un rang de nombres, els diccionaris són indexats per claus, que poden ser
# de **qualsevol tipus immutable**; els *strings* i els nombres sempre poden ser claus.
#
# Les tuples es poden utilitzar com a claus. No podem utilitzar llistes com a claus, ja que les llistes es poden modificar.
#
# El millor és pensar en les claus del diccionari com un **conjunt**, on a cada element del conjunt li correspon un valor.
#
# A continuació teniu les operacions bàsiques:

# + colab={"base_uri": "https://localhost:8080/", "height": 92} colab_type="code" id="sMFkcWrwzEeN" outputId="9dc0134b-58d6-4ff6-c1fa-3b7d23971dff"
# Creacio d'un diccionari nou

dicc = dict() # Fixau-vos que aixo es el constructor de la classe diccionari
# Tambe es podria construir aixi:
#dicc2 = {}


dicc = {43142512: "<NAME>",  44216793:"<NAME>", 44444444: "<NAME>"}
print(dicc)
# -

# #### Accedint als elements d'un diccionari
#
# En aquesta estructura no accedim als elements amb un índex, sinó que es fa amb el valor d'una clau.

# Donada una clau obtenció del seu valor:
nom = dicc[44444444]
print(nom)


# +
diccionari_strings = {"biel": "professor", "arnau": "alumne"}

diccionari_strings["arnau"]
# -

# #### Mutabilitat
#
# Un cop hem creat un diccionari, podem afegir, eliminar, canviar i moure elements a voluntat. `Python` ofereix una àmplia
# gamma de maneres d'operar amb els diccionaris.
#
#
# #### Modificació d'un valor
#
# Un únic valor d'un diccionari es pot substituir, de manera molt similar a com modifiquem una variable, però ara hem
# d'especificar quin dels valors volem modificar, ho fem mitjançant la seva clau.

# +

#Donada una clau podem modificar el seu valor
dicc[43142512] = "<NAME>" 
nom = dicc[43142512]
print(nom)
# -

# **Afegir informació**
#
# També podem afegir noves parelles clau-valor de manera dinàmica.

# +
# Insercio d'una parella clau - valor
dicc[43142133] = "<NAME>"


# Inserció de noves parelles clau - valor
dicc[41142133] = "<NAME>"
dicc[46094245] = "<NAME>"
dicc[40111134] = "<NAME>"

print(dicc)
# -

#
# ### Operadors, funcions i mètodes
#
#
# **Operadors**
#
# Als diccionaris també podem aplicar l'operador de pertinença ```in``` i el seu modificador ```not```

# +
# Consultam pertanyença

dni = 40111134
print("Tenim el dni", dni, "? ")

pertany = 40111134 in dicc
print(pertany)

print(dicc[dni])
# -

# **Funcions**
#
# Als diccionaris també podem usar la funció ```len``` que ens diu quants d'elements conté el diccionari.
#
# També podem usar les funcions ```max``` i ```min``` que ens retornen informació de les claus:

# +
longitud = len(dicc)
print(longitud)


valor_maxim = max(dicc)
print(valor_maxim)

# + [markdown] colab_type="text" id="FxQ5xnR9Btp8"
# **Mètodes**
#
# A més de modificar i afegir elements usant els claudàtors també ho podem fer mitjançant mètodes propis dels
# diccionaris. A més, usant els mètodes adients podem obtenir la informació del diccionari en forma de llista, siguin:
# les claus, els valors o tuples clau-valor:
#
# Modificació:
#
# * ```clear```: Elimina totes les parelles clau-valor del diccionari.
# * ```get```: Ens retorna el valor de la clau que passem per paràmetre, si la clau no existeix, retorna Error.
# * ```pop```: Elimina la clau del diccionari i ens torna el seu valor, si no existeix retorna Error.
#

# +
## get
clau = 40111134
valor =  dicc.get(clau)
print(valor)

# pop 
#clau2 = 43142133
valor = dicc.pop(clau2)   # error si una clau no existeix

print(valor)

nombre_elements = len(dicc)
print(nombre_elements)


# clear
#dicc.clear()
print(dicc)
# -

# Consulta:
#
# * ``` keys```: mètode que ens retorna una llista amb el conjunt de claus que hi ha al diccionari.
# * ``` values```: mètode que ens retorna una llista amb els valors que hi ha al diccionari.
# * ``` items```: mètode que ens retorna una llista de tuples. Cada tupla té una clau i el seu valor corresponent.
#
# És important destacar 2 coses:
#
#   * Les llistes que obtenim de les operacions anteriors, poden no seguir cap ordre.
#   * Obtenir llistes ens dóna la possibilitat d'iterar sobre elles (usar un `for`).

# + colab={"base_uri": "https://localhost:8080/", "height": 167} colab_type="code" id="KeG2bEncBuJ6" outputId="0070bc2e-f1d7-4512-9370-4311ccb34494"
#Del nostre diccionari en podem obtenir les claus amb el metode keys()
print("Claus", end= " -> ")
print(dicc.keys())


#Podem tenir tots els valors
print("Valors", end= " -> ")
print(dicc.values())

#Tambe podem obtenir totes les parelles clau - valor en format llista de tuples
print("Parelles", end= " -> ")
print(dicc.items())

print("Iteracions")
print("Iteracio sobre les claus d'un diccionari: ")
for clau in dicc.keys():
    print(clau, end= " ")
    print("")

print("Iteracio sobre les claus valors d'un diccionari: ")

for item in dicc.items():
    clau, valor = item # Desempaquetam els valors d'una tupla
    print(valor, end= " ")
    

#NOTA: Aquest for es pot fer d'almanco una altra manera mes: desempaquetar a l'encapçalament

for clau, valor in dicc.items():
    print(clau)
    
# -

# Les llistes i els diccionaris són dos dels tipus Python més usats. Com heu vist, tenen diverses similituds, però
# difereixen en com s’accedeix als seus elements. S'accedeix als elements de les llistes mitjançant un índex numèric en
#  funció de l'ordre i al diccionari s'hi accedeix mitjançant claus.
#
# A causa d'aquesta diferència, les llistes i diccionaris són adequats per a circumstàncies diferents.

# + [markdown] colab_type="text" id="pceCDfOjFFKO"
#
# ### List comprehension
#
# Ens proporcionen una manera concisa de crear llistes. 
#
# Les aplicacions més habituals són la creació de noves llistes en què cada element és el resultat d’alguna operació
# aplicada a cada membre d’una altra seqüència o per crear una subseqüència d'aquells elements que satisfan una
# determinada condició.
#
# nova_llista = **[** expresio **for** element **in** iterable **]**
#
# Per exemple, suposam que volem crear una llista dels quadrats dels nombres entre 0 i 10:

# + colab={"base_uri": "https://localhost:8080/", "height": 36} colab_type="code" id="dTToOzn2I3O4" outputId="83566706-6dc4-4cad-883f-8f88a6ecfd6f"
# Manera tradicional
quadrats = []
for x in range(0, 10):
    quadrats.append(x**2)

print(quadrats)
# La manera de generar aquesta mateixa llista amb list comprehension:
llista_original = (0,1,2,3,4,5,6,7,8,9)
quadrats_2 = [x**2 for x in llista_original]
print(quadrats_2)

# + [markdown] colab_type="text" id="_BYeJj1hK-VF"
# A continuació teniu varis exemples del seu ús:

# + colab={"base_uri": "https://localhost:8080/", "height": 111} colab_type="code" id="wn0LQpjBLCF5" outputId="e697c11f-cdd5-45bd-e26a-2a8634d36e47"
vec = [-4, -2, 0, 2, 4]

# donada una llista, multiplicam per dos cada un dels seus valors
r1 = [x*2 for x in vec]
print(r1)


# -

# nova_llista = **[** expresio **for** element **in** iterable **if** condicio **]**

# ens quedam amb els valors positius
r2 = [x for x in vec if x >= 0]
print(r2)

# podem aplicar funcions a cada un dels elements
r3 = [abs(x) for x in vec]
print(r3)

# +
# tenim molta flexibilitat, per exemple:
r5 = [(x, x**2) for x in range(6)]
print(r5)


nombre, quadrat = r5[len(r5)-1]
print(nombre)
print(quadrat)

# + [markdown] colab_type="text" id="jKlpTcVS72z2"
# ### La funció *zip*
#
# La funció `zip` pren una o més seqüències i combina els elements corresponents de les seqüències en una tupla. Es
# deté quan s'esgota la seqüència més curta.

# + colab={"base_uri": "https://localhost:8080/", "height": 167} colab_type="code" id="-fGNMN7_77WW" outputId="f6f89a12-23b0-490b-f4f7-05e211a33732"
l = list(range(18))
l2 = list(range(6))

z = list(zip(l,l2))

print("Resultat d'executar zip")
print(z)

for element in z:
    
    print(str(element[0]) + " - " + str(element[1]))

# +
# Exemple sumar els elements de dues llistes

import random

# Agafam 6 mostres d'una llista que te els elements de 0 a 99
l3 = random.sample(range(100), 6) 

print("Llista generada aleatoriament")
print(l3)
print("Llista simple")
print(l)

zipat = zip(l,l3)
print(list(zipat))

# +
# En aquest exemple aplicam un list comprehension a la funcio zip
suma = [x + y for x, y in zip(l, l3)]

print("Suma")
print(suma)


# +
longitud = len(l3)-1

sumatori = []
for i in range(0, longitud):
    
    sumat = l[i] + l3[i]
    sumatori.append(sumat)

print(sumatori)
    
# -

# **Exercicis**
#
# 1. Donada una llista amb els nombres d'1 a 100, obtenir una llista amb els senars.
# 2. Donada una llista amb tots els nombres d'1 a 7000, eliminar tots els múltiples de 7.
# 3. Crear un llista de tuples que tingui els primers 50 nombres parells, juntament amb els 50 primers nombres que no
# són múltiple de 7.

# +
### 1
llista_1_100 = range(1, 101)

llista_senars = [x for x in llista_1_100 if x % 2 == 1]

#print(llista_senars)

#2
llista_7 = [x for x in range(1, 7001) if x % 7 != 0]

#print(llista_7)

#3

zippat = zip(llista_senars, llista_7)
print(list(zippat))


# + [markdown] colab_type="text" id="-v5K7zkxSNYv"
# ### Funcions lambda
#
# A `Python`, utilitzem la paraula clau `lambda` per declarar una funció anònima. Una funció anònima fa referència a
# una funció declarada sense nom.
#
# Tot i que sintàcticament semblen diferents, les funcions lambda es comporten de la mateixa manera que les funcions
# regulars que es declaren usant la paraula clau `def`.
#
# Les següents són les característiques de les funcions lambda de Python:
#
# * Una funció lambda pot prendre qualsevol nombre d’arguments.
# * De forma sintàctica, les funcions lambda només **es limiten a una sola expressió**. Recordar que una expressió és una
# peça de codi executada per la funció lambda.
#
# La seva sintaxi és:
# ```lambda argument(s): expressio```

# + colab={"base_uri": "https://localhost:8080/", "height": 54} colab_type="code" id="LtUfSRUBUdIr" outputId="f53aee43-7652-4b83-8df7-7496c717e6ef"
def divisible2(num):
    return num % 2 == 0

# es equivalent a:

f = lambda num: num % 2 == 0

print(f(22))


# no es necessari ni tan sols assignar un identificador

print((lambda num: num % 2 == 0)(5))


# Poden tenir el nombre de paràmetres que volguem

s = lambda x,y,z:  x+y+z

print(s(5,5,10))


# + [markdown] colab_type="text" id="nRya-1-ZXWVu"
# Les funcions anònimes (`lambda`) s’utilitzen quan necessitem una funció durant un període curt de temps.
#
# Normalment s'utilitza quan es vol passar una funció com a argument per a funcions d'ordre superior, és a dir,
# funcions que prenen altres funcions com a arguments tal com veurem a continuació:

# + [markdown] colab_type="text" id="RuGny5xSk_y7"
# ### Map i Filter
#
# Aquestes dues funcions ens permeten aplicar funcions a objectes iterables (llistes, tuples, strings o diccionaris)
#
# #### Map
#
# La funció `map` és una funció del nucli de `Python` que com a paràmetres rep una funció i una o més llistes. La
# sintaxi de la funció map és la següent:
#
# `map(funcio, iterable_1, iterable_2, ...) `
#
#
# `map ` aplica a cada element de l'iterable la funció que rep com a primer paràmetre. I ens retorna un iterable que
# podem transformar de manera molt senzilla en una llista (usant la funció` list`).
#
# Anem a veure un exemple:

# + colab={"base_uri": "https://localhost:8080/", "height": 92} colab_type="code" id="6AyNHHpvnxfz" outputId="8754b6fc-fda5-496f-f91c-7ae0ca77512c"
llista = list(range(10))

#definim una funcio lambda
suma_3 = lambda x: x + 3

# aplicam aquesta funcio a una llista
resultat = list(map(suma_3, llista))
#print(llista)
#print(resultat)


llista_nova = [x + 3 for x in llista]
#print(llista_nova)


# Anem a sumar els valors de dues llistes
#print("Suma dues llistes")
suma_2_ll = map(lambda x,y: x+y, llista, resultat) # la funcio esta declarada directament

suma_2_ll = list(suma_2_ll)
print(llista)
print(resultat)
print(suma_2_ll)


resultat_2 = [x + y for x, y in zip(llista,resultat)]
print(resultat_2)

# + [markdown] colab_type="text" id="QTpO63o7rnlp"
# #### Filter
#
# Aquesta funció també pertany al nucli de `Python`, la seva sintaxi és la següent:
#
# `filter(funcio, iterable) `
#
# La funció que passem ha de retornar un valor de tipus booleà. L’objecte serà cridat per a cada ítem de l'iterable per
# fer-ne l’avaluació. Evidentment per cada element tindrem un valor `True` o `False`.  Aquesta funció només pot tenir un
# sol paràmetre iterable
#
# La funció `filter` retorna una llista d'aquells elements que s'avaluen com a certs per la funció.
#
# Anem a veure un exemple:

# + colab={"base_uri": "https://localhost:8080/", "height": 92} colab_type="code" id="Nod-piwTnvIu" outputId="dc949943-d8bb-43cc-b5a0-fb81f5a99ee3"
nombres = [2, 6, 8, 10, 11, 4, 12, 7, 13, 17, 0, 3, 21]

filtratge = filter(lambda num: num > 7, nombres)
print("Eliminam els valors majors a 7")
print(list(filtratge))  


filtratge_2 = [ y for y in nombres if y > 7]
print(filtratge_2)


# + [markdown] colab_type="text" id="mJdO3pvwMwBh"
# ### Yield
#
# Els generadors són un tipus d’iterador els elements dels quals només podem usar una vegada. Els generadors no
# emmagatzemen tots els valors de l'iterador en la memòria, els generen sobre la marxa:
#
# `yield` és una paraula clau que s'utilitza com a `return` d'una funció, excepte que la funció retornarà un generador.
#
# Vegem un exemple en codi:

# + colab={"base_uri": "https://localhost:8080/", "height": 92} colab_type="code" id="PgroqM8QdkDm" outputId="a40bc37c-3575-497c-af4d-3d5995f5a8c7"
#Primer exemple
def generador_simple():
    print("Inici")
    yield 1
    yield 2
    yield 3
    yield 25
  
  
generador = generador_simple()


print(next(generador))
print(next(generador))
print(next(generador))

nou_valor = next(generador)
print(nou_valor)


# + colab={"base_uri": "https://localhost:8080/", "height": 73} colab_type="code" id="PXsRCBkrMvpk" outputId="b3411b34-790d-4def-c5bd-36e7b623958d"
# Generador en un recorregut usant un for

def generador_quadrats(maxim):
    i = 0
    while i <= maxim:
        yield i*i
        i = i +1
    
generador = generador_quadrats(10)

y = 0
while y < 4:
    valor = next(generador)
    print(valor)
    y = y + 1
    
nou_valor =  next(generador)
print(nou_valor)


# + [markdown] colab_type="text" id="ez22pWqpSWds"
# És imporant entendre que quan es crida per primer cop a la funció, el codi de la funció no s’executa. La funció només
# retorna l’objecte del generador. La primera vegada que usem el nostre generador, s'executarà el codi de la nostra
# funció des del principi fins que arribi a `yield`, i tornarà el primer valor del bucle. A continuació, cada una de
# les altres trucades del nostre generador executarà el bucle de la funció una vegada més i retornarà el valor següent,
#  fins que no hi hagi cap valor per tornar.
#
# El generador es considera buit una vegada que la funció s’executa, però ja no es produeix un nou valor. Pot ser que
# el bucle hagi acabat, o perquè ja no satisfà una certa condició.

# + colab={"base_uri": "https://localhost:8080/", "height": 1812} colab_type="code" id="pjidTpM_W1-3" outputId="2397e2a0-bbed-4ae4-f25c-ad9cdeed196a"
# Aquí tenim un exemple que ens permet generar tants elements de la successio de
# Fibonacci com necessitem.
def Fibo():
    pre1 = 1
    pre2 = 1
    suma = 1
    yield suma
    while True:
        yield suma
        suma = pre1 + pre2
        pre1 = pre2
        pre2 = suma
        
generador = Fibo()
i = 0
while i < 33:
  
    print(next(generador))
    
    i += 1

# + colab={} colab_type="code" id="KGv_TuNnPVtz"
l = list(range(0,101))


StarcoderdataJupyterScriptsDedupFiltered