def dobra(x):
    return x * 2

dobra(10)

"""
Explanation: [Py-Intro] Aula 05
Funções e arquivos
O que você vai aprender nesta aula?


Funções

Definir nções
Funções como objetos
Argumentos padrão
Invocar funções pelos nomes dos argumentos (keyword arguments)
Empacotamento e desempacotamento de argumentos



Arquivos

Como ler e escrever arquivos
Trabalhando com arquivos CSV (Comma-separated values)



Funções
Definindo funções
Nas aulas anteriores já usamos e definimos muitas funções. Nesta aula revisaremos como essas coisas acontecem e aprofundaremos o assunto.
End of explanation
"""


dobra('do')

dobra([1, 2, 3])

"""
Explanation: Vale notar que o Python não faz checagem de tipos, então podemos usar nossa função dobra() com outros tipos de argumentos:
End of explanation
"""


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

soma(1, 2, 3, 4)

soma('h', 'o', 'j', 'e')

"""
Explanation: Uma função pode receber mais de um parâmetro:
End of explanation
"""


def fatorial(n):
    """ Retorna o fatorial de n (n!)"""
    return 1 if n < 1 else n * fatorial(n - 1)

fatorial(3), fatorial(4), fatorial(5)

"""
Explanation: A documentação de funções é feita utilizando docstring. Docstring são lorem ipsum dolor sit amet:
End of explanation
"""


fat = fatorial
fat(3), fat(4), fat(5)

fat

type(fat)

"""
Explanation: Funções como objetos
Funções em python podem ser tratadas como outros objetos (no jargão formal diz-se que funções são objetos de primeira classe)
End of explanation
"""


fat.__doc__

"""
Explanation: É possível acessar atributos desse objeto function:
End of explanation
"""


dir(fat)

"""
Explanation: Para conhecermos os atributos e métodos de um objeto function podemos usar a função dir() que retorna os métodos atributos de um objeto
End of explanation
"""


fat.__name__

fat.__doc__

"""
Explanation: Os métodos e atributos envoltos em __ são conhecidos como Métodos Mágicos ou Métodos Dunder (Double UNDERline) e serão vistos no minicurso de Orientação a Objetos
End of explanation
"""


fat.__code__

dir(fat.__code__)

fat.__code__.co_name

fat.__code__.co_varnames

"""
Explanation: É possível acessar o metadados e o bytecode dessas funções:
End of explanation
"""


fat.__code__.co_code

import dis
dis.dis(fat)

"""
Explanation: Bytecode:
End of explanation
"""


bromas = {'z': 10, 'n': 5, 'm': 7}
bromas

sorted(bromas.items())

def pega_segundo(sequencia):
    return sequencia[1]

sorted(bromas.items(), key=pega_segundo)

"""
Explanation: Como mostramos anteriormente é possível enviar funções como argumentos de outras funções. No caso usamos esse artifício para mudar o funcionamento padrão da função de ordenação sorted():
End of explanation
"""


def dolar_para_real(valor_real, dolar=3.53):
    return valor_real * dolar

"""
Explanation: Neste exemplo definimos a funçao pega_segundo e enviamos ela como argumento para a função sorted()
Valores padrões de argumentos (default arguments)
O Python permite a atribuição de valores padrão para argumentos de uma função. Ao chamar essa função esses argumentos são opcionais, sendo utilizado o valor padrão fornecido na definição da função.
Por exemplo vamos criar uma função que converte um valor em dólar para real com o preço do dólar como argumento com valor padrão:
End of explanation
"""


dolar_para_real(89)

"""
Explanation: Para calular um preço de um produto de, por exemplo, U$89,00 só precisamos passar esse valor:
End of explanation
"""


dolar_para_real(89, 2.8)

"""
Explanation: Supondo que queiramos calcular o preço do produto no ano passado quando o valor do dólar estava menor:
End of explanation
"""


help(str.split)

"""
Explanation: Muitas funções da biblioteca padrão usam argumentos padrão para simplificar e extender seus usos. Muita funções que vimos neste curso fazem isso, como,  por  exemplo a função str.split().
Para mostrar isso vamos recorrer a sua documentação que é invocada ao passar essa função como argumento para a função help():
End of explanation
"""


'Frase sem sentido algum para ser usada como exemplo'.split()

"""
Explanation: Como visto a função split possui dois argumentos com valores padrão: separador e número máximo de splits. Por padrão o separador é um espaço em branco e o número máximo de splits é todos os possíveis, como podemos observar neste exemplo:
End of explanation
"""


frase = 'Frase sem sentido algum para ser usada como exemplo'
frase.split(' ', 1)  # somente 1 split foi feito gerando uma lista de dois elementos

url = 'www.dominio.com.br'
url.split('.')

url.split('.', 1)  # para separar somente o www do resto

"""
Explanation: Podemos mudar esse comportamento passando outros argumentos:
End of explanation
"""


arq = open('arq.txt', 'w')  # passa nome do arquivo e modo abertura 'w' (escrita)
arq  # arquivo aberto

arq.close()  # fechando arquivo

arq = open('arq.txt')  # por padrão o modo de abertura é 'r' (leitura)
arq

arq.close()

"""
Explanation: Outra função que também faz isso é a função open() usada para abrir arquivos:
End of explanation
"""


def anexa(elemento, lista=[]):
    lista.append(elemento)
    return lista

anexa(1)

anexa(2)

anexa(3)

"""
Explanation: Veremos mais sobre esta função ainda nesta aula.
Cuidado com argumentos padrões!
Os argumentos padrões de funções são executados apenas uma vez e isso pode causar alguns comportamentos "estranhos".
Suponhamos que queremos criar uma função anexa() que adiciona um elemento a uma lista e, se a lista não for passada, criamos uma nova:
End of explanation
"""


def anexa(elemento, lista=None):
    if not lista:
        lista = []
    lista.append(elemento)
    return lista

"""
Explanation: Como dito anteriormente o valor do argumento da lista [] (que cria uma lista) é executado apenas uma vez, portanto a mesma lista é usada sempre que chamamos a função anexa(). Para criarmos uma anova lista quando não nos é passado uma fazemos:
End of explanation
"""


lista = anexa(10)
lista

anexa(5)

anexa(20, lista)
lista

"""
Explanation: Desse jeito criamos uma nova lista cada vez que a função é executada:
End of explanation
"""


anexa(elemento=100, lista=[1, 2, 3])

'Exemplo de split chamado pelo nome dos argumentos'.split(sep=' ', maxsplit=-1)

"""
Explanation: Como já vimos anteriormente (porém não foi explicado como) o Python permite que os argumentos da função sejam chamados por seu nome e não somente por sua posição:
End of explanation
"""


from datetime import date, timedelta
hoje = date.today()
hoje  # objeto do tipo date

hoje.year, hoje.month, hoje.day  # atributos de date: day, month e year

"""
Explanation: Exemplo de uso de argumentos nomeados: biblioteca datetime
Uma função da biblioteca padrão do Python que faz uso extensivo de argumentos padrões é a timedelta() da biblioteca datetime (que trabalha com datas e horários). Essa função é usada para representar durações, diferenças entre datas ou horários:
End of explanation
"""


hoje + timedelta(days=1)  # amanhã

hoje - timedelta(days=1) # ontem

hoje + timedelta(days=2)  # depois de amanhã

hoje - timedelta(days=2)  # antes de ontem

hoje + timedelta(days=7)  # semana que vem

hoje

hoje + timedelta(days=30)  # mês que vem

"""
Explanation: foo
End of explanation
"""


hoje.replace(month=hoje.month + 1)  # mesmo dia e ano no próximo mês

hoje.replace(year=hoje.year + 1)  # mesmo dia e mês no próximo ano

"""
Explanation: Como nem todo mês possui 30 dias pode ser necessário saber qual o próximo mês. Para isso é melhor usar a função datetime.replace() que retorna a mesma data com os valores fornecidos trocados:
End of explanation
"""


from datetime import datetime

agora = datetime.now()
agora

agora.year, agora.month, agora.day, agora.hour, agora.minute, agora.second, agora.microsecond

agora + timedelta(hours=1)  # daqui uma hora

agora - timedelta(hours=1)  # uma hora atrás

agora + timedelta(hours=2, minutes=30)  # daqui 2 horas e meia

"""
Explanation: timedelta() também pode ser usado com datetimes (data e hora):
End of explanation
"""


daqui_a_pouco = agora + timedelta(minutes=15, seconds=45)
daqui_a_pouco - agora

"""
Explanation: Subtrair dates e datetimes gera objetos timedelta que representam a diferença de tempo entre os dois:
End of explanation
"""


max(1, 2)  # funciona com 2 argumentos

max(1, 2, 3)  # 3 argumentos

max(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)  # muitos argumentos

"""
Explanation: Chamar funções dando nomes aos seus argumentos, em conjunto com bons nomes de funções e argumentos, é uma ótima forma de aumentar a legibilidade de seu código.
Exercícios



Empacotamento e desempacotamento de argumentos de funções
A criação de funções com argumentos arbitrários é feita usando o conceito de empacotamento de argumentos. Algumas funções da biblioteca padrão do Python usam esse conceito:
End of explanation
"""


sum(1, 2, 3, 4)  # não suporta

"""
Explanation: O que acontece é que esses vários argumentos são empacotados para uma tupla e então a partir dessa tupla de elementos é possível encontrar o máximo. A funcão sum() que soma os elementos de uma sequência não suporta argumentos abritrários, vamos fazer uma versão dessa função que suporte isso:
End of explanation
"""


def soma(*numeros):
    print('O tipo é: {}'.format(type(numeros)))
    print('Valores: {}'.format(numeros))

"""
Explanation: O empacotamento de argumentos é feito na definição dos argumentos da função usando o operador *, conforme é mostrado a seguir. Vamos começar analisando o resultado desse argumento para depois implementar a funcionalidade de soma:
End of explanation
"""


soma(-1, 0, 1)

"""
Explanation: Só para não haver dúvidas: *numeros não é um ponteiro. O que realmente acontece é que os valores recebidos ao invocar a função soma() serão empacotados para o argumento numeros.
End of explanation
"""


def soma(*numeros):
    soma = 0
    for num in numeros:
        soma += num
    return soma

soma(1, 2, 3, 4)

soma(-2, 0, 3)

"""
Explanation: Nesse exemplo a função soma() foi chamada com os valores -1, 0, 1 que foram empacotados na tupla numeros. Sabendo disso podemos calcular a soma desses elementos:
End of explanation
"""


def foo(bar, *baz):
    print(type(bar), type(baz))
    print('bar: {}, baz: {}'.format(bar, baz))

foo(1, 2, 3, 4, 5)

foo([1, 2, 3], 10, 'aba', False, (1, 2, 3))

"""
Explanation: É possível criar função que receba alguns argumentos fixos e argumentos arbitrários:
End of explanation
"""


numeros = -1, 0, 10
max(*numeros)

"""
Explanation: Do mesmo jeito que empacotamos argumentos recebidos na chamada de uma função podemos desempacotar argumentos para enviar as funções:
End of explanation
"""


def soma(a, b, c):
    """ Soma três números a, b, e c (a + b + c) """
    return a + b + c

"""
Explanation: No exemplo anterior desempacotamos a tupla com os valores -1, 0, 10 e, ao invés de enviar uma tupla, mandamos cada um deles como um argumento separado. Para deixar esse conceito claro criaremos uma função que recebe argumentos desempacotados:
End of explanation
"""


soma(1, 2, 3)

"""
Explanation: Normalmente faríamos:
End of explanation
"""


números = -1, 0, 1
soma(*números)

"""
Explanation: Porém podemos desempacotar uma sequência de 3 elementos e enviá-los para a função:
End of explanation
"""


números = -1, 0
soma(*números)

números = -1, 0, 1, 10
soma(*números)

"""
Explanation: Se a lista for maior ou menor uma exceção será levantada:
End of explanation
"""


def foo(a, b, c, *args):
    print('a: {} {}'.format(a, type(a)))
    print('b: {} {}'.format(b, type(b)))
    print('c: {} {}'.format(c, type(c)))
    print('*args: {} {}'.format(args, type(args)))

args = ['foobarbaz', False, 10]
foo(*args)

"""
Explanation: Para ficar mais claro ainda vamos criar uma função que deve receber, obrigatoriamente, três argumentos fixos e, opcionalmente, quantos mais valores forem enviados:
End of explanation
"""


min(10)  # levanta exceção

min(10, -10)  # correto

min(10, -10, 0)  # também correto

"""
Explanation: Ao receber argumentos empacotados, como feito na função foo(a, b, c, *args), permitimos o envio de uma quantidade arbitrária de argumentos. Isso inclui o envio de nenhum argumento, por esse motivos as funções embutidas min() e max() definem dois argumentos fixos e depois recebem mais argumentos empacotados:
End of explanation
"""


foo(1)

foo(1, 2)

foo(1, 2, 3)

foo(1, 2, 3, 4, 5, 6, 7, 8, 9)

"""
Explanation: O mesmo vale para nossa função foo():
End of explanation
"""


def foo(a, b, c):
    print('a: {} {}'.format(a, type(a)))
    print('b: {} {}'.format(b, type(b)))
    print('c: {} {}'.format(c, type(c)))

kwargs = {'a': 1.5, 'b': True, 'c': 'alo'}
foo(**kwargs)  # desempacotando dicionário kwargs para função foo()

"""
Explanation: Empacotamento e desempacotamento de argumentos são conceitos importantes, uma vez que são usados extensivamente em bibliotecas e frameworks Python. Funções embutidas como format(), max() e min() usam. Além disso, muitos métodos das Class-Based Views no framework web Django também usam.
Além de empacotar e desempacotar argumentos em/de sequências também é possível fazer isso para dicionários:
End of explanation
"""


kwargs = {'q': 10, 'x': 'foo', 'a': 123}
foo(**kwargs)

"""
Explanation: O que acontece por trás disso é: os argumentos com o nome das chaves do dicionário recebem o respectivo valor, portanto é necessário se atentar com as chaves e nomes do argumentos:
End of explanation
"""


def foo(a, b, c, **kwargs):  # kwargs = KeyWord Arguments (argumentos de palavra-chave)
    print('a: {} {}'.format(a, type(a)))
    print('b: {} {}'.format(b, type(b)))
    print('c: {} {}'.format(c, type(c)))
    print('kwargs: {} {}'.format(kwargs, type(kwargs)))

foo(1, 2, 3)

foo(1, 2, 3, nome='José', idade=100, vivo=True)

"""
Explanation: Assim como visto anteriormente também é possível, ao criar uma função, empacotar os argumentos em dicionários:
End of explanation
"""


'{0}'.format(1)

'{0} {1}'.format(1, 2)

"""
Explanation: A função str.format() recebe argumentos posicionais e de palavra-chave arbitrários que devem corresponder a quantidade de variáveis a ser substituidas na string de formatação:
End of explanation
"""


numeros = [1, 2, 3, 4, 5]
'{0} {1} {2} {3} {4}'.format(*numeros)

'{nome} é {sexo} e tem {idade} anos de idade.'.format(nome='Joana', sexo='mulher', idade=35)

"""
Explanation: Podemos desempacotar uma sequência e enviar à função de formatação:
End of explanation
"""


dados = {'nome': 'Joana', 'sexo': 'mulher', 'idade': 35}
'{nome} é {sexo} e tem {idade} anos de idade.'.format(**dados)

"""
Explanation: Podemos desempacotar um dicionários e enviar essas informações à função:
End of explanation
"""


def silverbullet(*args, **kwargs):
    print('args: {} {}'.format(args, type(args)))
    print('kwargs: {} {}'.format(kwargs, type(kwargs)))

silverbullet(1, 2, 3, 4, a=10, b=20, c=30)

foo = 1, 2, 3
bar = {'abc': False, 'def': 'alololo'}
silverbullet(-1, -10, *foo, a=150, b='oi', **bar)

"""
Explanation: Para criar uma função que receba argumentos arbitrários é preciso criar uma função que empacote tanto os argumentos posicionais (em uma tupla) quanto os nomeados (em um dicionários):
End of explanation
"""


arq = open('dados.txt', 'w')
arq

arq.write('você tem dado em casa?\n')
arq.write('não teve graça... eu sei.')
arq.close()

"""
Explanation: Arquivos
Já vimos anteriormente um exemplo do uso de arquivos, aqui nesta seção vamos trabalhar mais a fundo com eles.
Para abrir um arquivo existe a função embutida open() que recebe, além de outras coisas, o nome do arquivo e modo de abertura. Os modos suportados são:
<table>
<thead>
<th>Character</th>
<th>Meaning</th>
</thead>
<tbody>
<tr>
<td>'r'</td><td>abrir par leitura (padrão)</td>
</tr>
<tr>
<td>'w'</td><td>abrir para escrita, o arquivo é truncado primeiro</td>
</tr>
<tr>
<td>'x'</td><td>abrir para criação exclusiva, falhando se o arquivo existe</td>
</tr>
<tr>
<td>'a'</td><td>abrir para escrita, anexando o conteúdo para o fim do arquivo caso ele exista</td>
</tr>
<tr>
<td>'b'</td><td>modo binário (pode ser usado em  conjunto com os de abertura)</td>
</tr>
<tr>
<td>'t'</td><td>modo  texto (padrão)</td>
</tr>
<tr>
<td>'+'</td><td>abrir um arquivo do disco para atualização (funciona para escrita e leitura)</td>
</tr>
<tr>
<td>'U'</td><td>modo quebra de linhas universal (depreciado)</td>
</tr>
</tbody>
</table>

Vamos começar abrindo um arquivo de texto para escrita:
End of explanation
"""


arq.write('esqueci de escrever esta frase')

"""
Explanation: Agora abra o arquivo dados.txt e veja seu conteúdo.
Note que após fechar o arquivo não podemos fazer operações nele:
End of explanation
"""


arq = open('dados.txt')

conteudo = arq.read()
conteudo

"""
Explanation: Agora vamos usar o próprio python para ler o arquivo:
End of explanation
"""


arq.close()  # não podemos esquecer de fechar o arquivo

"""
Explanation: Como visto a função file.read() nos dá o conteúdo de todo o arquivo como uma única string. Mais para frente veremos outros métodos de leitura e escrita.
End of explanation
"""


from faker import Factory
faker = Factory.create('pt_BR')  # cria fábrica de dados falsos em pt-BR

arq = open('dados.txt', 'w')
for _ in range(100):
    nome = faker.name()
    cargo = faker.job()
    empresa = faker.company()
    arq.write('"{}","{}","{}"\n'.format(nome, cargo, empresa))
    
    
arq.close()

"""
Explanation: Agora vamos gerar um arquivo mais complexo com dados mais úteis usando a famigerada biblioteca faker:
End of explanation
"""


arq = open('dados.txt')  # abrimos o arquivo no modo padrão (escrita)
linhas = arq.readlines()  # o arquivo é todo lido e cada linha vira um elmento na lista linhas
print(type(linhas))
print(linhas)
arq.close()

"""
Explanation: Abra o arquivo dados.txt e dentro dele haverá 100 linhas. Cada linha contém os dados (nome, cargo e empresa) separados por vírgula.
Esse formato de dados é muito popular e é chamado de CSV (Comma-separated Values ou Valores Separados por Vírgula), muitas empresas usam o CSV para mover dados entre sistemas que trabalham com formatos incompatíveis ou proprietários. Ainda nesta aula veremos mais sobre como trabalhar com esses dados.
Existem algumas maneiras diferentes para ler arquivo. Podemos ler todas as linhas de uma só vez e jogá-la em uma lista (ou iterá-la diretamente) usando file.readlines():
End of explanation
"""


for linha in linhas[:10]:  # pegando só as 10 primeiras por brevidade
    print(linha.strip())  # .strip() remove a quebra de linha no final

"""
Explanation: Podemos iterar a lista e trabalhar com os dados vindo do arquivo:
End of explanation
"""


with open('dados.txt') as arq:  # abre o arquivo numeros.txt e o coloca na variável arq
    for linha in arq.readlines()[-10:]:  # pega as 10 últimas linhas por brevidade
        print(linha.strip())

"""
Explanation: Como vocês viram anteriormente abrimos o arquivo, mexemos com ele e depois o fechamos. Trabalhar dessa forma geralmente pode levar a erros, pois é comum esquecer de fechar o arquivo e, ter vários arquivos abertos, pode deixar o programa lento/ineficiente. (eu mesmo esqueci de fechar os arquivos nos dois exemplos acima)
Uma maneira melhor de manipular arquivos é usando gerenciadores de contexto se responsabilizam pelo fechamento ou finalização de recursos utilizados e isso pode ser usado para trabalhar com arquivos ou cuidar de transações ao trabalhar com banco de dados, por exemplo.
Vamos mostrar como trabalhar com gerenciadores de contexto lendo o arquivo que criamos anteriormente:
End of explanation
"""


arq.readline()

"""
Explanation: O arquivo arq já foi fechado, podemos verificar isso tentando ler uma linha do arquivo:
End of explanation
"""


with open('dados.txt') as arq:
    linha = arq.readline()
    while linha:
        print(linha, end='')
        linha = arq.readline()

"""
Explanation: Tambem é possível ler o arquivo linha a linha usando a função file.readline():
End of explanation
"""


import csv

with open("dados.txt") as arq_csv:  # abrindo o arquivo
    leitor = csv.reader(arq_csv)
    for linha in leitor:
        print(type(linha), linha)

"""
Explanation: Trabalhando com arquivos CSV
Não existe nenhum padrão de arquivos CSV. É comum ver arquivos desses formatos separados por outros caracteres que não a vírgula como: ; - e .. Em alguns casos cada coluna pode ser envolta em aspas simples ou aspas duplas. Por conta dessas particularidades o Python criou uma biblioteca csv que auxilia na manipulação de arquivos CSV.
Para ler arquivos CSV precisamos primeiro importar a biblioteca e depois criamos um leitor CSV com a função csv.reader():
End of explanation
"""


with open('dados.txt') as arq_csv:
    leitor = csv.reader(arq_csv)
    for nome, cargo, empresa in leitor:
        print('{} trabalha como {} na {}'.format(nome, cargo, empresa))

"""
Explanation: A função csv.reader() já retorna cada linha como uma lista Python com as aspas e quebra de linhas removidas.
Podemos deixar a leitura do arquivo ainda melhor usando desempacotamento de sequências:
End of explanation
"""


with open('mais-dados.csv', 'w') as arq_csv:
    escritor = csv.writer(arq_csv, delimiter=' ', quotechar='|')
    for _ in range(20):
        dados = faker.name(), faker.job(), faker.company()
        escritor.writerow(dados)

"""
Explanation: Agora vamos criar um arquivo CSV em um padrão diferente usando a função csv.writer(). Nossas colunas serão separadas por espaço em branco e cada coluna será separada por | ao invés de aspas:
End of explanation
"""


with open('mais-dados.csv') as arq_csv:
    for linha in csv.reader(arq_csv, delimiter=' ', quotechar='|'):
        print(linha)

"""
Explanation: Para ler o arquivo é só usar a mesma função csv.reader() usada anteriormente especificando o padrão:
End of explanation
"""


