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

# + [markdown] slideshow={"slide_type": "slide"}
# # <font color = "blue" style="background-color: #E9F7E1;"> Funções e Arquivos</font>
#
# <ul><li><b>Funções</b></li>
#     <li><b><a href="#arquivos"  title="persistência de dados em memória secundária">Arquivos</a></b></li>
#     <li><b><a href="#pickle"   title="conservação de objetos em memória secundária">Pickle</a></b></li>
#     <li><b><a href="#anexo">Anexo - Biblioteca Padrão do Python</a></b></li></ul>

# + [markdown] slideshow={"slide_type": "slide"}
# ## 1. Funções
#
# ### 1.1 Chamada de uma Função
# No contexto da programação, função é um conjunto de instruções que realiza uma computação. Quando você define uma função, você especifica o nome, parâmetro(s) e o conjunto de instruções. Posteriormente, pode-se "chamar" a função pelo seu nome e o argumento(s). A expressão em parênteses é chamada de argumento(s) da função na sua chamada, e de parâmetro(s) da função na sua declaração (definição). O argumento é um valor ou variável que passamos à função como entrada.   

# + [markdown] slideshow={"slide_type": "slide"}
# ### 1.2 Funções Nativas (incorporadas, *built-in*)
# O Python fornece várias funções internas (nativas, embarcadas) que são usadas sem que seja preciso definí-las. Os criadores do Python (<NAME> e comunidade) escreveram várias funções para solucionar problemas comuns.
# A funções `max()` e `min()` nos dão o maior e o menor valores de uma sequência, respectivamente:
# -

valores = (33,2,45,3,1,46,12)
print(valores)
print(max(valores), min(valores), len(valores))

# ### Relação das [Funções Nativas](https://docs.python.org/3/library/functions.html)
# A - **abs()**, aiter(), all(), any(), anext(), **ascii()** <br>
# B - **bin()**, **bool()**, **breakpoint()**, bytearray(), bytes().  
# C - callable(), **chr()**, classmethod(), compile(), **complex()**.   <br>
# D - delattr(), **dict()**, **dir()**, **divmod()**.  <br>
# E - **enumerate()**, **eval()**, exec().  
# F - filter(), **float()**, format(), frozenset().  
# G - getattr(), globals().  
# H - hasattr(), hash(), **help()**, **hex()**.   <br>
# I - id(), **input()**, **int()**, isinstance(), issubclass(), iter().  
# L - **len()**, **list()**, locals().  
# M - map(), **max()**, memoryview(), **min()**.   <br>
# N - next().  
# O - object(), **oct()**, **open()**, **ord()**.<br>
# P - **pow()**, **print()**, property().   
# R - **range()**, repr(), reversed(), round().  
# S - set(), setattr(), slice(), **sorted()**, staticmethod(), **str()**, **sum()**, **super()**.  <br>
# T - **tuple()**, **type()**.  <br>
# V - vars().  
# Z - zip().  
# _ - \_\_import\_\_()
#
# *Obs*.: Em negrito, as funções mais usuais.

# ### 1.3 Funções de Conversão de Tipo
# O Python também fornece funções internas que convertem valores de um tipo de dados em outro. A função `int()` recebe qualquer valor e o converte em um inteiro, se for possível. Outras funções de conversão de tipo:
#
# |Função|Conversão|
# |:---: |:---|
# |`int()`|para número inteiro|
# |`float()`|para número real|
# |`complex()`|para número complexo|
# |`str()`|para string|
# |`bool()`|para booleano|

x = input("Digite algo:")   # sempre retorna uma 'string'
y = complex(x)
print(y+20, type(y))

print(bool(0), bool(40), complex(-1))
print(int('1292'))
print(int('2343a'))

# ### 1.4 Funções Matemáticas
# As funções matemáticas do Python estão disponíveis num módulo (biblioteca) chamado `math`, e por isso, antes de usar suas funções matemáticas temos que importá-lo:  
#
# `import math`  
#
# Essa declaração cria um objeto do módulo chamado `math`. Se usarmos `print(math)` veremos algumas informações sobre ele:

import math
print(math,"\nNúmero de Neper:", math.e)
fm = dir(math)
for funcao in fm:
    print(funcao,end=', ')

# O objeto do módulo contém as funções e as variáveis definidas no módulo. Para acessar uma dessas funções, você deve especificar o nome do módulo e o nome da função, separados por um ponto. Esse formato é conhecido como **notação de ponto**. O valor da constante pi ($\pi$), com 15 dígitos de precisão, pode ser obtida por `math.pi`.  
# Exemplo:

potencia_do_sinal, potencia_do_ruido = 1000, 0.001   # 1 kW, 1 mW
razao_abs = potencia_do_sinal / potencia_do_ruido
razao_db  = 10 * math.log10(razao_abs)
theta     = 0.7                                      # em radianos
modulo    = math.sqrt(math.sin(theta)**2 + math.cos(theta)**2)
print(f"Pot_sinal/Pot_ruido: {razao_abs} vezes, ou {razao_db:+.1f} db\nMódulo: {modulo}")

# ### 1.5 Geração de Números Aleatórios
# Os algoritmos disponíveis não são capazes de gerar números aleatórios mas apenas números pseudoaleatórios, e são assim denominados por serem gerados por uma computação determinística.
#
# O módulo `random` oferece funções que geram números pseudoaleatórios, tais como a função `random()` que retorna um número real (ponto flutuante, *`float`*) entre 0.0 e 1.0 (incluindo o limite inferior, 0.0, mas não
# o limite superior, 1.0). Todas as vezes que chamarmos essa função, teremos o próximo número de uma longa série de números pseudoaleatórios.  
#
# Exemplo:

# +
# import random
# import random as rnd
from random import random

for i in range(10):
    x = random()           # random.random()      # rnd.random()
    print(x, end=', ')
# -

# A função `random()` é apenas uma das funções do módulo **random** que geram números aleatórios. Outra 
# função usual é a `randint()` que recebe dois parâmetros:  `'menor'` e `'maior'` e retorna um inteiro entre `'menor'` e `'maior'`, incluindo os limites.

# Aposta na Mega-Sena
from random import randint
aposta = []
dez = 0
while dez < 6:
    num = randint(1, 60)
    if num not in aposta:
        aposta.append(num)
        dez += 1
    # print(f'{randint(1, 60)}', end=' ')     # podem aparecer valores repetidos
print(aposta)

# Outra função do módulo `random` muito útil é a `choice()`, que escolhe aleatoriamente um elemento de uma sequência fornecida como argumento:

# +
from random import choice

t = [10, 20, 30, 40, 50, 60]
for esc in range(1,11):
    print(f'Escolha {esc:2d}: {choice(t)}')


# -

# O módulo `random` também disponibiliza funções para gerar valores aleatórios de acordo com funções de distribuição contínuas: gaussiana, exponencial, gamma e outras.

# + [markdown] slideshow={"slide_type": "slide"}
# ### 1.6 Funções do Usuário
# Até agora, usamos apenas as funções do Python, mas também é possível adicionar novas funções. A definição de uma função pelo usuário começa pela especificação do nome da função, parâmetro(s) (se existir(em)) e o conjunto de comandos que serão executados quando a função for chamada.  
#
# Sintaxe:  
# ``` python
# def nome_funcao([parametro1[, parametro2, ...[, parametroN]]]):
#     comando (ou bloco de comandos)
# ```
# Uma vez definida, a função pode ser utilizada repetidas vezes ao longo do programa.  
#
# **Exemplo**:
# -

def refrao():
     print('Fica sempre um pouco de perfume\n' \
           'nas mãos que oferecem rosas\n'     \
           'nas mãos que sabem ser generosas')


# A palavra-chave **`def`** indica a definição de uma função de usuário. O nome da função é `refrao`. As regras para nomes de funções são as mesmas usadas para nominar variáveis. Não se pode usar uma palavra-chave como nome de função, e deve-se evitar também uma variável e uma função com mesmos nomes.  
#
# Os parênteses vazios após o nome da função indicam que essa função não usará argumentos ao ser chamada.
# A primeira linha da definição da função é chamada de **cabeçalho** da função, e o restante é o **corpo** da função. O cabeçalho é finalizado sempre com um caracter de dois pontos, `:`, e o corpo tem que estar
# indentado. Por convenção, a indentação é de quatro espaços em branco. 
#
# Todos os elementos da linguagem Python são objetos, inclusives as funções de usuários.

print(refrao, type(refrao))
refrao()


# ### 1.7 Fluxo de Execução
# Para garantir que a função seja definida antes de sua primeiro chamada, é preciso saber a ordem em que cada declaração é executada -- fluxo de execução.  
#
# A execução sempre começa na primeira declaração do programa. Declarações são executadas uma por vez, na ordem de cima para baixo. Definições de função não alteram o fluxo da execução do programa, e lembre-se de que comandos dentro da função não são executados até que a função seja chamada. 
#
# Uma chamada de função provoca um desvio no fluxo da execução. Em vez de ir para a próxima declaração, o fluxo de execução é desviado para o corpo da função, e depois de ter executado todos os comandos da função, o fluxo de execução volta para o ponto de onde havia sido desviado.

# ### 1.8 Parâmetros e Argumentos
# Algumas das funções internas já usadas necessitavam de argumentos. Por exemplo, a função `math.sin()` precisa de um número como argumento, o ângulo (em radianos). Algumas funções requerem mais de um argumento: `math.pow()` recebe dois argumentos, a base e o expoente.  
#
# Exemplo:

# +
def mostra_duas_vezes(param):
    print(param)
    print(param)

# Observe que a função funciona com qq. tipo de dado passado como argumento
mostra_duas_vezes(5)
mostra_duas_vezes('Olá! ')
# -

# ### 1.9 Funções e Subrotinas
# Alguns subprogramas, como as funções matemáticas, retornam resultados ao chamador da `função`; outros subprogramas, como `mostra_duas_vezes()`, executam uma ação mas não retornam um valor ao chamador. Elas são chamadas de `subrotinas` ou `funções vazias`.  
#
# Quando se chama um subprograma do tipo `função`, espera-se um valor de retorno; por exemplo, podemos atribuir o retorno da função a uma variável ou usá-lo como parte de uma expressão:

x = math.cos(theta)
y = (math.sqrt(5) + 1) / 2
print(f'x={x} y={y}')


# Para retornar um resultado de uma função ao chamador, usa-se o comando **`return`** na função criada pelo usuário. Por exemplo, vamos criar uma função para calcular e retornar a soma de dois números.

# +
def soma(x, y):                  # função de soma
    return x + y

print('33 + 54 =', soma(33, 54))


# -

# Os parâmetros podem ter valores padrões, caso o usuário não queira especificar argumentos usuais nas chamadas às funções.  
# Exemplo:

# +
def familia(nome, sobrenome = "<NAME>"):     # subrotina
    print(f'{nome} {sobrenome}')

familia("João")
familia("Tobias","Oliveira")
familia("Maria")

# -

# ### 1.10 Número Variável de Argumentos (Python 3)
# Na definição de uma função, os **parâmetros** são entidades nomeadas que especificam os **argumentos** que uma determinada função pode aceitar.
#
# Ao programar, você pode não estar ciente de todos os casos de uso possíveis de seu código e pode querer oferecer mais opções para futuros programadores que usem o seu módulo ou para usuários que interagem com o código. Podemos passar um **número variável de argumentos** ao chamar uma função, usando **\*args** e **\*\*kwargs** em nosso código.
#
# Vejamos uma típica função Python que usa dois argumentos:

# +
def multiplicacao(x, y):       # dois argumentos nominados: x, y
    print (x * y)

multiplicacao(6, 9)
# -

# E se, mais tarde, precisarmos multiplicar três números em vez de apenas dois? Se tentarmos adicionar um novo número à função, conforme mostrado abaixo, receberemos uma mensagem de erro.

multiplicacao(6, 9, 3)


# ### 1.10.1 Usando \*args
# Portanto, prevendo a possibilidade de se usar mais argumentos posteriormente, podemos usar **\*args** como parâmetro da função `multiplicacao()`, da seguinte forma:

# +
def multiplicacao(*args):        # lista de argumentos não nominados
    resultado = 1
    for argumento in args:
        resultado *= argumento
    print(resultado)

multiplicacao(6, 9, 3)
multiplicacao(6, 9, 3, 2, 0.5)


# -

# Como usamos **\*args** para enviar uma lista de argumentos de comprimento variável para função `multiplicacao()`, fomos capazes de passar tantos argumentos quantos desejamos nas chamadas à função, ou seja, foi possível a criação de um código mais flexível que aceita uma quantidade variada de argumentos não nominados na função em questão.

# ### 1.10.2 Usando \*\*kwargs
# A forma que usa asterisco duplo, **\*\*kwargs** é usada para passar um dicionário de argumentos de comprimento variável nominados para uma função. Os dois asteriscos (`**`) são os elementos importantes na sintaxe, visto que a palavra **kwargs** é usada convencionalmente, embora não seja imposta pela linguagem.
#
# Assim como **\*args**, **\*\*kwargs** pode aceitar quantos argumentos você quiser. No entanto, **\*\*kwargs** difere de **\*args** porque você precisa atribuir os valores dos argumentos aos respectivos nomes dos parâmetros.
#
# Vamos usar uma função curta para mostrar os argumentos **\*\*kwargs** passados à função:

# +
def mostra_argumentos(**kwargs):    # dicionário de argumentos nominados
    print(kwargs)
    
mostra_argumentos(arg1="Maria", arg2=242.7, arg3=False)


# -

# **Obs**.: Dependendo da versão do Python 3 que você está usando, o tipo de dados dicionário pode estar não ordenado. No Python 3.6 e superior, você receberá os pares de **valores-chave** em ordem, mas nas versões anteriores, os pares serão exibidos em uma ordem aleatória.

# Vamos criar outra função curta para mostrar como usar **\*\*kwargs**, uma função para saudar pessoas cujos nomes (argumentos) são passados como dicionário de nomes:

# +
def mostra_nomes(**kwargs):
    for chave, valor in kwargs.items():
        print("O valor de '{}' é {}".format(chave, valor))

mostra_nomes(nome1="João", nome2="Maria")
# -

# Vamos agora passar argumentos adicionais à função para mostrar que **\*\*kwargs** aceita quantos argumentos você quiser incluir:

mostra_nomes(nome1="João", 
             nome2="Maria",
             nome3="José",
             nome4="Davi",
             nome5="Matheus")


# Usar **\*\*kwargs** nos fornece flexibilidade para usar uma quantidade variável de argumentos nominados em nosso programa, ou seja, não é preciso saber quantos argumentos serão passados à uma função, durante a sua declaração.  
#
# ### 1.10.3 Argumentos Ordenados
# Ao indicar os argumentos na declaração ou chamada de uma função, é preciso que eles apareçam em uma ordem específica:
#
# <ol><li>Argumentos posicionais formais.</li>
#     <li><strong>*args</strong>.</li>
#     <li>Argumentos nominados.</li>
#     <li><strong>**kwargs.</strong></li></ol>
#     
# Vejamos um exemplo de função com parâmetros posicionais explícitos junto com **\*args** e **\*\*kwargs**:
# ```python
# def exemplo1(arg1, arg2, *args, **kwargs):
# ...
# ```
# Usando parâmetros posicionais com parâmetros nomeados, além de **\*args** e **\*\*kwargs**, teríamos:
# ```python
# def exemplo2(arg1, arg2, *args, kw1="Maria", kw2=33.21, **kwargs):
# ...  
# ```  
# É importante manter a ordem dos argumentos em mente ao criar funções para evitar erros de sintaxe em seu código Python.
#
# ### 1.10.4 Usando **\*args** e **\*\*kwargs** nas Chamadas de Funções
# Vamos ver um exemplo usando  **\*args**:

# +
def exemplo3(arg1, arg2, arg3):
    print("arg1: ", arg1, ", arg2: ", arg2, ", arg3: ", arg3, sep='')

argumentos = ("Maria", "João", 33.21)       # tupla de argumentos a serem usados na função 'exemplo3'
exemplo3(*argumentos)
outros = [3.4, True, 'Olá!']
exemplo3(*outros)
# -

# Da mesma forma, os argumentos **\*\*kwargs**  podem ser usados para chamar uma função. Vamos configurar uma variável igual a um dicionário com 3 pares de valores-chave (usaremos **kwargs** aqui, mas pode ser chamado como você quiser) e passá-lo para uma função com três argumentos:

kwargs = {"arg1": "Maria", "arg2": "José", "arg3": "Pedro"}
exemplo3(**kwargs)

# **Concluindo**: Podemos usar a sintaxe especial de **\*args** e **\*\*kwargs** na definição de uma função para passar um número variável de argumentos à função. 
# A criação de funções que aceitam **\*args** e **\*\*kwargs** é melhor usada em situações em que você espera que o número de entradas na lista de argumentos permaneça relativamente pequeno.  
# Fonte: https://www.digitalocean.com/community/tutorials/how-to-use-args-and-kwargs-in-python-3 

# + [markdown] slideshow={"slide_type": "slide"}
# ### Exercício
# 1. (Questão de concurso) Preencha as lacunas no código abaixo (L1 até L10), de forma a obter um programa que recebe uma string contendo exclusivamente palavras separadas por espaços em branco e determina a palavra mais frequente e sua frequência relativa de ocorrência de texto.
# Por exemplo, para "figurinha no pacote de figurinha" a saída do programa deve ser:
# palavra: figurinha - freq. rel.: 0.4
#
# ``` python
# def separa (texto):
#     L = L1
#     p = ''
#     for c in texto:
#         if c == ' ':
#             L.append(p)
#             p = ''
#         else:
#             p += c
#     L2
#     return L
#
# def maxfreq (texto):
#     L = L3
#     pf = []
#     f = L4
#     for p in L:
#         L5
#             pf.append(p)
#             f.append(1)
#         L6
#             for i in range(len(pf)):
#                 if p == pf[i]:
#                     L7
#     maxpf = ''
#     maxf = 0
#     for i in range(len(pf)):
#         if f[i] > maxf:
#             L8
#             L9
#     return L10
#
# def main ():
#     texto = input('Digite as palavras: ')
#     p, f = maxfreq(texto)
#     print ('palavra:', p, 'freq. rel.:', f)
#
# main()
# ```
#
# Para cada um dos itens a seguir, correspondendo às lacunas no código acima, assinale a única resposta que torna o programa acima correto.
#
# L1: A. [texto] B. ′′ C. texto D. 0 E. []  
# L2: A. L+=1 B. L+=c C. L=[L,p] D. L=[L] E. L.append(p)  
# L3: A. [separa(texto)] B. texto[i] C. texto  D. separa(texto) E. []  
# L4: A. ′′  B. [] C. 0  D. [0]  E. L  
# L5: A. if p[i] == pf[i]:   B. while p in pf:  C. if p != f:  D. if p != pf:  E. if not p in pf:  
# L6: A. else:  B. if f == 0:  C. elif p not in pf:   D. if not f in pf:  E. if pf[i] == f[i]  
# L7: A. f[i] += 1  B. f.append(f[i]+1)  C. f[i] = 2   D. pf[i] = f[i]   E. pf.append(f)  
# L8: A. maxf = f[i]   B. maxf = 0   C. f[len(f)-1]   D. maxf = len(pf[i])  E. maxf = i  
# L9: A. maxpf = separa(pf[i])[1]  B. maxpf = str(maxf)  C. maxpf = pf[i]   D. maxpf = ′′
#  E. maxpf = separa(texto)[i]  
# L10: A. maxf, len(L)   B. maxpf, maxf  C. pf, f  D. maxf/len(pf), maxpf  E. maxpf, maxf/len(L)  
#
# Gabarito: https://estudar.com.vc/conceitos/exercicios-de-prova-strings/exercicio-3-programa-com-strings-prova
# -

# ## <a id="arquivos">2. Arquivos</a>
#
# A partir deste instante começamos a trabalhar com a **Memória Secundária** (ou arquivos) para persistir as informações processadas.  
#
# Vamos primeiramente ver a leitura e a escrita de arquivos de texto, como aqueles criados por meio de um editor de texto, por exemplo, **notepad**.
#
# ### 2.1 Abrindo um Arquivo
# Quando queremos ler ou escrever um arquivo, primeiro temos que abrí-lo para que o sistema operacional providencie o acesso aos dados do arquivo armazenado no dispositivo de memória secundária. No exemplo seguinte, vamos abrir o arquivo chamado 'caixa.txt' com a opção de anexar novas informações textuais ao arquivo, ou seja, o modo de abertura será 'at', onde 'a' é para anexação de dados e 't' indica dados do tipo textual.
#
# A função `open()` e demais funções relacionadas fazem parte da <a href="#anexo">biblioteca padrão do Python</a> (vem embutida na distribuição do Python, e é bem ampla, oferecendo uma variedade de recursos). 
#
# Sempre que seu código abrir um arquivo para leitura ou gravação, você deve tomar cuidado para que o arquivo seja fechado ao terminar de usá-lo, para que seus dados não fiquem vulneráveis a modificações. Python fornece um gerenciador de contexto `open`, que é projetado para garantir que um arquivo seja fechado mesmo no caso de seu código gerar um erro.
#
# Ajuda mostrada pelo comando: ` >>> help(open)`
# >**`open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)`**  
#    Abre o arquivo e retorna um descritor de fluxo (*stream*). Lança um OSError em caso de falha.  
#
# > **'file'** ou é um texto ou uma string de bytes que fornece o nome (e o caminho se o arquivo não estiver no diretório de trabalho atual) do arquivo a ser aberto ou um descritor de arquivo do tipo inteiro. (Se um descritor de arquivo é fornecido, ele é fechado quando o objeto de E/S retornado é fechado, a menos que **'closefd'** seja definido como *False*).  
#
# > **'mode'** é uma string opcional que especifica o modo em que o arquivo é aberto. O padrão é 'r', o que significa aberto para leitura em modo texto. Outros valores comuns são 'w' para escrita (truncar o arquivo se já existe), 'x' para criar e gravar em um novo arquivo, e 'a' para anexar (que em alguns sistemas Unix, significa que todas as gravações serão anexadas ao final do arquivo, independentemente da posição de busca atual). No modo de texto, se a codificação não for especificada, a codificação usada é dependente da plataforma: locale.getpreferredencoding (False) é chamado para obter a codificação local atual. (Para ler e escrever bytes brutos use o modo binário e não especifique a codificação). Os modos disponíveis são:
#
# Especificar `mode = 'w'` indica que iremos gravar no arquivo -- se o arquivo já tiver algum conteúdo, esse conteúdo será apagado antes de ser gravado. A seguir estão os "modos" disponíveis de abertura de um arquivo:
#
# |Caracter | Significado |
# | :-----: | :---------- |
# |    'r'  | aberto para leitura (padrão) |
# |    'w'  | abre para escrita, truncando o arquivo caso já exista |
# |    'x'  | cria um novo arquivo e o abre para escrita |
# |    'a'  | aberto para escrita, acrescentado ao final do arquivo, se existir |
# |    'b'  | modo binário |
# |    't'  | modo de texto (padrão) |
# |    '+'  | abre um arquivo em disco para atualização (leitura e gravação) |
#
#
# O modo padrão é 'rt' (aberto para leitura de texto). Para acesso binário aleatório, o modo 'w+b' abre e trunca o arquivo para 0 bytes, enquanto 'r+b' abre o arquivo sem truncamento. O modo 'x' implica 'w' e gera um `FileExistsError` se o arquivo já existe.
#
# Por padrão, os arquivos do tipo **texto** utilizam a especificação de decodificação/codificação unicode (utf-8). Ou seja, quando você lê dados de arquivos com `mode = 'r'`, o Python decodificará automaticamente os dados binários que foram armazenados de acordo com o código **utf-8**, que converte os dados binários em texto escrito armazenado como uma string. Da mesma forma, gravar uma string num arquivo nos modos `'w', 'a', 'x'` ou `'+'` presumirá que a string será codificada em uma representação binária (que é necessária para ser armazenada num arquivo) de acordo com o esquema de codificação **utf-8**.
#
# Podemos forçar o Python a ler e escrever estritamente em termos de dados binários, adicionando um `'b'` aos modos: `'rb', 'wb', 'ab', 'xb', '+b'`. Se você estiver salvando uma matriz NumPy, vc deve abrir um arquivo nos modos `'wb'` ou `'xb'` para gravar dados binários; obviamente, não estamos salvando texto quando salvamos uma matriz NumPy de números.
#
# #### Criando um Arquivo Texto

# +
# Demonstração do uso do gerenciador de contexto `open`
# criando um objeto caminho (path) apontando para o diretório corrente
from pathlib import Path

# vamos escrever no arquivo chamado 'arq1.txt', localizado no diretório atual
caminho_do_arq = Path ('.\\arq1.txt')
with open(caminho_do_arq, mode="w") as arq:
    # O espaço recuado entra no 'contexto' do arquivo aberto.
    # Saindo do espaço indentado existe o contexto do arquivo aberto, forçando
    # o arquivo a ser fechado. Isso é garantido mesmo se nosso código causar um erro
    # dentro deste bloco recuado
    arq.write('Esta é uma linha.\nEsta é uma segunda linha.\nEsta é a terceira linha.')
# O arquivo é fechado automaticamente daqui pra frente.
# -

# #### Informações Sobre o Arquivo

for narq in ['arq1','mbox']:
    arq = open(narq+'.txt','at')   # open retorna um handler (alça de manipulação do arquivo)
    print(arq)
    print("Nome do arquivo  : ", arq.name)
    print("Fechado          : ", arq.closed)
    print("Modo de abertura : ", arq.mode, "\n")
    arq.close()

# ### 2.2 Arquivos Textuais
# Um arquivo de texto pode ser pensado como uma sequência de linhas, assim como uma string em Python pode ser pensada como uma sequência de caracteres. Por exemplo, essa é uma amostra de um arquivo de texto que grava a atividade de *emails* de vários inivíduos em um projeto *open source* em desenvolvimento...
#
# > From <EMAIL> Sat Jan 5 09:14:16 2008
# Return-Path: <<EMAIL>>
# Date: Sat, 5 Jan 2008 09:12:18 -0500
# To: <EMAIL>
# From: <EMAIL>
# Subject: [sakai] svn commit: r39772 - content/branches/
# Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772
# ...
#
# Todo o arquivo de interações de email está disponível em www.py4e.com/code3/mbox.txt e uma versão reduzida do arquivo está disponível em www.py4e.com/code3/mbox-short.txt.   
#
# Eles estão em um formato padrão para um arquivo com várias mensagens de email. As linhas que começam com **"From"** separam as mensagens, e as linhas que começam com "From:" fazem parte delas. Para mais informações sobre o
# formato **mbox**, consulte https://en.wikipedia.org/wiki/Mbox.  
#
# Para quebrar o arquivo em linhas, tem um caracter especial que representa o "fim da linha", comumente chamado de *newLine*.  
# Em Python, representamos esse caracter pela constante string `'\n'`. Mesmo que pareçam ser dois caracteres, é na verdade um só. Quando olhamos para a variável inserindo "algo" no interpretador, ele nos mostra o '\n' na string, mas quando usamos `print()` para mostrá-la, vemos a string quebrada em duas linhas pelo uso do caracter *newline*. Este caractere divide o conteúdo do arquivo em linhas.

algo = 'Olá\nmundo!'
algo

print(algo)
algo = '1\n2'
print(algo)
print(len(algo))

# ### 2.3 Lendo Arquivos Textuais
# Depois de aberto o arquivo com a função `open()`, temos uma alça para manipular o conteúdo do arquivo em questão. Para ler o conteúdo do arquivo, podemos fazê-lo por caracter (um tanto quanto demorado) ou por linha (mais ágil). O exemplo seguinte mostra a leitura por linha:

# ajuste o caminho do arquivo baixado, na variável 'caminho'
caminho = 'dat/'
arq = open(caminho+'mbox-short.txt')     # open() retorna uma alça (handler) para manipulação de dados do/pro arquivo
linhas = 0
for linha in arq:                        # a iteração de linhas provoca a leitura dos dados no arquivo
    linhas += 1
print('Qtde de linhas no arquivo de emails:', linhas)
arq.close()

# O método **`close()`** de um objeto arquivo libera todas as informações não gravadas e fecha o objeto arquivo, após o que nenhuma outra gravação pode ser feita.  
#
# O fechamento dos arquivos pode se dar de forma automática se usarmos `try/finallly`:
# 1. ``` python
# arq = open('entrada.txt')
# try:
#      # processamento do arquivo 
# finally:
#      arq.close()
# ```
# 2. ``` python
# with open('entrada.txt') as arq:
#     # processamento do arquivo
# ```
#
# A instrução `with` cuida automaticamente do fechamento do arquivo quando o bloco `with` é encerrado, e mesmo em casos de erro. Recomenda-se o uso da declaração `with`, tanto quanto possível, pois ela permite um código mais limpo e facilita a manipulação de erros inesperados.
#
# Para mais detalhes sobre manipulação de arquivos, visite https://www.codigofluente.com.br/aula-20-python-biblioteca-padrao-modulo-io/.

# demonstração do uso do método `read` do objeto 'file'
with open(caminho_do_arq, mode="r") as arq:
    # lê o conteúdo todo do arquivo numa string
    conteudo = arq.read()
print(conteudo)

# Se o tamanho do arquivo é relativamente pequeno comparado ao da memória principal, então podemos ler o arquivo todo em uma string usando o método `read()` do identificador de arquivo, como mostra o exemplo seguinte:

arq = open(caminho+'mbox-short.txt')
conteudo = arq.read()                       # armazena todos os caracteres do arquivo na var. 'conteudo'
print('Qtde de caracteres:', len(conteudo))
print(conteudo[:50])
if arq.closed == False:
    arq.close()

# Quando o arquivo é lido dessa forma, todos os caracteres, incluindo os caracteres de novas linhas (*newline*) formam uma grande string armazenada na variável `conteudo`. É uma boa ideia armazenar a saída de `read()` numa variável porque cada chamada de `read()` faz uso do recurso computacional.
#
# #### Métodos do Objeto `file`:
#
# * **read()**: Lê todo o conteúdo do arquivo como uma string ou como bytes (dependendo do modo de abertura)
# * **readline()**: Lê a próxima linha de texto do arquivo, incluindo o caractere final '\n'
# * **readlines()**: Lê as linhas de texto do arquivo, armazenando cada linha como uma string em uma lista.
# * **write(x)**: Escreve a string 'x' no arquivo.
# * **writelines(x)**: Dado um iterável de strings, trata cada string como uma linha de texto a ser gravada no arquivo (o inverso de `readlines()`)
#
# Além disso, é importante observar que o objeto `file` pode ser iterado e que cada iteração retornará uma linha individual de texto do arquivo. Esta é a melhor maneira de ler um arquivo inteiro, linha a linha.

# ### 2.4 Busca em um Arquivo
# Quando se está procurando por dados em um arquivo, é muito comum percorrer o arquivo, ignorando a maioria das linhas e apenas processando as que combinam com uma condição particular. Podemos ajuntar a leitura do arquivo com os métodos de strings para construir um mecanismo de busca.  
#
# Por exemplo, se quisermos ler um arquivo e mostrar apenas as linhas que começam com o prefixo "From:", podemos usar o método `startswith()` para selecionar apenas as linhas que possuem o prefixo desejado. Usamos também o método `rstrip()` para eliminar os espaços em branco à esquerda ou direita da string:

arq = open(caminho+'mbox-short.txt')
for linha in arq:
    if linha.startswith('From:'):
        print(linha.rstrip())

# Vamos usar o método `find()` de strings para procurar por uma ocorrência de uma string dentro de outra e retornar a posição dela ou -1 se não for encontrada. Em particular, vamos mostrar somente as linhas que contém a string “@<EMAIL>” (isto é, emails oriundos da Universidade de Cape Town, na África do Sul):

arq = open(caminho+'mbox-short.txt')
for linha in arq:
    if linha.startswith('From:'):
        if linha.find('@uct.ac.za') == -1: continue
        print(linha.rstrip())

# No script anterior usamos a forma contraída da sentença `if` quando colocamos o `continue` na mesma linha do `if`. Essa forma funciona do mesmo jeito que se o `continue` estivesse na próxima linha e indentado.

# ### 2.5 Qual Arquivo?
#
# #### Usuário Escolhe o Arquivo
# Para não ter quer editar o código Python toda vez que quisermos processar um arquivo diferente, podemos solicitar ao usuário a entrada do nome do arquivo a ser processado, cada vez que o programa for executado.

nomearq = input('Entre nome do arquivo (sem a extensão): ')
arq = open(caminho+nomearq+'.txt')
contagem = 0
for linha in arq:
    if linha.startswith('Subject:'):
        contagem += 1
print(f"Tem {contagem} linhas com 'Subject' em {nomearq}.txt")

# #### Procurando Arquivos
# Existem muitos casos em que podemos querer iterar o conteúdo de uma lista de arquivos... Vamos construir manualmente essa lista de arquivos:

arquivos = ['dat/arq1.txt', 'dat/arq2.txt', 'dat/arq3.txt', 'dat/arq4.txt']

# No entanto, isso é extremamente tedioso e sujeito a erros, seja digitando incorretamente o nome de um arquivo ou esquecendo-se de um ou outro arquivo. A maneira certa de se construir essa lista de arquivos é **agrupando** arquivos. Um *glob* é um conjunto de nomes de arquivos que correspondem a um dado padrão. Para globalizar arquivos, usamos caracteres curinga especiais que irão corresponder a todos os arquivos com uma determinada parte de um nome de arquivo. Nesse caso, o asterisco `*` será o caractere curinga que mais usaremos -- ele corresponde a qualquer caractere. Abaixo, vemos alguns *globs* e os tipos de padrões aos quais eles corresponderão:

# ```python
# # corresponde a qualquer coisa que comece com `arq` e termine com `.txt` como
# # arq1.txt, arq_tmp.txt, arq.txt, arq12345.txt, ...
# arq*.txt
#
# # corresponde a todos os arquivos '.txt' no diretório 'dat'
# dat/*.txt
#
# # corresponde a qualquer nome de arquivo
# *
#
# # corresponde a todos os arquivos de imagem png
# *.png
#
# # corresponde a qualquer coisa que contenha 'teste' como parte de seu nome de arquivo
# *teste*
#
# # corresponde a todos os arquivos '.py' que contêm 'numero'
# *numero*.py
# ```

# O módulo `pathlib` fornece funcionalidade conveniente para agrupamento de arquivos (*globbing*). Assim que tivermos um objeto `Path`, podemos simplesmente chamar `glob()` nele e passar uma string. Isso retornará um gerador que produzirá cada um dos arquivos agrupados.

# agrupa todos os arquivos de texto no diretório atual
# que começa com 'arq' e termina com '.txt'
dir_raiz = Path('.')
arqs = dir_raiz.glob('arq*.txt')              # isso produz um gerador
arqs

# obter uma lista ordenada dos caminhos agrupados
sorted(arqs)

# iterando sobre o gerador diretamente
for arquivo in dir_raiz.glob('arq*.txt'):
    with open(arquivo, 'r') as f:
        # faz algum processamento
        string = f.readlines()
        print(string)

# <a style="color:red"><b>Exercício</b></a>:
#
# Escreva um padrão *glob* para cada um dos seguintes agrupamentos:
#
# - Todos os arquivos '.txt' do diretório './arqs'.
# - Todos os arquivos que contêm 'ifg' como parte de seu nome.
# - Todos os arquivos que começam com 'dados'.
# - Todos os arquivos que começam com a letra 'q', contém um 'w' e terminam com uma extensão '.npy'.

# O caractere `*` não é o único padrão disponível. Às vezes, pode ser útil combinar certos subconjuntos de caracteres. Por exemplo, podemos querer apenas combinar nomes de arquivo que começam com um dígito numérico. Com o `*` curinga sozinho, isso não é possível. Para combinar um subconjunto de caracteres, podemos usar colchetes: `[abc]*` corresponderá a qualquer coisa que comece com `'a', 'b'` ou `'c'` e nada mais. Também podemos usar um hífen `'-'` dentro dos colchetes para agrupar faixa de caracteres. Por exemplo:
#
# ```python
# # corresponde a qualquer arquivo texto que comece com um dígito decimal
# [0-9]*.txt
#
# # corresponde a qualquer arquivo que tenha uma vogal em seu nome
# *[aeiou]*
#
# # corresponde a qualquer arquivo que comece com uma letra minúscula
# [a-z]*
# ```

# <a style="color:red"><b>Exercício</b></a>:
#
# Escreva um padrão *glob* para cada um dos seguintes agrupamentos:
#
# - Todos os arquivos com um dígito octal ímpar em seu nome
# - Todos os arquivos '.txt' que contêm as letras 'q' ou 'z'

# ### 2.6 Cuidando dos Erros Previsíveis
#  O que ocorre se o usuário não digitar um nome válido de arquivo no script anterior?

nomearq = input('Entre nome do arquivo (sem a extensão): ')
arq = open(caminho+nomearq+'.txt')
contagem = 0
for linha in arq:
    if linha.startswith('Subject:'):
        contagem += 1
print(f"Tem {contagem} linhas com 'Subject' em {nomearq}.txt")

# Então, agora que vimos uma possível falha no programa, podemos corrigi-la usando a estrutura
# `try/except`:

nomearq = input('Entre nome do arquivo (sem a extensão): ')
try:
    arq = open(caminho+nomearq+'.txt')
except:
    print(f'Erro: Arquivo [{nomearq}.txt] não pode ser aberto!')
else:
    contagem = 0
    for linha in arq:
        if linha.startswith('Subject:'):
            contagem += 1
    print(f"Tem {contagem} linhas com 'Subject' em {nomearq}.txt")

# ### 2.7 Gravando em Arquivos Textuais
# Para gravar dados em um arquivo é preciso abri-lo no modo de escrita, usando o parâmetro `mode = 'w'`:

arq = open(caminho+'saida.txt', 'w')
print(arq)

# Se o arquivo já existir, abri-lo no modo de gravação provocará o apagamento dos dados antigos e
# inicia a gravação a partir do zero, por isso tome cuidado! Se o arquivo não existir, um novo será
# criado.  
# O método `write()` do objeto arquivo escreve dados no arquivo, retornando o número de caracteres gravados. O modo de gravação padrão é **texto** para escrever e ler strings.

linha1 = "Bem-aventurados os pobres de espírito, porque deles é o Reino dos céus. (Mt.5:3)\n"
arq.write(linha1)

# O arquivo mantém o controle de onde está o ponteiro de inserção de dados, logo, ao chamar `write()` novamente, ele adicionará os novos dados ao final. Devemos nos certificar de gerenciar as extremidades das linhas à medida que escrevemos no arquivo inserindo explicitamente o caracter *newline* quando terminar uma linha. A instrução `print()` anexa automaticamente uma nova linha, mas o método `write()` não adiciona a nova linha automaticamente.

linha2 = "Convém que vivamos de forma mais simples...\n"
arq.write(linha2)
arq.close()

# Visualizando o conteúdo do arquivo gravado
arq = open(caminho+'saida.txt')
print(arq.read())
arq.close()

# ### 2.8 Arquivos Binários
# Para gravar dados em um arquivo binário é preciso abri-lo no modo de escrita, usando o parâmetro `mode = 'wb'` ou `'xb'`:

# salvando um vetor NumPy (arranjo) no arquivo 'vetor.npy'
import numpy as np
x = np.array([1, 2, 3])
with open("vetor.npy", mode="wb") as f:
    np.save(f, x)

# carregando um vetor NumPy (arranjo) salvo no arquivo 'vetor.npy'
with open("vetor.npy", mode="rb") as f:
    y = np.load(f)
print(y)

# <a style="color:red"><b>Exercícios</b></a>:
# 1. Escreva um programa que leia um arquivo e mostre o conteúdo (linha a linha), completamente em caixa alta (em letras maiúsculas). <br><br>
# 2. Escreva um programa que solicite o nome de um arquivo, e depois o leia e procure por linhas da forma: 'X-DSPAM-Confidence: 0.8475'. Quando encontrar uma linha que inicie com "X-DSPAM-Confidence:", separe o número de ponto flutuante que aparece no final da linha. Conte essas linhas e então compute o total de valores de Confiança de Spam. Quando chegar no fim do arquivo, mostre a média de Confiança de Spam. Teste seu programa com os arquivos mbox.txt e mbox-short.txt
# > Digite o nome de um arquivo: mbox.txt  
# > Média de confiança de spam: 0.894128046745  <br><br>
# > Digite o nome de um arquivo: mbox-short.txt  
# > Média de confiança de spam: 0.750718518519   
#

# ## <a id="pickle">3. Pickle</a>
#
# <img src= "img/pickles.jpg" width=300>
# Queremos evitar situações desagradáveis, como a perda de dados, calculados em seu programa Python. Então, vamos mostrar como salvar seus dados de maneira fácil, de modo que seu programa possa reler-los posteriormente. Vamos 'conservar' (*pickling*) os dados, para que nada se perca.
#
# Python oferece um módulo para essa finalidade, chamado `pickle`. Com os algoritmos do módulo **pickle** podemos serializar e deserializar estruturas de objetos Python. 'Conservar' (*unpickling*) é o processo de converter uma hierarquia de objetos Python num fluxo de bytes, e 'desconservar' (*pickling*) é a operação inversa, isto é, o fluxo de bytes é convertido novamente em uma hierarquia de objetos Python. O que chamamos de conservação é também conhecido como 'serialização' ou 'achatamento' de uma estrutura de dados.
#
# Um objeto pode ser serializado com o método de despejo (*dump*) do módulo `pickle`:
#
# ```python
# pickle.dump(obj, arquivo[, protocolo, *, fix_imports=True])
# ```
# `dump()` cria uma representação serializada de `obj` para abrir um arquivo do objeto. O argumento facultativo `protocolo`  diz ao serializador para usar o protocolo dado:
#
# * Protocolo versão 0 é o original (antes da Python3) protocolo legível (ASCII) e é compatível com versões anteriores do Python.
# * Protocolo versão 1 é o antigo formato binário que também é compatível com versões anteriores do Python.
# * Protocolo versão 2 foi introduzido em Python 2.3. Ele oferece muito mais conservação eficiente de classes de estilo novos.
# * Protocolo versão 3 foi introduzido em Python 3.0. Ele tem suporte explícito para bytes e não pode ser deserializar (*unpickled*) pelo módulo pickles do Python 2.x. É o protocolo recomendado para Python 3.x.
#
# O protocolo padrão de Python3 é a versão 3.
#
# Se `fix_imports` for Verdadeiro e o protocolo for menor que 3, piclkes tenta mapear os novos nomes do Python3 para os nomes do módulo antigo utilizados no Python2, de modo que o fluxo de dados pickles seja legível com Python2.
#
# Os objetos que foram descarregados num arquivo com `picles.dump` podem ser relidos num programa usando o método de `piclke.load(file)`. Esse método reconhece automaticamente o formato que tinha sido usado para escrever os dados. 
#
# Exemplo simples:

import pickle
cidades = ['Pirenópolis', 'Corumbá', 'Luziânia', 'Cidade de Goiás', 'Cavalcante']
fh = open('dados.pkl', 'wb')
pickle.dump(cidades, fh)
fh.close()

# O arquivo de "dados.pkl" pode ser lido novamente por Python, na mesma ou em outra sessão ou por um programa diferente:

f = open("dados.pkl", "rb")
cids = pickle.load(f)
print(cids)

# Apenas os objetos, não os seus nomes, são guardados. É por isso que usamos a atribuição de 'cids' no exemplo anterior, ou seja, cids = pickle.load(f).
#
# O exemplo anterior teve em conserva apenas um objeto, por exemplo, uma lista das cidades goianas mais antigas. Mas como seria a consevação de vários objetos? A solução é fácil: arrumar os objetos em outro objeto, portanto, só teremos de serializar um objeto novamente. Vamos embalar duas listas 'linguagens_programação' e 'dialetos_python' em uma lista objeto_pickle no exemplo a seguir:

fh = open("dados2.pkl","bw")
linguagens_programação = ["Python", "Perl", "C++", "Java", "Lisp"]
dialetos_python = ["Jython", "IronPython", "CPython"]
objeto_pickle = (linguagens_programação, dialetos_python)
pickle.dump(objeto_pickle,fh)
fh.close()

# A conserva de dados do exemplo anterior, isto é, os dados que temos escrito para o arquivo 'dados2.pkl', pode ser separada em duas listas, novamente, quando relermos os dados:

f = open('dados2.pkl','rb')
(lings, dialetos) = pickle.load(f) 
print(lings, dialetos)

# + [markdown] slideshow={"slide_type": "slide"}
# ## Fim.
# <p style="text-align:right;"><a href='../Índice.ipynb' target="_self">Volta ao Índice</a></p>
# -

# ## Anexo - Biblioteca Padrão do Python
# <a id="anexo"></a>Alguns dos módulos mais importantes dessa biblioteca:
#
# * Matemática: `math, cmath, decimal e random`.
#
# * regex: `re`- módulo de expressões regulares com funções muito poderosas para manipulação de texto.
#
# * Sistema Operacional: `os, io, sys, glob, logging, shutils e subprocess`.
#
# * Threads (linhas de processamento): `threading`.
#
# * Manipulação de datas e horários: `datetime`.
#
# * Persistência de dados: `pickle e cPickle`.
#
# * XML: `xml.dom, xml.sax e elementTree`.
#
# * Configuração: `ConfigParser e optparse`.
#
# * Tempo: `time e datetime`.
#
# * Outros: `traceback, types, json e timeit`.
#
# Listagem completa dos módulos da **Biblioteca Padrão** da Ling. Python pode ser vista [aqui](https://onlinelibrary.wiley.com/doi/pdf/10.1002/9781119207580.app2), ou se quiser, podemos gerar a lista de módulos disponíveis na versão Python usada como base a esse Jupyter Notebook:

import distutils.sysconfig as sysconfig
import os
bibs = sysconfig.get_python_lib(standard_lib=True)
# Se standard_lib for True, o diretório para a biblioteca padrão é retornado 
# em vez do diretório para a instalação de extensões de terceiros.
n = 0
for topo, dirs, arqs in os.walk(bibs):
    for arq in arqs:
        if arq != '__init__.py' and arq[-3:] == '.py':
            print(os.path.join(topo, arq)[len(bibPadrao)+1:-3].replace(os.sep, '.'))
            n += 1
print(f"\n* Total de módulos instalados: {n}")


# +
def bib_padrao(versao=None, so_nivel_superior=True):
    import re   # módulo da bibl. padrão p/ análise de exprs. regulares
    from urllib.request import urlopen
    if versao is None:
        import sys # outro módulo da bibl. padrão
        versao = sys.version_info
        versao = f"{versao.major}.{versao.minor}"
    url = f"https://docs.python.org/{versao}/py-modindex.html"
    with urlopen(url) as f:
        paginaWeb = f.read()
    modulos = set()
    n = 0
    for modulo in re.findall(r'#module-(.*?)[\'"]',
                             paginaWeb.decode('ascii', 'replace')):
        if so_nivel_superior:
            modulo = modulo.split(".")[0]
        modulos.add(modulo)
        n += 1
    return (modulos,n,versao)

mod, qtd, vs = bib_padrao()
print(f"Qtde de módulos da biblioteca padrão da Ling. Python Vs. {vs}: {qtd}\nMódulos:\n{sorted(mod)}")

# -

# Fontes: 
# 1. https://www.codigofluente.com.br/aula-19-python-biblioteca-padrao-modulo-math/
# 1. https://www.pythonlikeyoumeanit.com/Module5_OddsAndEnds/WorkingWithFiles.html#Opening-Files
# 1. https://python-course.eu/python-tutorial/file-management.php

StarcoderdataJupyterScriptsDedupFiltered