a = None
if a is None:
    print('nulo')

"""
Explanation: Garimpagem de Dados
Aula 1 - Python Básico
Nulo
Em Python, o tipo None representa a falta de valor em uma variável
End of explanation
"""


a = True
b = False
c = True
print(a == b)
print(a == c)

"""
Explanation: Boolean
Booleanos são escritos com a primeira letra maiúscula
End of explanation
"""


print(type(1))
print(type(1.2))

"""
Explanation: Números
Em python, existem dois tipos básicos de números: Inteiros (int) e Ponto Flutuante (float)
End of explanation
"""


print("Divisao: ",1 / 2)
print("Divisao inteira: ",1 // 2)

"""
Explanation: Ao dividir dois números (inteiros ou flutuantes), podemos fazer a divisão comum (/) ou divisão inteira (//), onde a última mantém apenas a parte inteira do resultado
End of explanation
"""


print(2 ** 3)
print(25 ** (1/2))

"""
Explanation: Existe também o operador de potência, permitindo elevar um número ao outro.
PS: como as raízes também são potências (porém, invertidas), podemos computá-las com o mesmo operador
End of explanation
"""


import math
print(math.sqrt(2))
print(math.log(2))
print(math.ceil(2 ** 0.5))
print(math.floor(2 ** 0.5))

"""
Explanation: Python, assim como muitas linguagens, conta com um módulo que implementa diversas funções matemáticas (raíz, logaritmo, topo, piso, etc.
End of explanation
"""


import random
print(random.random())        # normalizado entre 0 e 1
print(random.randint(1,10))   # intervalo fechado

"""
Explanation: Python também conta um módulo de funções para operações randômicas
End of explanation
"""


single_quoted_string = 'Data Science'
print(single_quoted_string, type(single_quoted_string))

print(len(single_quoted_string))    # tamanho da string
print(single_quoted_string.upper()) # colocar caracteres em maiusculo
print(single_quoted_string.lower()) # colocar caracteres em minusculo

"""
Explanation: Strings
Em Python, trabalhar com strings é bem simples, uma vez que já existem diversas funções implementadas.
Para criar uma string em Python, basta colocar o conteúdo entre aspas, simples ou duplas (o tipo de aspas que começa a string é o mesmo tipo que irá finalizá-la)
End of explanation
"""


str1 = 'Hoje_vai_chover_novamente.'.replace('_', ' ')
print(str1)

"""
Explanation: Podemos facilmente substituir a ocorrência de um caractere/substring utilizando o método replace
End of explanation
"""


print(' Hello '.strip())  # remove no começo e no final
print(' Hello '.lstrip()) # remove no começo
print(' Hello '.rstrip()) # remove no final

"""
Explanation: Outra operação interessante de limpeza de strings é o strip, o qual remove os espaços em branco no começo e no final da string
End of explanation
"""


multi_line_string = """linha 1
linha 2
linha 3"""
print(multi_line_string)
print(repr(multi_line_string))

"""
Explanation: Também podemos criar string mais longas, com múltiplas linhas, utilizando um total de 6 aspas (3 para abrir e 3 para fechar)
End of explanation
"""


multi_line_string.split("\n")

"""
Explanation: Quando temos strings que contém múltiplas informações e desejamos separá-la, podemos usar a função split, informando o caractere/substring a ser utilizado para dividir a string
End of explanation
"""


print("Big" + " " + "Data")
print('Repete ' * 5)

"""
Explanation: Fato interessante: strings em python podem ser manipuladas com operadores de soma (concatenação) ou multiplicação (repetição)
End of explanation
"""


# %d e %f indicam, respectivamente, que um número inteiro ou ponto flutuante será inserido
print("%d/%f/%d" % (2, 4.5, 6))
print("%s/%s/%s" % ('a', 2, False)) # %s indica que algo será inserido como string

"""
Explanation: Existem algumas maneiras de interpolar strings, ou seja, injetar valores dentro dela
End of explanation
"""


print("{}/{}/{}".format(2, 4, 6))
print("{:2d}/{:2d}/{:2d}".format(2, 4, 16))
print("{:02d}/{:02d}/{:02d}".format(2, 4, 160))
print('{:.2f}'.format(99.8765))
print('{:.0f}'.format(99.8765))

"""
Explanation: Outra maneira de interpolar strings é usando o format
End of explanation
"""


valor = 99
if valor == 99:
    print('veloz')
elif value > 200:
    print('muito veloz')
else:
    print('lento')

"""
Explanation: Condições
Estruturas condicionais em Python seguem a mesma lógica de diversas outras linguagens. Uma diferença é que, para os casos em que encadeamos várias condições, para não escrevermos else if ... temos o operador elif
End of explanation
"""


x = 5
par_ou_impar = "par" if x % 2 == 0 else "impar"
print(par_ou_impar)

"""
Explanation: Atribuição ternário
Python também conta com atribuição ternária
End of explanation
"""


x = 0
while x < 5:
    print(x)
    x += 1


"""
Explanation: Laços / Loops
Em Python, a estrutura de repetição while funciona de maneira análoga a diversas outras linguagens:
End of explanation
"""


# definindo um intervalo

for i in range(10): # de 0 a 9, pulando de 1 em 1
    print(i, end= " ")
    
print()

for i in range(5,10): # de 5 a 9, pulando de 1 em 1
    print(i, end= " ")
    
print()

for i in range(0,10,2): # de 0 a 9, pulando de 2 em 2
    print(i, end= " ")
    
print()

# utilizando uma sequência pre-definida
a = [1, 3, 4, 5, 7]
for i in a:
    print(i)

# a função enumerate retorna uma lista de pares (i,x), onde: 
# i é o índice (0, 1, 2, ...)
# x é o elemento da sequência original
a = [1, 3, 4, 5, 7]
for indice, valor in enumerate(a):
    print(indice, valor)

# podemos também percorrer uma coleção mas não utilizar o valor daquele passo
for _ in range(5):
    print('oi')

# existem também dois operadores dentro de um for:
# continue - encerra o passo atual e passa para o próximo
# break - encerra o laço
for x in range(10):
    if x == 3:
        continue
    if x == 5:
        break
    print(x)


"""
Explanation: Entretanto, o for funciona iterando sobre uma coleção/sequência de valores. Existem diversas maneiras de fazer isso:
End of explanation
"""


def soma(a, b):
    return a + b

print(soma(3, 5))
print(soma('casa ', 'organizada'))

"""
Explanation: Funções
Para definir uma função em python, basta utilizarmos a seguinte estrutua:
def nome_da_função (argumento1, argumento2, ...): 
    corpo da função
End of explanation
"""


def f1():
    print("Function 1")
    
def f2():
    print("Function 2")
    
def chamar_funcao(f):
    f()
    
chamar_funcao(f1)
chamar_funcao(f2)

"""
Explanation: Manipulando Funções
Python pode ser considerada uma linguagem funcional. Uma das características do paradigma funcional é permitir que uma função seja manipulada como um valor ou variável, ou seja, podemos passar funções como argumentos para outras funções
End of explanation
"""


chamar_funcao(lambda : print("Lambda function"))

"""
Explanation: Existem também as funções anônimas (chamadas, em Python, de lambda). Uma função anônima é uma função que contém apenas a assinatura e o corpo, sem um nome definido. Em Python, elas recebem o nome de lambda pois elas são funções de um comando só (diferente de linguagens como Javascript e Scala, que permitem funções anônimas de múltiplas linhas).
End of explanation
"""


from datetime import datetime
import time

inicio = datetime.now() # obtém o timestamp do momento atual
print(inicio)
for i in range(2):
    time.sleep(1) # aplica um delay, em segundos
fim = datetime.now()
print(fim)
print("Tempo de execução: {}".format(fim - inicio))

"""
Explanation: Data
Em Python, podemos manipular informações de data, hora e tempo utilizando o módulo datetime
End of explanation
"""


lista = [1, 2, 3, 4, 5]
print(lista)

"""
Explanation: Listas
Quando desejamos trabalhar com uma sequência de dados, podemos utilizar uma lista
End of explanation
"""


lista[2] #elemento da lista

lista[-1] # último elemento

lista[1:3] #sublista

lista[:3] # 3 primeiros elementos

lista[-2:] # 2 últimos elementos

lista[1:-1] # do segundo ao penúltimo elemento

"""
Explanation: Existem diversas maneiras de acessar os elementos de uma lista:
End of explanation
"""


len(lista)

"""
Explanation: O operador len pode ser utilizado para medir tamanhos de objetos com múltiplas informações (strings, lista, etc.)
End of explanation
"""


sum(lista)

"""
Explanation: Podemos somar elementos de uma lista simplesmente usando a função sum
End of explanation
"""


print(10 in lista)
print(5 in lista)

"""
Explanation: Podemos também consultar a pertinência de um elemento em uma lista utilizando o operador in
End of explanation
"""


lista.append(7) # adiciona elemento na lista
lista.append(6)
lista.append(7)
print(lista)

lista.remove(7) # remove o primeiro elemento cujo valor é 7
print(lista)

lista.extend([8, 9, -10]) #extende a lista com outra lista
lista

"""
Explanation: Em Python, as listas são mutáveis, ou seja, ela pode ser modificada.
End of explanation
"""


lista2 = lista  # lista2 referencia lista
lista2[-1] = 10
print(lista)
print(lista2)

"""
Explanation: Por padrão, as variáveis em Python (quando apontando para tipos de dados mais complexos) são ponteiros, ou seja, elas são uma referência a um dado alocado na memória. Logo, quando salvamos o valor de uma variável A em uma variável B, estamos apenas dizendo que B aponta para a mesma informação que A
End of explanation
"""


lista3 = lista[:]  # lista3 é uma nova lista com o mesmo conteúdo de lista
lista3[-1] = -10
print(lista)
print(lista3)

"""
Explanation: Embora a lista seja mutável, as operações de acesso aos elementos (com o operador [ ]) retornam novos elementos. Logo, podemos fazer uma cópia de uma lista da seguinte maneira
End of explanation
"""


lista4 = [-4, 1, -2, 3]

print(sorted(lista4)) # ordena sem alterar a lista
print(sorted(lista4, reverse=True))
print(sorted(lista4, key=abs))
print(lista4)

lista4.sort() # altera a lista original
print(lista4)

"""
Explanation: Existem duas maneiras de se ordenar uma lista em Python: uma abordagem mutável (que altera a lista original) e uma imutável (que retorna uma nova lista ordenada)
End of explanation
"""


import random

a = ['a', 'casa', 'está', 'muito', 'bem', 'organizada']

random.shuffle(a) # mutável
print(a)
random.shuffle(a)
print(a)

random.choice(a) # com repetição

numeros = range(1, 100)
random.sample(numeros, 5) # sem repetição

"""
Explanation: O módulo random contém também um conjunto de funções para trabalharmos com listas. Podemos embaralhá-las, escolher um elemento aleatório ou obter uma amostra de tamanho n
End of explanation
"""


print(a)
print(' '.join(a))

"""
Explanation: A representação padrão de lista em string é uma sequência das strings de seu conteúdo, separado por ', ' e cercados por colchetes. O método join, de strings, existe para usar uma string como separador de uma lista, conforme o exemplo abaixo
End of explanation
"""


b = [1, 'a', 25, 'abc']
print(b)
print(' '.join(b))

"""
Explanation: OBS: se um elemento da lista não for uma string, o método join não funciona
End of explanation
"""


a = [1, 2, 3, 4, 5]

doubled_a = map(lambda x: 2*x, a)
doubled_a

"""
Explanation: Operações funcionais sobre listas
Existem algumas funções primitivas para operar sobre os elementos de uma lista. Duas delas são transformação (map) e filtragem (filter).
Transformação
O objetivo desta função é, como o próprio nome sugere, transformar os elementos de uma lista, gerando uma nova lista. Em termos mais matemáticos: dada uma função f(x) definida para todos os elementos de uma lista L [a1, a2, ..., an], será gerada uma nova lista L2 [b1, b2, ..., bn] tal que bi = f(ai).
Para realizar uma trnasformação, utilizamos a função map
End of explanation
"""


#descomente uma alternativa para checar seu funcionamento

doubled_a = map(lambda x: 2*x, a)

# for x in doubled_a:
#     print(x, end=" ")

# str_doubled_a = map(lambda x: str(x), doubled_a)
# print(" ".join(str_doubled_a))

# list(doubled_a)

"""
Explanation: O retorno do map é iterador, ou seja, os seus valores só existirão qdo forem chamados (seja em um for, outro map ou fazendo um cast para lista) e, uma vez que forem consumidos, o iterador fica vazio
End of explanation
"""


even_a = filter(lambda x: x%2 == 0, a)
even_a

"""
Explanation: Filtragem
Obviedades a parte, o objetivo dessa operação é gerar uma nova coleção apenas com os elementos que satisfaçam uma condição. De maneira mais matemática: dado um predicado p(x) definido para todos os elementos de uma lista L [a1, a2, ..., an], será gerada uma nova lista L2 [b1, b2, ..., bm] tal que bi $\in$ L e p(bi) é verdade.
End of explanation
"""


#descomente uma alternativa para checar seu funcionamento

even_a = filter(lambda x: x%2 == 0, a)

# for x in even_a:
#     print(x, end=" ")

# str_even_a = map(lambda x: str(x), even_a)
# print(" ".join(str_even_a))

# list(even_a)

"""
Explanation: Novamente temos um iterator como retorno. Podemos utilizar as mesmas abordagens do iteratoor apresentado no map
End of explanation
"""


quadrados = [x * x for x in range(5)]
print(quadrados)

quadrados_dos_pares = [x * x for x in range(5) if x%2 == 0]
print(quadrados_dos_pares)

"""
Explanation: Combinando transformação e filtragem: Compressão de Listas
Uma maneira prática para combinar as operações de transformação e filtragem é utilizar uma compressão de lista. A anatomia da compressão de lista é a seguinte:
[f(x) for x in lista if p(x)]
End of explanation
"""


tupla = (1, 2, 3, 4)
print(tupla)

"""
Explanation: Tuplas
Tuplas são grupos ordenados de valores. Esse par é imutável e normalmente é utilizado para representar informações estruturadas (linha de um csv, tupla de um banco de dados, etc)
End of explanation
"""


dicionario = { 'nome': 'Fulano', 'sobrenome': 'Da Silva' }
print(dicionario['nome'])
print(dicionario['sobrenome'])

"""
Explanation: Dicionários
Dicionários são pares de chave : valor, utilizados para representar um dado cujos valores tem uma semântica atribuída a ela.
End of explanation
"""


print(dicionario.keys())
print(dicionario.values())

"""
Explanation: Em Python, podemos acessar, separadamente, suas chaves e seus valores
End of explanation
"""


