#!/usr/bin/env python
# coding: utf-8
# ##
Introdução à Lógica de Programação
# ### Um curso prático para estudantes das Ciências da Vida
# ---
# ## Aula 2. Funções
# ##### Instrutor: Pedro C. de Siracusa
# Funções são como liquidificadores! Inserimos bananas e um copo de leite, ajustamos alguns parâmetros como o tempo de processamento e por fim recebemos uma deliciosa vitamina! E o melhor é que não precisamos entender os mínimos detalhes sobre como os mecanismos internos do aparelho funcionam para fazer nossa vitamina. Tampouco precisamos ter construído o liquidificador para poder saboreá-la. Além disso, qualquer que fosse o liquidificador, esperaríamos obter algo não muito diferente de uma vitamina, desde que utilizássemos os mesmos ingredientes e regulagens.
#
#
# Agora de forma mais sofisticada, funções são construções que **encapsulam** um determinado comportamento que se espera executar múltiplas vezes durante a execução de um programa. São rotinas, que podem ter sido definidas pelo próprio programador ou por outros programadores. No caso do liquidificador, a rotina foi definida pelo próprio fabricante. O usuário só precisa saber o que precisa saber como operá-lo: que tipos de coisas deve fornecer como **entrada** (*input*) e o que deve esperar receber como **saída** (*output*).
#
#
# Funções facilitam sua vida por dois motivos principais.
#
# 1. Permitem ao programador **abstrair** computações, sem precisar se preocupar a todo momento sobre os mínimos detalhes de como elas são de fato realizadas. Imagine se você tivesse que se preocupar com os detalhes sobre como um texto é imprimido na tela de seu computador toda vez que você precisasse desta funcionalidade... Felizmente a função `print`, que utilizamos no nosso programa "Hello World", permite que esta rotina seja abstraída para você, o que facilita bastante seu aprendizado!
#
# 2. Permitem **compartilhar** e **reutilizar** código. Se você construir uma função que possa ajudar outras pessoas também, por que não compartilhar? Isso acontece bastante na comunidade de programadores, e os ajuda a não ficar "reinventando a roda" quando precisam de alguma funcionalidade que já foi implementada por alguém.
# Para começar a entender como trabalhar com funções, precisamos conhecer seus três componentes principais: *(i)* **nome**, *(ii)* **parâmetros** e *(iii)* **corpo**.
#
# Dar um **nome** às funções é uma forma simples de mantermos uma referência a elas. Embora possamos nomear funções conforme nossa vontade, é recomendável escolhermos nomes que nos digam algo sobre seu funcionamento. É fácil lembrar que a função `print`, por exemplo, serve para imprimir algo na tela.
#
# Os **parâmetros** fornecem um meio para "afinarmos" o comportamento de uma função para nossas necessidades. Por exemplo, no caso do liquidificador, alguns parâmetros relevantes seriam o tempo de processamento, ou a velocidade de rotação do motor. Os dados de entrada (*inputs*) são também passados para as funções através de parâmetros. No jargão da programação, nos referimos aos valores que passamos no lugar de cada um dos parâmetros como **argumentos**. Pense em um parâmetro como um *placeholder* para um valor, enquanto o argumento é o valor em si, passado para dentro da função através de um parâmetro.
#
# Por fim, no **corpo** da função especificamos todas as etapas que devem ser realizadas por ela. Estas etapas incluem o processamento dos dados de entrada (*inputs*) e a construção do resultado que ela produzirá como saída (*output*). No fim das contas, a ideia é que as computações descritas no corpo da função sejam abstraídas para o usuário da função.
# ### Objetivos.
#
# Após esta aula você deverá ser capaz de:
# * **Definir** e **executar** funções;
# * Delimitar o **escopo** de uma função;
# * Reutilizar funcionalidades distribuídas em **pacotes** pela comunidade *Python*.
# ## 1. Definindo uma nova função
# Para definir uma nova função precisamos obedecer à seguinte sintaxe:
# * a palavra `def` indica que uma nova função está sendo definida;
# * após `def`, deve ser escrito o **nome** da função;
# * após o nome da função, entre parênteses, os **parâmetros** são separados por vírgulas;
# * Os dois pontos `:` após os parênteses indica que o **corpo** da função vem a seguir, no bloco de código abaixo;
# * O bloco de código deve ser escrito com uma **indentação**, o distanciamento em relação à margem esquerda da célula;
# * No fim do corpo da função, o valor de saída (*output*) é indicado após a palavra `return`.
# Para ilustrar, vamos construir uma nova função, batizada com o nome `foo` (poderia ser qualquer outro nome, tipo `dinossauro`). Ela simplesmente deve imprimir no console os valores que passamos em cada um dos parâmetros e, no fim, retornar como *output* o valor $1$.
# In[1]:
def foo(par1, par2):
print(par1)
print(par2)
return 1
# Uma vez definida, a função fica guardada na memória, mas não é automaticamente executada. Para de fato executá-la, precisamos **chamá-la**, escrevendo seu nome e passando argumentos no lugar dos parâmetros, entre parênteses.
# In[2]:
foo("Santos", "Dumont")
# Vamos definir agora uma função chamada `funcaoSoma`, que simplesmente soma dois números e retorna o resultado. Ela possui os parâmetros `n1` e `n2`, que esperam receber como valores (ou argumentos) dois valores numéricos.
# In[3]:
def funcaoSoma(n1,n2):
res = n1 + n2
return res
# In[4]:
funcaoSoma(3,5)
# In[5]:
funcaoSoma(-3,23)
# In[6]:
funcaoSoma(42,0)
# Podemos também armazenar o resultado de funções em variáveis! Vamos declarar as variáveis `a`, `b`, `c` e `d`, com quaisquer valores numéricos. Em seguida, usaremos a função `funcaoSoma` para somar `a` e `b` e armazene o resultado em uma variável `a_b`. Depois, usaremos novamente a função `funcaoSoma` para somar `c` e `d` e armazene o resultado em uma variável `c_d`. Finalmente, somaremos os valores em `a_b` e `c_d` usando a mesma `funcaoSoma`, armazenando o resultado final em uma variável `resFinal`.
# In[7]:
a = 3
b = 5
c = 9
d = 11
a_b = funcaoSoma(a,b)
c_d = funcaoSoma(c,d)
resFinal = funcaoSoma( a_b, c_d )
resFinal
# Podemos também passar resultados de funções como argumentos para outras funções, sem precisar utilizar variáveis intermediárias! Podemos, por exemplo, realizar as mesmas computações da célula acima sem precisar declarar `a_b` e `c_d`.
# In[8]:
a = 3
b = 5
c = 9
d = 11
resFinal = funcaoSoma( funcaoSoma(a,b), funcaoSoma(c,d) )
resFinal
# ## 2. Escopo de funções
# Funções operam em um "contexto" próprio.
# É como se criássemos uma "bolha" toda vez que executamos uma função. Tudo o que está dentro desta bolha não é visível de fora dela, mas todo o conteúdo que está fora dela é visível de dentro.
# Nos referimos ao contexto de "dentro da bolha" como o **escopo local** da função, e tudo o que está fora como o **escopo global**.
#
# Sendo assim, uma funçao "enxerga" todas as variáveis em escopo local, ou seja, aquelas que são definidas dentro dela própria (incluindo os parâmetros); e as variáveis em escopo global, definidas fora dela.
# No entanto, variáveis locais, definidas dentro de funções, não são visíveis de fora delas!
# In[9]:
a = 3
print("Variável a ANTES da execução da função:",a)
b = 5
print("Variável b ANTES da execução da função:",b)
def foo(): # A função é apenas definida aqui
a = 7
print("Variável a DENTRO da função foo:",a)
print("Variável b DENTRO da função foo:",b)
m = 11
print("Variável m DENTRO da função foo:",m)
print('---')
foo() # A função apenas é de fato executada aqui
print('---')
print("Variável a APÓS a execução da função",a)
print("Variável b APÓS da execução da função:",b)
print("Variável m",m)
# **Obs:** Um mesmo nome pode ser dado para variáveis em escopos diferentes (que podem conter valores diferentes)! Nestes casos, a variável que foi definida em escopo local prevalece sobre a global.
# ## 3. Reutilizando funcionalidades
# O segredo para um bom desempenho como programador é jamais reinventar a roda! Sempre que possível, devemos buscar reutilizar código que já foi escrito por outros programadores.
# **Pacotes** são uma forma eficiente usada pelos programadores para estruturar e compartilhar seu código com a comunidade, sendo criados com o intuito de entregar aos usuários um conjunto de funcionalidades.
# O código dentro de um pacote é organizado em **módulos**, cada qual contendo um conjunto de funções e variáveis que implementam funcionalidades mais específicas.
#
#
# Veja uma lista de pacotes potencialmente interessantes para um biólogo:
# * [Biopython](http://biopython.org/) com funcionalidades específicas para aplicações em biologia molecular computacional, na **bioinformática**;
# * [Numpy](http://www.numpy.org/) para computação científica, com representações e operações otimizadas sobre objetos matemáticos;
# * [Pandas](http://pandas.pydata.org/) para representação de *Data frames* e análise de dados;
# * [Matplotlib](https://matplotlib.org/) para construção de figuras (visualização de dados);
# * [Statsmodels](http://www.statsmodels.org/stable/index.html) para trabalhar com modelos estatísticos;
# * [ScikitLearn](http://scikit-learn.org/stable/) para trabalhar com algoritmos de aprendizado de máquina;
# * [Networkx](https://networkx.github.io/) para construir modelos em redes complexas;
# * [ArcPy](http://desktop.arcgis.com/en/arcmap/10.3/analyze/arcpy/what-is-arcpy-.htm) é um pacote para automação de tarefas de análise e geração de mapas em ArcGis;
# * [Matplotlib Basemap](https://matplotlib.org/basemap/) é um conjunto de ferramentas para plotar mapas em python, utilizando como base a *Matplotlib*;
# * [Pygbif](http://pygbif.readthedocs.io/en/latest/) é um cliente *Python* que facilita o acesso a dados de ocorrência de espécies no [GBIF](https://www.gbif.org/) (Global Biodiversity Information Facility).
# Em *Python*, existem algumas funções que são carregadas com a inicialização do interpretador, por serem utilizadas de forma mais ampla pelos programadores. Alguns exemplos são as funções `print`, `type`, `range` e `open`.
# Outras, no entanto, são mais específicas e portanto não são carregadas automaticamente.
# Usamos a palavra `import` para carregar um pacote ou módulo.
# O módulo `math`, por exemplo, reúne um conjunto de funções e constantes que facilitam muito trabalhar com elementos da matemática. Estas funções não são carregadas automaticamente, e portanto devemos **importar o módulo** para utilizá-las.
# Vamos carregar o módulo `math` e, em seguida, executar a função `factorial` provida por ele. Conforme podemos consultar na documentação, esta função espera um número como *input* e retorna seu fatorial, como *output*.
# In[10]:
import math
print("Fatorial de 1:",math.factorial(1))
print("Fatorial de 2:",math.factorial(2))
print("Fatorial de 3:",math.factorial(3))
print("Fatorial de 10:",math.factorial(10))
# ---
# ## Exercícios
# **Ex 1.** Defina uma função `calculaMedia`, que calcula a média entre **três** números quaisquer. Em seguida, execute sua função com valores diferentes. O que acontece se você definir a função com três parâmetros mas passar um número diferente de argumentos?
# In[11]:
def calculaMedia(n1,n2,n3):
res = (n1+n2+n3)/3
return res
# In[12]:
calculaMedia(1,3,5)
# In[13]:
calculaMedia(1,2,5)
# In[14]:
# Este código vai gerar um erro: A função esperava três argumentos mas nós só passamos 2
calculaMedia(1,5)
# **Ex 2.** Defina uma função `multiplica`, que multiplica dois números quaisquer. Em seguida, declare as variáveis `a`, `b` e `c` com quaisquer valores numéricos. Multiplique `a` e `b` utilizando a função `multiplica` e, em seguida, execute novamente a função `multiplica` para multiplicar o resultado do passo anterior pelo número em `c`.
# In[15]:
def multiplica(n1,n2):
return n1*n2
# In[16]:
a=3
b=5
c=7
multiplica( multiplica(a,b), c)
# **Ex 3.** A função `exponencia` abaixo deve realizar uma exponenciação do número `n1` à potência `n2`. Mas ao executar a célula para definir a função recebemos uma mensagem de erro. Corrija o código e experimente executar a função com alguns pares de números.
# In[17]:
def exponencia(n1,n2):
return n1**n2
# In[18]:
exponencia(2,3)
# In[19]:
exponencia(5,2)
# **Ex 4.** A função `mensagemMeuNome` abaixo deveria receber como argumento o seu nome, imprimir uma mensagem com ele e, por fim, retornar o seu nome como *output*. No entanto, existe um *bug*. Você consegue identificá-lo e corrigí-lo?
# In[20]:
nome = "Luke Skywalker"
def mensagemMeuNome( meuNome ):
print("Olá! Me chamo", meuNome)
return meuNome
mensagemMeuNome("Pedro") # insira seu nome entre parênteses
# **Ex 5.** A função `type` é automaticamente carregada com a inicialização do interpretador *Python*. Você consegue descobrir o que ela faz? Experimente passar diferentes tipos de dados.
# In[21]:
# A função type retorna o tipo do valor passado como argumento
print("Tipo de 'Algum texto':", type("Algum texto") )
print( "Tipo de 43:",type(43) )
print( "Tipo de True:", type(True) )
print( "Tipo de 32.21:", type(32.21) )