# -*- coding: utf-8 -*-
"""aula_python_4.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/10JwswxJwA6xL3sSl9tIH0HMp3-ZLC5gH

# Revisão

def inicia a definição de uma função
"""


def parabola(x, a, b, c):
    y = a * x ** 2 + b * x + c
    return y


parabola(1, 5, 2, 4)

"""Funções com argumentos com valor padrão:"""


def parabola_2(x, a=5, b=2, c=4):
    y = a * x ** 2 + b * x + c
    return y


parabola_2(1, 6, 7, 8)

"""Funções anônimas podem ser criadas com a palavra-chave lambda. As funções lambdas são restritas a uma única (em geral pequena) expressão. """

f2 = lambda x: x ** 2

f2(5)

f_soma = lambda a, b: a + b
f_soma(5, 9)

"""# Estrutura de dados

Veja a seguir mais métodos de objetos do tipo lista:
"""

l = [5, 18, 43, 77, 91, 18, 77]

"""Adicionar um elemento ao final da lista"""

l.append(100)
print(l)

"""Inserir um elemento em uma dada posição. Nesse caso insere o elemento 9 na posição de índice 1. """

l.insert(1, 9)
print(l)

"""Remover o primeiro elemento da lista que tem valor igual ao argumento (nesse caso remove o elemento 18)"""

l.remove(18)
print(l)

"""Contar o número de vezes que um elemento aparece na lista"""

l.count(77)

print(l)

"""Remover o elemento em uma dada posição. Esse método remove o elemento e o retorna. 

Nesse caso remove o elemento de índice 1.
"""

l.pop(1)

print(l)

"""Devolver o índice do primeiro elemento cujo valor é igual ao argumento (nesse caso 91)

"""

l.index(91)

l.index(77)

l.index(53)

"""Ordenar os elementos da lista"""

print("lista:", l)
l.sort()
print("lista ordenada:", l)

"""Ordenar em ordem reversa"""

l = [5, 43, 77, 91, 18, 77, 100]  # definindo novamente a lista l
l.sort(reverse=True)
print(l)

"""Inverter a ordem dos elementos"""

l = [5, 43, 77, 91, 18, 77, 100]  # definindo novamente a lista l
l.reverse()
print(l)

"""Copiar os elementos da lista"""

l_c = l.copy()
print(l_c)

"""Para iterar sobre uma lista, a posição e o valor podem ser obtidos simultaneamente com a função enumerate()"""

for i, v in enumerate(l_c):
    print(i, v)

"""Para percorrer duas ou mais listas ao mesmo tempo, as entradas podem ser pareadas com a função zip()"""

l1 = [2.5, 3.9, 8.7]  # definindo lista l1
l2 = [8.7, 23.4, 12.5]  # definindo lista l2
for a, b in zip(l1, l2):
    print(a, b)

"""Uma lista pode ser percorrida na ordem inversa com a função reversed()"""

for z in reversed(range(10)):
    print(z)

"""**Compreensões de listas**

Compreensões de listas são uma maneira concisa de criar uma lista. Uma aplicação comum é criar uma nova lista onde cada elemento é resultante da avaliação de uma expressão no contexto das cláusulas for e if. 

Por exemplo, podemos criar uma lista com os quadrados desta forma:

"""

quadrados = []  # cria uma lista vazia e armazena na variável quadrados
for x in range(10):
    quadrados.append(x ** 2)

print(quadrados)

"""Alternativamente, podemos fazer:"""

quadrados2 = [x ** 2 for x in range(10)]
print(quadrados2)

quadrados3 = list(map(lambda x: x ** 2, range(10)))
print(quadrados3)

"""**Tuplas**

Uma tupla consiste em uma sequência de valores separados por vírgulas. Na sua criação os valores de uma tupla podem ser envolvidos ou não por parênteses. Na saída do console tuplas são sempre envolvidas por parênteses. Apesar de serem similares às listas, as tuplas são frequentemente utilizadas em situações e com propósitos distintos. O principal motivo é que tuplas são imutáveis, enquanto listas são mutáveis. 
"""

t = 5, 87, 'bla'
print(t)

"""Tuplas podem ser indexadas

"""

print(t[2])

"""É possível criar uma tupla contendo outras tuplas

"""

u = t, (56, 89, 346)
print(u[0][0])

"""Tuplas são **imutáveis**!"""

t[0] = 65

v = (27.6, 45.9)  # 'empacotamento' de dois valores em uma tupla
vx, vy = v  # processo inverso de 'desempacotamento' também é possível
print(vx)

"""
**Dicionários** 

Uma estrutura de dados muito útil é o dicionário. Eles são indexados por palavras-chaves (keys), que podem ser de qualquer tipo imutável. Os dicionários são delimitados por {} e contém uma lista de pares chave:valor separados por vírgulas. O dicionário vazio é {}. Os dicionários são utilizados para armazenar e recuperar valores a partir das palavras-chaves. É possivel substituir uma valor de uma chave por um novo valor. """

ramais = {'Cristina': 6473, 'Ana': 6486, 'Marina': 7256, 'Allan': 7257, 'Gustavo': 7257}
print(ramais['Marina'])

"""Remover um par chave:valor"""

del (ramais['Allan'])
print(ramais)

"""Incluir novo par chave:valor"""

ramais['Charles'] = 6486
print(ramais)

"""Alterar o valor de uma chave"""

ramais['Cristina'] = 6478
print(ramais)

"""Listar todas as chaves presentes no dicionário """

list(ramais)

"""Verificar se uma palavra-chave está ou não no dicionário"""

'Cristina' in ramais

'Sandra' in ramais

"""O construtor dict cria um dicionário a partir de sequências de pares chave,valor"""

dict([('Cristina', 6473), ('Ana', 6486), ('Marina', 7256)])

dict(Allan=7257, Charles=6486)

"""As compreensões de dicionário também podem ser usadas para criar dicionários a partir de expressões"""

dici = {x: x ** 2 for x in range(10)}
print(dici)

"""Ao iterar sobre dicionários, a chave e o valor podem ser obtidos simultaneamente usando o método items()"""

for nome, fone in ramais.items():
    print(nome, fone)

"""# Módulos

Uma boa prática ao escrever códigos maiores é dividí-lo em arquivos menores, o que pode facilitar a manutenção. 

Um módulo é um arquivo contendo definições e instruções Python. O nome do arquivo é o nome do módulo acrescido do sufixo .py.

Um módulo também pode ser muito útil para escrever funções que são usadas em vários programas diferentes. É preferível usar um arquivo separado para uma função do que copiá-la para dentro de vários programas. 

Por exemplo, usando um editor de texto, foi criado um arquivo chamado fibonacci.py, contendo as funções fib e fib_lista, apresentadas na última aula. Ao importar um módulo, as funções definidas no arquivo são colocadas diretamente na tabelas de símbolos atual e poderão ser chamadas e executadas.
"""

import fibonacci

fibonacci.fib(20)

fibonacci.fib_lista(50)

"""Módulos podem importar outros módulos. É costume colocar todos os comandos de import no início do módulo ou de um programa. 

Outras maneiras de importar as definições de um módulo são:
"""

from fibonacci import fib, fib_lista

from fibonacci import *
# Importa todos os nomes definidos em um módulo
# Nem sempre aconselhado, pois introduz um conjunto desconhecido de nomes no ambiente,
# podendo levar a conflitos com outros nomes previamente definidos

import fibonacci as fibo

fibo.fib(10)

from fibonacci import fib_lista as f_l

f_l(30)

"""O Python guarda versões compiladas de cada módulo no diretório \__ pycache__ com o nome modulo.versão.pyc para acelerar o carregamento de módulos.

# Pacotes

Os pacotes são uma maneira de estruturar o “espaço de nomes” (namespace) dos módulos Python. Eles são uma coleção de módulos. Nos referimos a um módulo dentro de um pacote como nomedopacote.nomedomodulo. 

Os arquivos \__ init__.py são necessários para que o Python trate diretórios contendo o arquivo como pacotes. \__ init__.py pode ser apenas um arquivo vazio.

O pacote **math** contém todas as funções matemáticas padrão (sqrt,log, log10, exp, funções trigonométricas).
"""

from math import log

x = log(3)
print(x)

from math import pi

print(pi)

from math import exp
from math import sin, cos

from math import *

# Algoritmo para converter a posição de um ponto dada em coordenadas polares (r,theta) para coordenadas
# Cartesianas (x,y)

from math import sin, cos, pi

r = float(input('Entre o valor de r: '))
d = float(input('Entre o valor de theta em graus: '))

theta = d * pi / 180.  # conversão de graus para radianos
x = r * cos(theta)
y = r * sin(theta)

print('x=', x, 'y=', y)

"""O pacote **numpy** é fundamental para computação científica com Python. Ele oferece funções matemáticas, geradores de números aleatórios, rotinas de álgebra linear, transformada de Fourier e muito mais. Mas o seu poder está em permitir cálculos em arrays multidimensionais. 

O numpy executa facilmente cálculos numéricos que são muito utilizados em problemas de machine learning (aprendizado de máquina) e de processamento de imagem (imagens no computador são representadas como arrays multidimensionais de números). Além disso, o numpy oferece várias outras tarefas matemáticas.

Para mais informações, consulte https://numpy.org/.

# Arrays

Um array é um conjunto ordenado de valores. Mas há diferenças importantes entre listas e arrays:
* O número de elementos de um array é fixo e não é possível adicionar ou remover elementos de um array após criá-lo;
* Os elementos de um array devem ser todos de um mesmo tipo, como float ou inteiros.

As principais vantagens de usar arrays em vez de listas são:
* Arrays podem ter qualquer número de dimensões. Arrays unidimensionais e bidimensionais são, respectivamente, como vetores e matrizes em álgebra linear. Listas são sempre unidimensionais. 
* Como se comportam como vetores ou matrizes, podemos fazer qualquer aritmética com eles, ao contrário de listas.
* Arrays funcionam mais rápido que listas em Python.

Cada dimensão de um array é chamada axis. O número de axes é chamada de rank do array. A indexação dos arrays também começa em 0, com um número para cada axis do array.
"""

from numpy import array

a = array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print(a)

# Minha recomendação
import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print(a)

type(a)  # checar que o tipo é um numpy array

a.dtype  # fornece o tipo dos elementos do array

a.shape  # dimensões do array

a.ndim  # número de dimensões

a.size  # número de elementos

"""Indexação de um array: o índice da linha vem primeiro e depois o da coluna"""

print(a)

print(a[2, 1])

a[1, 0:2]

"""Iterando sobre um array

"""

b = np.array([0, 2, 5, 8], dtype=int)
print(b)

for z in b:
    print(z)

for i in range(len(b)):
    print(b[i])

"""Outras formas de criar arrays

"""

a0 = np.zeros(5)
print(a0)

a0_2 = np.zeros([3, 4])
print(a0_2)

a1 = np.ones(7)
print(a1)

a1_2 = np.ones([2, 3])
print(a1_2)

a1_3 = np.ones([2, 3, 4])
print(a1_3)

"""A função arange retorna valores espaçados igualmente em um certo intervalo. 
O espaçamento é definido pelo passo (terceiro argumento da função).

numpy.arange(início,fim,passo)
"""

a2 = np.arange(1., 4., 0.5)
print(a2)

"""A função linspace também retorna n valores espaçados igualmente em um certo intervalo. 

numpy.linspace(início,fim,n) 
"""

a3 = np.linspace(1., 10., 3)
print(a3)

"""É possível criar arrays com números aleatórios (entre 0 e 1)

"""

a5 = np.random.rand(3, 4, 2)
print(a5)

"""**Operações com arrays**

O poder do numpy vem da possibilidade de realizar operações com arrays.
"""

c = np.array([1, 2])
d = np.array([3, 4])

print(c + d)

print(c * d)

print(c ** d)

e = a1_2 + 6.5
print(e)

# Produto escalar de dois array é feito pela função dot
prod_esc = np.dot(c, d)
print(prod_esc)

log10_c = np.log10(a5)
print(log10_c)
print(log10_c[1, 0, 0])

"""Cuidado ao fazer cópias de arrays"""

a = np.array([1, 1])
b = a
a[0] = 2
print(a)
print(b)

a = np.array([1, 1])
b = np.copy(a)
a[0] = 2
print(a)
print(b)