Python intermediário - II

Funções

O conceito de “função” é conhecido por nós como algo que produz uma saída (output) a partir de uma entrada (input). Matematicamente, podemos representar este processo como \(y= f(x)\). Por exemplo, uma impressora jato de tinta ao receber uma folha de papel em branco (\(x\)) aciona seus mecanismos de impressão (\(f\)) e produz uma folha impressa (\(y\)).

Em Python, funções são utilizadas praticamente da mesma forma, em que zero ou mais entradas retornam uma saída correspondente. Funções são fundamentais para organizar e reutilizar código, sempre que uma tarefa tenha de ser executada repetidamente. Toda função é um objeto de primeira classe. Isto essencialmente significa que ela pode ser: i) atribuída a uma variável ou elemento em uma estrutura de dado; ii) passada como argumento para outra função; iii) retornada como resultado de uma função.

Tipos de funções

Neste curso, separaremos as funções em três grupos:

  • predefinidas (built-in functions): aquelas pré-existentes no core da linguagem ou em outros módulos;

  • regulares (user-defined functions): aquelas definidas por você que possuem um nome definido (podem ser chamadas de UDFs);

  • anônimas (lambdas): funções, em geral, criadas por você que não exigem um nome.

Note

Funções lambda, a rigor, são caracterizadas como uma user-defined function e podem ser incorporadas juntamente com as funções nominadas por def em apenas um grupo. Apesar disso, aqui neste curso, tratamos funções anônimas em separado apenas para evitar subclassificações.

Funções predefinidas

Exploraremos exemplos com funções built-in do core Python.

Exemplos: funções predefinidas do core da linguagem.

# built-in functions
hex(1234),bin(345),round(12.3456,3)
('0x4d2', '0b101011001', 12.346)

Discussão:

  • hex converte um número para hexadecimal, indicado pelo prefixo 0x.

  • bin converte um número para binário, indicado pelo prefixo 0b.

  • round(x,n) arredonda um número x em n dígitos de precisão. Se n < 0, retorna 0.0.

# built-in function
for i in range(600,700,10):
    print(chr(i),end=',')
ɘ,ɢ,ɬ,ɶ,ʀ,ʊ,ʔ,ʞ,ʨ,ʲ,

Discussão:

  • chr retorna o caracter Unicode correspondente ao número inteiro passado, desde que esteja no intervalo [0,1114111].

Exemplo: somando números em uma lista.

sum(range(100)), sum([])
(4950, 0)

Discussão:

  • sum é uma função predefinida aplicável a sequências iteráveis.

  • Se o iterável for vazio, sum retorna zero.

  • No exemplo anterior, somamos os números de 0 a 100 e aplicamos a função a uma lista vazia.

Exemplo: número de elementos em iteráveis.

# no. de elementos
len([1,4,5])
3
# conta keys
len({'a':1,'b':2})
2
# lembre da unicidade
# 3 elementos no conjunto
len({1,3,1,2})
3
# conta caracteres
len('nome')
4

Funções regulares

Funções regulares, como dissemos, possuem um nome definido pelo usuário. Vejamos alguns exemplos.

Exemplos: funções regulares e uso de funções.

def cm_to_inch(x):
    """converte número de centímetros para polegadas"""
    return 2.45*x

Comentários:

  • Funções regulares são declaradas com a keyword def e valores de retorno com a keyword return.

# chamada simples
cm_to_inch(23)
56.35
# atribuindo em objeto
cmi = cm_to_inch
# usando como argumento de outra função
def fn(n,f):
    """Calcula f(n) dados n e uma função f."""
    return f(n)

fn(2,cm_to_inch)
4.9
# docstring da função
fn.__doc__
'Calcula f(n) dados n e uma função f.'

Parâmetros de funções podem assumir um argumento padrão que pode ser modificado sempre que necessário.

Exemplos:

# 'BEGIN' é valor padrão
def line(title='BEGIN'):
    print(title.center(20,'-'))
# especificação não necessária
line()
-------BEGIN--------
# alterando padrão
line('HEAD')
--------HEAD--------

Exemplo: função com argumentos posicionais.

# função com 2 argumentos posicionais
def upper_len(s,cut):
    if isinstance(s,str): # checa se é str
        print(':: ' + s.upper()[:cut],sep=',')
    else:
        pass # não faz nada
from random import randint

abc = ['alfa', 'bravO', 'chARlie', 230, 111.222]

# fatiamento aleatório
for s in abc:
    upper_len(s,randint(0,7)) 
:: ALF
:: BRAVO
:: CHARLI

Comentários:

  • Esta função aceita strings, formatam-nas em maiúsculas e imprime-as fatiadas até o índice cut, determinado aleatoriamente.

  • Note que nada é impresso para entradas que são números.

  • A função possui dois argumentos posicionais, isto é que obedecem às posições especificadas.

  • Se as posições dos argumentos forem alteradas, o sentido da função muda.

# Nada faz porque o primeiro 
# parâmetro deixou de ser str
for s in abc:
    upper_len(randint(1,3),s) 

Exemplo: especificando argumentos com keywords.

# declaração com 3 keywords
def triple(x=0,y=0,z=0):
    return x*y*z
# padrão
triple()
0
# sem declarar keywords
triple(2,3,4)
24
# declarando uma keyword
triple(3,4,z=4)
48
# erro!
triple(x=3,4,4)
  File "<ipython-input-23-e785690d4856>", line 2
    triple(x=3,4,4)
               ^
SyntaxError: positional argument follows keyword argument

Discussão:

  • Argumentos com keyword devem vir após os argumentos posicionais, se houver algum.

# declaração com 1 posicional e 2 keywords
def triple_2(x,y=0,z=0):
    return x*y*z
triple_2(2,y=5,z=3)
30
triple_2(2,5,z=3)
30
# declaração com 2 posicionais e 1 keyword
def triple_3(x,y,z=0):
    return x*y*z
triple_3(3,1,2)
6
triple_3(3,1,z=2)
6
Escopos

Funções podem acessar variáveis em dois escopos: global e local. O escopo global é aquele que está fora do escopo da função, enquanto o local é aquele determinado pela função.

Exemplo: atribuindo variáveis global e localmente

a = 0 # global
def test_vars(a):
    b = 1 # local    
    return a + b
    
print(test_vars(a))
print(b) # erro! b é local
1
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-4b9fde101a12> in <module>
      5 
      6 print(test_vars(a))
----> 7 print(b) # erro! b é local

NameError: name 'b' is not defined

Comentários:

  • A variável a é definida globalmente e foi utilizada como argumento de test_vars;

  • A variável b é definida localmente e, portanto, não pode ser acessada fora do escopo da função;

  • Um erro de “variável indefinida” é lançado por print(b), visto que o escopo global não reconhece a variável b.

def test_vars(a):
    global c     
    c = 1 # global
    return a + c
    
print(test_vars(a))
print(c) # ok! c é global
1
1

Comentários:

  • Neste caso, a c foi atribuído um valor no escopo local, porém ela foi declarada como global.

  • Para tornar uma variável dentro do escopo de uma função como global, usa-se a keyword global.

  • A impressão do valor de c no escopo global não retorna erro pela explicação anterior.

Warning

O uso indiscriminado de global não é encorajado. Quando a densidade do uso de global está alta, recomenda-se partir para uma abordagem de orientação a objetos e usar classes.

Funções anônimas

Uma função anônima em Python consiste em uma função cujo nome não é explicitamente definido e que pode ser criada em apenas uma linha de código para executar uma tarefa específica.

Funções anônimas são baseadas na palavra-chave lambda. Este nome tem inspiração em uma área da ciência da computação chamada de cálculo-\(\lambda\).

Uma função anônima tem a seguinte forma:

lambda lista_de_parâmetros: expressão

Embora funções anônimas possam ser chamadas isoladamente, seu melhor uso é como argumento de uma função.

Vejamos alguns exemplos.

Exemplo:

square = lambda x: x**2 
[square(x) for x in [2,3,4]]
[4, 9, 16]

Comentários:

  • square é um objeto function (verifique com type(square).

  • Esta função anônima eleva um número passado ao quadrado.

Exemplo:

def op_to_list(lista,f):
    return [f(x) for x in lista]

op_to_list([2,3,4],lambda x: x**2)
[4, 9, 16]

Discussão:

  • Esta construção produz um resultado equivalente ao anterior.

  • Neste exemplo, a função anônima é passada como argumento para ser aplicada à lista.

Comentários:

  • A construção anterior nos dá liberdade para criar funções com propósitos diversos.

Exemplo:

# f(x) = x**3 - 4
op_to_list([2,3,4], lambda x: x**3 - 4)
[4, 23, 60]

Exemplo:

# separa nome e sobrenome, 
# e cria um user
op_to_list(['Aldo Bermudes',
            'Vicário Sempernaum',
            'Sebastian Folcher'],
          lambda x: (x.split(' '),
                     x.lower().replace(' ','.')))
[(['Aldo', 'Bermudes'], 'aldo.bermudes'),
 (['Vicário', 'Sempernaum'], 'vicário.sempernaum'),
 (['Sebastian', 'Folcher'], 'sebastian.folcher')]

Comentários:

  • O abuso de lambdas pode ser prejudicial à legibilidade de código.

  • No exemplo acima, uma lista de nomes é quebrada em nome e sobrenome e um nome de usuário de e-mail hipotético é construído para cada elemento da lista.

Exemplo:

frutas = ['melão','melancia','abacate',
          'morango','romã','banana']

sorted(frutas,key=lambda x: x[::-1])
['melancia', 'banana', 'abacate', 'morango', 'melão', 'romã']

Discussão:

  • x[::-1]faz um swap na string;

  • A função lambda passada como key permite que os elementos da lista sejam ordenados com base na soletração invertida delas.

map

A função map serve para construir uma função que será aplicada a todos os elementos de uma sequência. Seu uso é da seguinte forma:

map(funcao,sequencia)

A função opt_to_list, criada na seção anterior, na verdade, poderia ter sido substituída por map, visto que map “aplica” uma regra a todos os elementos da sequência.

Exemplo:

list(map(lambda x: x**2,[2,3,4]))
[4, 9, 16]

A conversão de map para um list é importante, pois o uso solitário de map produzirá uma resposta abstrata.

map(lambda x: x**2,[2,3,4])
<map at 0x7f937d09e460>

Exemplo:

frutas = ['melão','melancia','abacate',
          'morango','romã','banana']

tuple(map(lambda x: x[::-1],frutas))
('oãlem', 'aicnalem', 'etacaba', 'ognarom', 'ãmor', 'ananab')

Comentários:

  • O map é utilizado para realizar um swap nas strings.

  • Em seguida, realizamos uma conversão do map em um tupla.

filter

Podemos aplicar uma espécie de “filtro” para valores usando a função filter. No caso anterior, digamos que valores acima de 7 sejam inseridos erroneamente no gerador de números (lembre-se que no sistema sanguíneo ABO, consideramos um dict cujo valor das chaves é no máximo 7). Podemos, ainda assim, filtrar a lista para coletar apenas valores menores do que 7. Para tanto, definimos uma função lambda com este propósito.

from random import randint 

nums = []
for _ in range(15):
    nums.append(randint(0,30))

f = filter(lambda x: x < 20,nums)
list(f)
[4, 18, 4, 19, 15, 9, 8, 6, 5]

Funções de ordem superior

A partir da introdução de compreensões de lista, map e filter passaram a ser menos importantes, embora ainda estejam disponíveis na linguagem. Juntamente com reduce e apply, map e filter são funções de ordem superior (aquelas que recebem funções como argumento ou retornam funções como resultado). Todas elas são menos frequentes em Python 3 devido à aparição de métodos mais “modernos” de realizar o mesmo.

args e kwargs

Imagine que, por alguma razão específica, você precise construir uma função com um número arbitrário de argumentos. Este tipo de situação ocorre quando o usuário tem ampla liberdade para configurar algo.

Vimos no capítulo anterior o desempacotamento por star expression. Para construir funções como as que queremos, podemos usar uma abordagem similar usando as star expressions *args e **kwargs.

Um resumo da aplicação delas é o seguinte:

  • *args é uma tupla e utilizada para substituir um número arbitrário de argumentos posicionais.

  • **kwargsé um dicionário e utilizada para substituir um número arbitrário de keywords e seus valores correspondentes.

A melhor forma de compreendê-las é por meio de exemplos.

Exemplo: função que aceita um número arbitrário de argumentos de entrada.

def media_arit(*vals):
    return sum(vals)/len(vals)

print(media_arit(1,2))
print(media_arit(1,2,4,5))
print(media_arit(1,2,4,5,7,9,10))
1.5
3.0
5.428571428571429

Discussão:

  • *vals recebe, em cada caso, os argumentos de entrada. No primeiro caso, 2; no segundo, 4; no terceiro, 7.

Exemplo: equação de combinação linear em \(\mathbb{R}^n\) com coeficientes arbitrários.

from IPython.display import display as dpl, Math

def comb_lin(*args):
    l = []    
    for i,c in enumerate(list(args)):
        l.append(str(c) + 'x_' + str(i+1))
        
    final = '$v = ' + ' + '.join(l) + '$'
    return final

v2 = comb_lin(1.4,4)
dpl(Math(v2))

v4 = comb_lin(2,34,12,65.43)
dpl(Math(v4))
\[\displaystyle v = 1.4x_1 + 4x_2\]
\[\displaystyle v = 2x_1 + 34x_2 + 12x_3 + 65.43x_4\]

Discussão:

  • A lista de coeficientes é percorrida com agregação dos termos \(x_i\), para cada coordenada do espaço n-dimensional.

  • dpl(Math(v4) renderiza a equação.

Exemplo: coleta de valores médios de áreas das superfícies radiculares de dentes humanos (em milímetros quadrados) por demanda.

# dente: (area inf, area sup)
# Ver: https://bit.ly/3iT9tsA, p.37, Quadro 2.1
#dentes = {'Incisivo central':(170,230),
#          'Incisivo lateral':(194,200),
#          'Canino':(270,282),
#          'Primeiro molar':(475,533)}


def med_area_dent(**kwargs):
    for k,v in kwargs.items():
        print(f'Dente: {k} | Área inf: {v[0]} mm2 | Área sup: {v[1]} mm2 ')

        
med_area_dent(incisivo_cent=(160,200),
              incisivo_lat=(194,200))        
Dente: incisivo_cent | Área inf: 160 mm2 | Área sup: 200 mm2 
Dente: incisivo_lat | Área inf: 194 mm2 | Área sup: 200 mm2 
med_area_dent(IC=(160,200),C=(270,882),PM=(475,533))        
Dente: IC | Área inf: 160 mm2 | Área sup: 200 mm2 
Dente: C | Área inf: 270 mm2 | Área sup: 882 mm2 
Dente: PM | Área inf: 475 mm2 | Área sup: 533 mm2 

Discussão:

  • **kwargs admite a entrada de um dicionário com tamanho variável. Isto é, chaves e valores quaisquer.

  • Nestes exemplos, imprimimos os valores médidos de áreas dos radiculares.

Comentários:

  • Cabe enfatizar que as keywords podem ter nomes diferentes.

any e all

As funções predefinidas any e all são chamadas de “redutoras”, visto que têm um papel relacionado à filtragem de dados. Elas são aplicáveis a qualquer sequência. Podemos entendê-las como segue:

  • any: retorna True se todo elemento do objeto iterável for avaliado com uma condição verdadeira.

  • all: retorna True se qualquer elemento do objeto iterável for avaliado com uma condição verdadeira.

Exemplo:

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

all(list(map(lambda x: x < 0,x)))
False
any(list(map(lambda x: x < 0,x)))
True
x = [2,1,3,4]

all(list(map(lambda x: x > 0,x)))
True

Exercícios aplicados

Exercício. A FEBRABAN (Federação Brasileira dos Bancos) utiliza um padrão de 52 caracteres (45 dígitos + 3 pontos + 4 espaços) para numeração de boletos bancários na forma

BBBML.LLLLC LLLLL.LLLLD LLLLL.LLLLE G FFFFVVVVVVVVVV

onde

  • B é dígito do código verificador de banco

  • M é dígito do código verificador de moeda

  • C é dígito do código identificador de campo

  • D é dígito do código identificador de campo

  • E é dígito do código identificador de campo

  • F é dígito do fator de vencimento

  • G é dígito do código verificador geral

  • V é dígito do valor do documento

Use essas informações para criar uma função que gera códigos de boleto hipotéticos e retorne o código do banco, tipo de moeda e valor do documento como múltiplas respostas. Assuma que L é sempre igual ao dígito zero.

Dica: use random.randint.