"""
# Saída

Para imprimir alguma coisa na tela, utilizamos a _built-in function_ **_print_**
"""
print('Matheus Tenorio')
"""
### Note que o print deve possuir parenteses
"""
"""
Note que o valor passado como argumento da função está entre aspas.
Quando um texto está entre aspas, isto é uma _string_. 
Uma string pode estar entre aspas simples ('') ou aspas duplas ("")
"""
'Matheus Tenorio' # String com aspas simples
"Matheus Tenorio" # String com aspas duplas
"""
# Comentários
Um comentário é uma parte do código (ou do script) que não será executado. \
Os comentários são úteis para impedir, de forma fácil, que parte do código seja executado, seja para testes, para eliminar um bug, ou até para descobrir onde está o bug.
Também são utilizados para fazer anotações como no código acima.
Os comentários podem ser de uma linha, utilizando o símbolo cerquilha (#).
Tudo após o cerquilha será considerado um comentário
"""
print('Matheus Tenorio')
# print('Idade: 24')
# Esta linha não será executada bem como a linha acima
"""
Para fazer um comentário de muitas linhas podemos utilizar aspas triplas (''' ''')
"""
print('Matheus Tenorio') # comentário de uma linha
'''
Comentário de muitas linhas
print('Idade: 24')
Esta linha não será executada bem como a linha acima
'''
"""
Como pode ser visto no exemplo acima, as aspas triplas também são considerados strings de várias linhas
"""
print('''Meu nome é Matheus Tenorio
Minha Idade é 24 anos''')
"""
# Erros

Quando alguma coisa dá errada no script, o interpretador Python interrompe a execução do programa e imprime uma mensagem de erro
"""
print('Matheus Tenorio')
print('Idade: 24 anos')
print('Altura: 1,80cm')
"""
Vamos dar uma olhada na mensagem de erro impressa:
```Shell
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-12-72bcea0f1f9d> in <module>
      1 print('Matheus Tenorio')
      2 print('Idade: 24 anos')
----> 3 pritn('Altura: 1,80cm')

NameError: name 'pritn' is not defined
```
A primeira linha mostra o tipo do erro. Neste caso foi `NameError` que indica um erro de sintaxe. Mais precisamente a função `print` foi escrita errada (`pritn`)
Nas próximas linhas é mostrado em qual linha o erro ocorreu. Isto é útil para encontrar o erro em um script com muitas linhas.
"""
"""
# Variáveis

Em Python, as variáveis são declaradas da seguinte forma:
"""
age = 24
"""
Perceba que não precisamos definir o tipo da variável, pois Python é dinamicamente tipada.
Isto quer dizer que uma variável possui o tipo do dado que ela está armazenando no momento.
Diferente de outras linguagens como: C, C++, Java, Rust, em que todas as variáveis possuem um tipo na sua declaração e não podem ser alterados depois, em Python as variáveis possuem o tipo do dado que recebem, podendo esse tipo ser alterado depois da declaração.
"""
age = 24          # O tipo da variável age, no momento, é um inteiro
age = 'Ola Mundo' # O tipo da variável age passa a ser uma string
"""
Note também que a variável é criada no momento em que a atribuição é feita, isto é, no momento em que um valor é dado para ela `age = 24`.
Desta forma, a criação de uma variável sempre possui uma atribuição.
Para criar uma variável sem valor, podemos utilizar a palavra-chave `None`
"""
age = None
"""
A palavra-chave `None` representa o valor nulo do Python. Assim, a variável `age` existe mas não possui valor.
"""
age = None
age1 = 0
age == age1
"""
Perceba que o valor nulo `None` é diferente do inteiro 0. 
Na variável `age` não existe valor armazenado, enquanto na variável `age1` existe um valor, o inteiro zero
"""
"""
# Regras de nomeclatura de variáveis
Existe alguns nomes que são permitidos para os nomes das variáveis, enquanto outros nomes são proíbidos.

Os nomes permitidos possuem as seguintes regras:
1. Deve começar com **letra minúscula**, ou com **letra maiúscula** ou com **underline** (\_)
2. Pode possuir **números**, porém **não pode iniciar** com números
3. Os únicos simbolos permitidos são: **letras minúscula**, **letra maiúscula**, **underline**, **números**
"""
age = 24
Age = 24
_age = 24
my_age = 24
age23 = 24
_24 = 24
a2 = 24
"""
Os nomes proíbidos possuem as seguintes regras:
1. Qualquer nome que quebre uma, ou mais, regras dos nomes permitidos
2. Palavras-chave do Python
"""
2age = 24    # Quebra as regras 1 e 2 dos nomes permitidos
age@new = 24 # Quebra a regra 3 dos nomes permitidos
for = 23     # Regra 2 dos nomes proibidos
"""
# Convensão dos nomes das variáveis
O nome das variáveis seguem uma convensão de nomes chamada **snake_case**.
Nessa convensão as variáveis seguem o seguinte formato:
> Todas as letras são minúsculas e as palavras compostas são separadas por underline (\_)

A convensão de nomes é uma forma da comunidade padronizar a formatação (visual) do código. 
Isto quer dizer que é apenas uma escolha da comunidade. Não é obrigatório, mas é recomendado
"""
my_age = 24 # snake_case -> Recomendado para variáveis
myAge = 24  # camelCase -> Possível, mas não recomendado para variáveis
MyAge = 24  # CamelCase -> Possível, mas não recomendado para variáveis
"""
# Entrada
Para obter dados provenientes dos usuários, podemos utilizar a _built-in function_ `input`.
Assim como a função `print` está relacionada com a saída de dados, a função `input` está relacionada com a entrada de dados.
"""
age = input()
"""
O retorno da função `input` consiste nos dados digitados pelo usuário. Este retorno é sempre do tipo `string`.
Para adicionar um promt (uma frase, ou palavra, que vem antes da entrada de dados) basta adicionar uma string como argumento da função `input`
"""
age = input('Qual a sua idade? ')
"""
# Tipos básicos

Aqui vamos ver os tipos básicos em Python, qualquer outro tipo é uma composição deles. Outro ponto importante é que todos os tipos são objetos.

Primeiro, vamos ver a diferença entre valor e tipo:

* Valor: É o dado;
* Tipo: É a classificação do tipo.

Em Python, existe a função 'type', a qual retorna o tipo do dado.
"""
"""
Os tipos básicos são:

* Boleano - bool;
* Inteiro - int;
* Real - float;
* Complexo - complex;
* String - str.

E todos são object.
"""
type(True)
type(82)
type(4.0)
type(1 + 1j)
type("Maceió")
"""
Além disso, podemos também converter um tipo em outro. Existe algumas limitações como não é possível converter qualquer string em int ou float. Para isso, basta utilizar a palavra reservada de cada tipo.

   * bool(arg)
   * int(arg)
   * float(arg)
   * complex(arg)
   * str(agr)

Em especial o int(arg), pode converter uma base além da decimal. Ou seja, a função de conversão tem como padrão a base 10, porém é possível converter para outra base utilizando int(arg,base=10).
"""
int('1010', 2) # Binária
int('a', 16) # Hexadecimal
"""
Vale lembrar que para os boleanos, temos que o valor 0 (zero) indica false, enquanto qualquer outro valor indica verdadeiro.
"""
bool('1')
bool(0.1)
bool('0.0')
bool('0')
bool(0)
bool(0.0)
"""
Rode o código acima para ver cada saída.
"""
"""
# String

String é uma sequência de caracteres e um objeto iterável(Veja tabela ASCII).
"""
str1 = 'Edge'
str2 = "Edge"
"""
## Operadores

As string contém diversos operadores

Comparação - Utiliza como base a tabela ASCII:

   * Igualdade (==)
   * Diferente (!=)
   * Maior que (>)
   * Menor que (<)
   * Maior ou igual que (>=)
   * Menor ou igual que (<=)

Aritmétrico:

   * Soma (+)
   * Multiplicação (*)

Atribuição:

   * Atribuição (=)
   * Atribuição com soma (+=)
   * Atribuição com Multiplicação (*=)

Conjunto:

   * Pertence (in)
   * Não pertence (not in)
"""
string1 = 'abcd'
string2 = "abc"

# Igualdade
print( string1 == string2 )
# Diferente
print( string1 != string2 )
# Maior que
print( string1 > string2 )
# Menor que
print( string1 < string2 )

# Soma de strings (Concatenação)
print( string1 + string2 )
# Multiplicação
print( 5 * string1 )

# Atribuição
string3 = string2 + string1
print(string3)

# Pertence
print( string2 in string1 )
"""
Além dos operadores, as strings possuem um conjunto de métodos - https://docs.python.org/2.5/lib/string-methods.html. Vamos abordar alguns deles aqui como transformar todos os caracteres em 'caixa baixa' ou 'caixa alta' e separa em substring a partir de um padrão.
"""
phrase = 'Que desastre! Até minhas fraquezas são mais fortes que eu!'
print(phrase.lower())
print(phrase.upper())
print(phrase.split(' '))
"""
# Condicionais
Para executar um trecho de código de acordo com uma condição, podemos utilizar os condicionais. Em Python, temos 3 palavras-chaves para a utilização de condicionais: `if`, `else` e `elif`. A estrutura `if` pode ser utilizado sozinho ou em conjunto com o `elif` e com o `else`. A sintaxe do `if` é a seguinte:
```
if <condição>:
    <code>
```
Após a palavra-chave `if` temos uma condição `<condição>` e o símbolo dois pontos `:`, para determinar o final das condições. 
Na próxima linha é definido o corpo do `if`, sempre indentado. O corpo do `if` termina quando a indentação termina.
"""
age = input()
age = int(age)
print('Sua idade é %d' % age)

# 0 <= idade <= 18 -> 'Você é criança'
# 18 < idade <= 50 -> 'Você é adulto'
# 50 < idade       -> 'Você é idoso'

if 0 <= age and age <= 18:
    print('Você é criança')
if 18 < age and age <= 50:
    print('Você é adulto')
if 50 < age:
    print('Você é idoso')
"""
A estrutura `else` é sempre utilizada em conjunto com a estrutura `if`. Isto que dizer que o `else` começa assim que o corpo do `if` termina. A estrutura `else` possui a seguinte sintaxe:
```
if <condição>:
    <code>
else:
    <code>
```
Note que após a palavra-chave `else` não existe condição, somente o símbolo dois pontos `:`. Isto ocorre porque a estrutura `else` representa o código executado, quando a condição do `if` é falsa. O corpo do `else` segue a mesma linha do corpo do `if`.
"""
age = input()
age = int(age)
print('Sua idade é %d' % age)

# 0 <= idade <= 18 -> 'Você é criança'
# 18 < idade <= 50 -> 'Você é adulto'
# 50 < idade       -> 'Você é idoso'

if 0 <= age and age <= 18:
    print('Você é criança')
else:
    if age <= 50:
        print('Você é adulto')
    else:
        print('Você é idoso')
"""
Para criar várias condições, podemos unir as ideias do `if` e do `else`. No Python, utilizamos a estrutura `elif`. Essa estrutura é sempre acompanhada do `if` ou de outro `elif`. Isto quer dizer que podemos ter vários `elif` acompanhados do mesmo `if`. A sintaxe do `elif` é a seguinte:
```
if <condição1>:
    <code>
elif <condição2>:
    <code>
elif <condição3>:
    <code>
else:
    <code>
```
Como pode ser visto, a sintaxe do `elif` é parecida com a sintaxe do `if`: palavra-chave `elif`, seguida de uma condição, seguido do símbolo dois pontos `:`, seguido do corpo do `elif` na próxima linha (indentado). A estrutura `else` no final é opcional, enquanto a estrutura `if` no inicio é obrigatório.
"""
age = input()
age = int(age)
print('Sua idade é %d' % age)

# 0 <= idade <= 18 -> 'Você é criança'
# 18 < idade <= 50 -> 'Você é adulto'
# 50 < idade       -> 'Você é idoso'

if 0 <= age and age <= 18:
    print('Você é criança')
elif age <= 50:
    print('Você é adulto')
else:
    print('Você é idoso')
"""
# Operador Ternário
Existe uma forma mais resumida de implementar os condicionais, em apenas uma linha. Estes é o operador ternário. A origem do nome é porque esse operador é um dos poucos operadores que recebem 3 argumentos. Desta forma, o nome operador ternário é sempre associado a este operador que avalia uma condição e retorna uma expressão dependendo da condição avaliada. A sintaxe do operador ternário em Python é a seguinte:
```
var = <expressão verdadeira> if <condição> else <expressão falsa>
```
Note que a sintaxe do operador é bem parecida com a linguagem natural:
> Retorne `<expressão verdadeira>` se a condição `<condição>` for verdadeira, caso contrário retorne `<expressão falsa>`
"""
age = input()
age = int(age)
print('Sua idade é %d' % age)

# 0 <= idade <= 18 -> 'Você é criança'
# 18 < idade <= 50 -> 'Você é adulto'
# 50 < idade       -> 'Você é idoso'

age_range = 'criança' if 0 <= age <= 18 else ('adulto' if age <= 50 else 'idoso')
print('Você é ' + age_range)
"""
# Coleções
Em python, temos o conceito de coleções. Um coleção, como sugere o nome, é um conjunto de elementos relacionados, que podem possuir, ou não, o mesmo tipo.
Temos 4 tipos principais de coleções: 
1. Lista
1. Tupla
1. Conjunto (Set)
1. Dicionário
"""
"""
# Lista
A lista é uma coleção sequencial indexada. Sequêncial porque a ordem importa no momento que os itens são armazenados e indexada por utilizar indices númericos para obter os itens armazenados na lista. Por consequência, os itens da lista são acessados e modificados por indices númericos, sendo estes indices correspondentes as posições, nas quais os itens estão armazenados na lista. A lista corresponderia ao array de outras linguagens, mas com a vantagem de aceitar diversos tipos. A sintaxe da lista é a seguinte:

```
<nome_da_variavel> = [<item1>, <item2>, <item3>, ..., <itemN>]
```
O `<nome_da_variável>` representa o nome da variável que vai armazenar a lista. A lista é sempre delimitada por colchetes `[]`. Desta forma, tudo que estiver dentro dos colchetes será um item da lista. Os `<item1>`, `<item2>`, `<item3>` e `<itemN>`, correspondem aos itens da lista. Perceba que os itens da lista são separados por vírgula `,`. Caso a lista possua apenas um item não é necessário a vírgula, apenas o item.
"""
guests = ['Matheus', 'Luana', 'Lucas', 'Alfredo', 'Jéssica']
"""
## Indexação
Para acessar um item, ou modifica-lo, utilizamos a indexação. A indexação é uma operação que retorna um item de uma lista, de acordo com o indice que você utilizar.
O python possui dois tipos de indexação: a __indexação normal__ e a __indexação reversa__.

### Indexação Normal
A sintaxe da indexação normal (também chamada de forward indexing) é a seguinte:
```
<itemN> = <var_list>[<indexN>]
<var_list>[<indexN>] = <new_itemN>
```
A variável que contém a lista é `<var_list>` e para indexar um valor utilizamos: os colchetes `[]` após a variável e um inteiro (pode ser uma constante ou uma variável do tipo inteiro) representando o indice `<indexN>`. Vamos analizar as duas linhas da sintaxe mostrada:
1. Na primeira linha existe um acesso ao item na posição `<indexN>`, da lista `<var_list>`. O retorno deste item é armazenado na variável `<itemN>`;
1. Na segunda linha existe uma modificação na posição `<indexN>`, da lista `<var_list>`. Na posição `<indexN>` será atribuido o item `<new_itemN>`.

Na indexação normal, os indices começam por zero. Isso quer dizer que para obter o primeiro item, utilizamos o indice 0, para obter o segundo elemento o indice 1 e assim por diante. Resumindo, para indexar o n-ésimo item de uma lista, temos que utilizar o indice `(n-1)`
"""
print(guests)
first_guest = guests[0]
second_guest = guests[1]

print('O primeiro convidado é: %s' % (first_guest))
print('O segundo convidado é: %s' % (second_guest))
"""
### Indexação Reversa
Em Python, diferente de outras linguagens de programação, existe uma outra forma de indexar listas, chamada indexação reversa. Na indexação reversa os posições dos items são acessados na ordem reversa, isto é, de trás pra frente. Para utilizar esta indexação utlizamos a mesma sintaxe da indexação normal, porém com os indices negativos e começando por -1. Assim, o último item da lista é indexado com o indice -1, o penúltimo é indexado com o indice -2 e assim por diante. Resumindo, para acessar o n-ésimo item, da direita para a esquerda, utilizamos o indice `-n`.
"""
print(guests)
last_guest = guests[-1]
last_but_one_guest = guests[-2]

print('O último convidado é: %s' % (last_guest))
print('O penúltimo convidado é: %s' % (last_but_one_guest))
"""
Para indexar itens no início da lista utilizamos a indexação normal, já para indexar itens no final da lista utilizamos a indexação reversa
> - Inicio -> Normal
> - Final -> Reversa

## *Slicing* (Fatiamento)
Em Python, temos uma forma simplificada de obter uma sublista, chamada *slicing* (ou fatiamento). O *slicing* pode ser feito em qualquer coleção sequencial indexada, tal como: conjuntos e strings. A sintaxe é bem parecida com a indexação, porém o resultado é diferente: enquanto na indexação é retornado um item de uma lista, no *slicing* é retornado uma lista totalmente nova e independente da lista original. Formalizando, o *slicing* retorna uma nova coleção, do mesmo tipo da coleção original, porém independente dessa. Por exemplo: o slicing de uma string retorna uma nova string e independente da string original. A sintaxe do *slicing* é a seguinte:
```
<sublist> = <var_list>[<s>:<e>]
```
A variável que contém a lista é `<var_list>` e para fazer o *slicing* vamos utlizar: os colchetes `[]` após a variável e dentro dos colchetes vamos utilizar dois inteiros (pode ser constantes ou variáveis do tipo inteiro) separados por dois pontos `:`. O primeiro inteiro dentro dos colchetes `<s>` representa a posição inicial, enquanto o segundo inteiro dentro dos colchetes `<e>` representa a posição final. Assim a nova lista, que será atribuída a variável `<sublist>`, será composta por todos os itens, dentro do intervalo `[<s>, <e>)`, da lista `<var_list>`. Perceba que o item presente na posição `<e>` não será incluído na nova lista. Os itens presentes serão os itens da posição: `<s>`, `(<s>+1)`, `(<s>+2)` até o item na posição `(<e>-1)`.
"""
print(guests)
print(f'Os dois primeiros itens são: {guests[0:2]}')
print(f'O 3º, 4º e 5º itens são: {guests[2:5]}')
"""
Também podemos utilizar indices negativos no *slicing*. Eles funcionam da mesma forma como na indexação reversa.
"""
print(guests)
print(f'Os dois primeiros itens são: {guests[-5:-3]}')
print(f'O 3º e 4º itens são: {guests[-3:-1]}')
"""
Podemos omitir um dos indices no *slicing*, ou todos. 
- Se o indice omitido for o primeiro, então a nova lista começará no início da lista original. 
- Se o indice omitido for o segundo, então a nova lista terminará no final da lista original.
- Se ambos os indices forem omitidos, então a nova lista será igual a lista original.

Isto é útil para obter sublistas importantes:
1. Os `n` primeiros itens de uma lista `<var_list>`:
```
<var_list>[:n]
```
2. Os `n` últimos itens de uma lista `<var_list>`:
```
<var_list>[-n:]
```

Vamos analizar cada caso:
1. O primeiro indice está omitido, então o começo da nova lista será o começo da lista original. O segundo indice indica que a nova lista termina antes do indice `n`, ou seja, vai até o item no indice `(n-1)`, que é o n-ésimo item. Portanto, a nova lista começa no começo da lista original e termina no n-ésimo item, isto quer dizer que estamos obtendo o `n` primeiros itens da lista.
2. O primeiro índice indica que a nova lista começa no indice `-n`, ou seja, o n-ésimo item de trás pra frete (da direita para a esquerda), já que a indexação reversa começa com -1. O segundo indice está omitido, então o final da nova lista será o final da lista original. Portanto, a nova lista começa no n-ésimo último elemento e termina no final da lista original, isto quer dizer que estamos obtendo os `n` últimos itens da lista.
"""
print(guests[:])
print(f'Os dois primeiros itens são: {guests[:2]}')
print(f'Os três últimos itens são: {guests[-3:]}')
"""
## Operadores
Existem dois operadores importantes que podem ser utilizados nas listas: `in` e `del`.

### Operador `in`
O operador `in` é utilizado para verificar a relação de pertinencia entre itens e listas. A sintaxe deste operador é a seguinte:
```
<itemN> in <var_list>
```
Essa expressão é utlizada para verificar se o item `<itemN>` pertence à lista `<var_list>`. 
Ainda podemos ser verificado se o `<itemN>` não pertence à lista `<var_list>`, utilizando a palavra-chave `not` na frente do operador `in`. A sintaxe é da seguinte forma:
```
<itemN> not in <var_list>
```

### Operador `del`
O operador `del` é utilizado para excluir um item de uma lista, tendo a seguinte sintaxe:
```
del <var_list>[n]
```
O n-ésimo item da lista `<var_list>` será excluído. Para remover a lista `<var_list>` podemos utilizar a seguinte sintaxe:
```
del <var_list>
```
"""
guests = ['Matheus', 'Luana', 'Lucas', 'Alfredo', 'Jéssica']

print(guests)
del guests[1] # Exclui o segundo elemento da lista guests
print(guests)
del guests    # Exclui a lista guests
print(guests)
"""
## *Built-in Functions*
Existem 4 *built-in functions* que podem ser utilizadas em listas:
1. `type` -> utilizada para obter o tipo da lista (deve retornar `list`)
1. `len` -> utilizado para obter o tamanho da lista
1. `max` -> utilizado para obter o item de maior valor da lista
1. `min` -> utilizado para obter o item de menor valor da lista
"""
numbers = [ 3, 94,  2, 94, 72, 58, 72, 52]
print(type(numbers))
print(type(numbers) == list)
print(len(numbers))
print(max(numbers))
print(min(numbers))
"""
## Métodos
A lista é uma classe e, por isso, tem métodos. Alguns dos principais métodos da lista serão mostrados a seguir:
1. `append` -> Adiciona um item no final da lista. Recebe um parametro: o item que será adicionado. Retorna `None`;
1. `insert` -> Adiciona um item, em uma posição específica da lista. Recebe dois parametros: o primeiro parametro é o indice onde o item, do segundo parametro, será inserido na lista. Retorna `None`;
1. `remove` -> Remove o item de uma lista, utilizando o próprio item. Recebe um parametro: o item que será removido. Retorna `None`;
1. `pop` -> Remove um item, em uma posição específica da lista, se não for específicado a posição é removido o último item da lista. Recebe um parametro: o indice do item que será removido. Retorna o item foi removido;
1. `index` -> Retorna o indice de um item da lista. Recebe um parametro: o item que se deseja obter o indice. Retorna o indice do item passado como argumento;
1. `count` -> Conta a ocorrencia de um item em uma lista. Recebe um parametro: o item que se deseja contar as ocorrencias. Retorna a ocorrencia do item passado como argumento;
1. `sort` -> Organiza a lista em ordem crescente. Não recebe parametros. Retorna `None`;
1. `reverse` -> Inverte a ordem da lista. Não recebe parametros. Retorna `None` (Uma outra maneira de fazer esta operação e retornar uma nova lista é `<var_list>[::-1]`)
1. `copy` -> Cria uma nova lista (cópia) indenpedente da lista original (modificações na nova lista não afeta a lista original). Não recebe parametros. Retorna a nova lista criada;
1. `clear` -> Limpa uma lista, tornando-a uma lista vazia (sem itens, ou de tamanho zero). Não recebe parametros. Retorna `None`.
1. `extend` -> Concatena uma lista a outra lista. Recebe um parametro: a lista que vai ser concatenada. Retorna `None`.
"""
guests = ['Matheus', 'Luana', 'Lucas', 'Alfredo', 'Jéssica', 'Rodrigo', 'Ely', 'João']

guests.append('Carol')
print(f'convidados: {guests}')

guests.insert(1, 'Adriana')
print(f'convidados: {guests}')

guests.remove('Lucas')
print(f'convidados: {guests}')

name = guests.pop()
print(f'nome: {name}, convidados: {guests}')

name = guests.pop(3)
print(f'nome: {name}, convidados: {guests}')

index = guests.index('Luana')
print(f'indice: {index}')

guests[-1] = 'Alfredo'
times = guests.count('Alfredo')
print(f'vezes: {times}')

guests.sort()
print(f'convidados: {guests}')

guests.reverse()
print(f'convidados: {guests}')

other_guests = guests.copy()
print(f'outros convidados: {other_guests}, convidados: {guests}')

guests2 = guests
guests.clear()
print(f'outros convidados: {other_guests}, convidados 2: {guests2}, guests: {guests}')

guests.append('Franciele')
guests.append('Fabio')
print(guests)
guests.extend(other_guests)
print(f'outros convidados: {other_guests}, convidados: {guests}')
"""
## *Built-in function* map
No Python, temos a *built-in function* `map`, utilizada para transformar cada elemento da lista, de acordo com uma função definida pelo desenvolvedor. No exemplo abaixo, vamos calcular a raiz quadrada de cada elemento da lista `numbers`, utilizando a *built-in function* `map` e a função `math.sqrt`.

A *built-in function* `map` recebe dois parametros:
1. Uma função que recebe um parametro. Essa função vai ser mapeada em cada item da lista;
1. A lista onde a função será mapeada.
"""
import math

numbers = [ 3, 94,  2, 39, 72, 58, 75, 52]

square_roots = list(map(math.sqrt, numbers))
print(square_roots)
"""
## *Built-in function* filter
No Python, temos a *built-in function* `filter`, utilizada para filtrar os elementos da lista, de acordo com uma função definida pelo desenvolvedor. Esta função, chamada de filtro, deve receber um parametro. Este parametro representa um item da lista. Se esta função retornar `True`, então o elemento recebido permanece na lista. se a função retornar `False`, então o elemento é retirado da lista. É importante salientar que é gerada uma nova lista, com o filtro aplicado.
No exemplo abaixo, vamos filtrar os elementos ímpares da lista `numbers`, isto é, vamos manter o elementos pares. Para isso, vamos definir uma função filtro `is_even`, que verifica se o argumento é par, e vamos utilizar a *built-in function* `filter`.

A *built-in function* `filter` recebe dois parametros:
1. Uma função filtro, que recebe um parametro. Essa função vai ser aplicada em cada item da lista;
1. A lista onde a função filtro será aplicada.
"""
def is_even(x):
    return (x % 2) == 0

numbers = [ 3, 94,  2, 39, 72, 58, 75, 52]

even = list(filter(is_even, numbers))
print(even)
"""
# Tupla

As demais estruturas acima permitem que seja alterada, ou seja, é possível alterar os valores internos. Esta estrutura não permite alteração, imutável, dos itens após a construção. Ela é interessante para ser utilizadas em conjunto com set, pois eles necessitam que os dados sejam imutáveis e na chave de um dicionário.
"""
# Criação de tuplas
tlp1 = (82,'edge',2+1j, True)
tlp2 = (1,)
var1 = (1) # Não é uma tupla
tlp3 = tuple([1,2])

# Tamanho da tupla
len(tlp1)

# Acessar os valores a partir dos índeces
print(tlp1[2])
print(tlp1[-2])

# - Slice
print(tlp1[1:3])

# - Casting
list1 = list(tlp1)
set1 = set(tlp1)

# Juntar tuplas
print( tlp1 + tlp2 )
"""
# Conjunto (Set)

Um set ou conjunto é uma estrutura de dados que não permite a repetição de elementos.
"""
# Criação de set
set1 = set([1,2,3,2])
set2 = {1,2,3,2}
set3 = set()
var1 = {} # Não é um set

# Tamanho de um set
len(set1)

# Verificar se um item está no set
print( 1 in set1 )
print( 1 not in set1 )

# - Adição de item(s)
set1.add(6)
set2.update([5,4,7,10])
set1.union(set2) # Este método só funciona para set

# Remoção do primeiro item
set1.pop() # Remove o primeiro
set1.remove(2)
set1.clear() # Remove todos os itens
"""
# Dicionário

Essa estrutura tem o conceito chave - valor, ou seja, a partir de uma chave tem um valor/objeto. Por exemplo, na nossa sociedade temos o CPF que a partir desse número podemos identificar a pessoa (Nome,Data de Nascimento, ...).
"""
# Criação de um dicionário Create a dictionary
dic1 = {}
dic2 = dict()
dic3 = {
    1: 'abc',
    'a': 'd'
}
dic4 = {
	1: 1,
	1: 'a'
}
# Adição de uma nova chave
dic3[10] = '10'

# Tamanho
len(dic3)

# Acessar os valores a partir das chaves
print(dic3[1])

# Remoção de uma chave
del dic3[10]

# Verifica se a chave está no dicionário
print(1 in dic3)

# Pegar todas as chaves
print(dic3.keys())

# Pegar todas os valores
print(dic3.values())

# Pegar todas as chave/valor
print(dic3.items())