Mais sobre Funções

Intermediate

This tutorial is from open-source community. Access the source code

Introdução

Embora as funções tenham sido introduzidas anteriormente, foram fornecidos muito poucos detalhes sobre como elas realmente funcionam em um nível mais profundo. Esta seção visa preencher algumas lacunas e discutir questões como convenções de chamada (calling conventions), regras de escopo (scoping rules) e muito mais.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível intermediário com uma taxa de conclusão de 75%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Chamando uma Função

Considere esta função:

def read_prices(filename, debug):
    ...

Você pode chamar a função com argumentos posicionais:

prices = read_prices('prices.csv', True)

Ou você pode chamar a função com argumentos de palavra-chave (keyword arguments):

prices = read_prices(filename='prices.csv', debug=True)

Argumentos Padrão (Default Arguments)

Às vezes, você deseja que um argumento seja opcional. Se for o caso, atribua um valor padrão na definição da função.

def read_prices(filename, debug=False):
    ...

Se um valor padrão for atribuído, o argumento é opcional nas chamadas de função.

d = read_prices('prices.csv')
e = read_prices('prices.dat', True)

Nota: Argumentos com valores padrão devem aparecer no final da lista de argumentos (todos os argumentos não opcionais vêm primeiro).

Prefira argumentos de palavra-chave (keyword arguments) para argumentos opcionais

Compare e contraste estes dois estilos de chamada diferentes:

parse_data(data, False, True) ## ?????

parse_data(data, ignore_errors=True)
parse_data(data, debug=True)
parse_data(data, debug=True, ignore_errors=True)

Na maioria dos casos, os argumentos de palavra-chave melhoram a clareza do código – especialmente para argumentos que servem como flags ou que estão relacionados a recursos opcionais.

Melhores Práticas de Design (Design Best Practices)

Sempre dê nomes curtos, mas significativos, aos argumentos das funções.

Alguém que usa uma função pode querer usar o estilo de chamada por palavra-chave (keyword calling style).

d = read_prices('prices.csv', debug=True)

Ferramentas de desenvolvimento Python mostrarão os nomes em recursos de ajuda e documentação.

Retornando Valores (Returning Values)

A instrução return retorna um valor.

def square(x):
    return x * x

Se nenhum valor de retorno for fornecido ou return estiver ausente, None é retornado.

def bar(x):
    statements
    return

a = bar(4)      ## a = None

## OR
def foo(x):
    statements  ## No `return`

b = foo(4)      ## b = None

Múltiplos Valores de Retorno (Multiple Return Values)

Funções podem retornar apenas um valor. No entanto, uma função pode retornar múltiplos valores, retornando-os em uma tupla.

def divide(a,b):
    q = a // b      ## Quociente (Quotient)
    r = a % b       ## Resto (Remainder)
    return q, r     ## Retorna uma tupla (Return a tuple)

Exemplo de uso:

x, y = divide(37,5) ## x = 7, y = 2

x = divide(37, 5)   ## x = (7, 2)

Escopo de Variável (Variable Scope)

Programas atribuem valores a variáveis.

x = value ## Variável global (Global variable)

def foo():
    y = value ## Variável local (Local variable)

Atribuições de variáveis ocorrem fora e dentro de definições de funções. Variáveis definidas fora são "globais". Variáveis dentro de uma função são "locais".

Variáveis Locais (Local Variables)

Variáveis atribuídas dentro de funções são privadas.

def read_portfolio(filename):
    portfolio = []
    for line in open(filename):
        fields = line.split(',')
        s = (fields[0], int(fields[1]), float(fields[2]))
        portfolio.append(s)
    return portfolio

Neste exemplo, filename, portfolio, line, fields e s são variáveis locais. Essas variáveis não são retidas ou acessíveis após a chamada da função.

>>> stocks = read_portfolio('portfolio.csv')
>>> fields
Traceback (most recent call last):
File "<stdin>", line 1, in ?
NameError: name 'fields' is not defined
>>>

Variáveis locais também não podem entrar em conflito com variáveis encontradas em outros lugares.

Variáveis Globais (Global Variables)

Funções podem acessar livremente os valores de variáveis globais definidas no mesmo arquivo.

name = 'Dave'

def greeting():
    print('Hello', name)  ## Usando a variável global `name`

No entanto, funções não podem modificar variáveis globais:

name = 'Dave'

def spam():
  name = 'Guido'

spam()
print(name) ## imprime 'Dave'

Lembre-se: Todas as atribuições em funções são locais.

Modificando Globais (Globals)

Se você precisar modificar uma variável global, você deve declará-la como tal.

name = 'Dave'

def spam():
    global name
    name = 'Guido' ## Altera a variável global name acima

A declaração global deve aparecer antes de seu uso e a variável correspondente deve existir no mesmo arquivo que a função. Tendo visto isso, saiba que é considerado uma má prática. Na verdade, tente evitar global completamente, se possível. Se você precisar que uma função modifique algum tipo de estado fora da função, é melhor usar uma classe (mais sobre isso mais tarde).

Passagem de Argumentos (Argument Passing)

Quando você chama uma função, as variáveis de argumento são nomes que se referem aos valores passados. Esses valores NÃO são cópias. Se tipos de dados mutáveis são passados (por exemplo, listas, dicionários), eles podem ser modificados in-place (no local).

def foo(items):
    items.append(42)    ## Modifica o objeto de entrada

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

Ponto chave: Funções não recebem uma cópia dos argumentos de entrada.

Reatribuição (Reassignment) vs. Modificação (Modifying)

Certifique-se de entender a sutil diferença entre modificar um valor e reatribuir um nome de variável.

def foo(items):
    items.append(42)    ## Modifica o objeto de entrada

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

## VS
def bar(items):
    items = [4,5,6]    ## Altera a variável local `items` para apontar para um objeto diferente

b = [1, 2, 3]
bar(b)
print(b)                ## [1, 2, 3]

Lembrete: A atribuição de variáveis nunca sobrescreve a memória. O nome é meramente vinculado a um novo valor.

Este conjunto de exercícios fará com que você implemente o que é, talvez, a parte mais poderosa e difícil do curso. Há muitos passos e muitos conceitos de exercícios anteriores são reunidos de uma só vez. A solução final tem apenas cerca de 25 linhas de código, mas reserve um tempo e certifique-se de entender cada parte.

Uma parte central do seu programa report.py se concentra na leitura de arquivos CSV. Por exemplo, a função read_portfolio() lê um arquivo contendo linhas de dados de portfólio e a função read_prices() lê um arquivo contendo linhas de dados de preços. Em ambas as funções, há muitos detalhes de baixo nível e recursos semelhantes. Por exemplo, ambas abrem um arquivo e o envolvem com o módulo csv e ambas convertem vários campos em novos tipos.

Se você estivesse fazendo muita análise de arquivos na vida real, provavelmente gostaria de limpar um pouco disso e torná-lo mais genérico. Esse é o nosso objetivo.

Comece este exercício abrindo o arquivo chamado fileparse.py. É aqui que faremos nosso trabalho.

Exercício 3.3: Lendo Arquivos CSV

Para começar, vamos apenas focar no problema de ler um arquivo CSV em uma lista de dicionários. No arquivo fileparse_3.3.py, defina uma função que se parece com isto:

## fileparse_3.3.py
import csv

def parse_csv(filename):
    '''
    Parse a CSV file into a list of records
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## Read the file headers
        headers = next(rows)
        records = []
        for row in rows:
            if not row:    ## Skip rows with no data
                continue
            record = dict(zip(headers, row))
            records.append(record)

    return records

Esta função lê um arquivo CSV em uma lista de dicionários, enquanto esconde os detalhes de abertura do arquivo, envolvendo-o com o módulo csv, ignorando linhas em branco e assim por diante.

Experimente:

Dica: python3 -i fileparse_3.3.py.

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA', 'shares': '100', 'price': '32.20'}, {'name': 'IBM', 'shares': '50', 'price': '91.10'}, {'name': 'CAT', 'shares': '150', 'price': '83.44'}, {'name': 'MSFT', 'shares': '200', 'price': '51.23'}, {'name': 'GE', 'shares': '95', 'price': '40.37'}, {'name': 'MSFT', 'shares': '50', 'price': '65.10'}, {'name': 'IBM', 'shares': '100', 'price': '70.44'}]
>>>

Isso é bom, exceto que você não pode fazer nenhum tipo de cálculo útil com os dados porque tudo é representado como uma string. Vamos corrigir isso em breve, mas vamos continuar construindo sobre isso.

Exercício 3.4: Construindo um Seletor de Colunas

Em muitos casos, você está interessado apenas em colunas selecionadas de um arquivo CSV, não em todos os dados. Modifique a função parse_csv() para que ela permita opcionalmente que colunas especificadas pelo usuário sejam selecionadas da seguinte forma:

>>> ## Read all of the data
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA', 'shares': '100', 'price': '32.20'}, {'name': 'IBM', 'shares': '50', 'price': '91.10'}, {'name': 'CAT', 'shares': '150', 'price': '83.44'}, {'name': 'MSFT', 'shares': '200', 'price': '51.23'}, {'name': 'GE', 'shares': '95', 'price': '40.37'}, {'name': 'MSFT', 'shares': '50', 'price': '65.10'}, {'name': 'IBM', 'shares': '100', 'price': '70.44'}]

>>> ## Read only some of the data
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'])
>>> shares_held
[{'name': 'AA', 'shares': '100'}, {'name': 'IBM', 'shares': '50'}, {'name': 'CAT', 'shares': '150'}, {'name': 'MSFT', 'shares': '200'}, {'name': 'GE', 'shares': '95'}, {'name': 'MSFT', 'shares': '50'}, {'name': 'IBM', 'shares': '100'}]
>>>

Um exemplo de um seletor de coluna foi dado no Exercício 2.23. No entanto, aqui está uma maneira de fazê-lo:

## fileparse_3.4.py
import csv

def parse_csv(filename, select=None):
    '''
    Parse a CSV file into a list of records
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## Read the file headers
        headers = next(rows)

        ## If a column selector was given, find indices of the specified columns.
        ## Also narrow the set of headers used for resulting dictionaries
        if select:
            indices = [headers.index(colname) for colname in select]
            headers = select
        else:
            indices = []

        records = []
        for row in rows:
            if not row:    ## Skip rows with no data
                continue
            ## Filter the row if specific columns were selected
            if indices:
                row = [ row[index] for index in indices ]

            ## Make a dictionary
            record = dict(zip(headers, row))
            records.append(record)

    return records

Há uma série de partes complicadas nesta parte. Provavelmente a mais importante é o mapeamento das seleções de coluna para os índices de linha. Por exemplo, suponha que o arquivo de entrada tenha os seguintes cabeçalhos:

>>> headers = ['name', 'date', 'time', 'shares', 'price']
>>>

Agora, suponha que as colunas selecionadas fossem as seguintes:

>>> select = ['name', 'shares']
>>>

Para realizar a seleção adequada, você precisa mapear os nomes das colunas selecionadas para os índices das colunas no arquivo. É isso que esta etapa está fazendo:

>>> indices = [headers.index(colname) for colname in select ]
>>> indices
[0, 3]
>>>

Em outras palavras, "name" é a coluna 0 e "shares" é a coluna 3. Quando você lê uma linha de dados do arquivo, os índices são usados para filtrá-la:

>>> row = ['AA', '6/11/2007', '9:50am', '100', '32.20' ]
>>> row = [ row[index] for index in indices ]
>>> row
['AA', '100']
>>>

Exercício 3.5: Realizando Conversão de Tipos

Modifique a função parse_csv() no diretório /home/labex/project/fileparse_3.5.py para que ela permita opcionalmente que conversões de tipos sejam aplicadas aos dados retornados. Por exemplo:

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'shares': 200, 'price': 51.23}, {'name': 'GE', 'shares': 95, 'price': 40.37}, {'name': 'MSFT', 'shares': 50, 'price': 65.1}, {'name': 'IBM', 'shares': 100, 'price': 70.44}]

>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name', 'shares'], types=[str, int])
>>> shares_held
[{'name': 'AA', 'shares': 100}, {'name': 'IBM', 'shares': 50}, {'name': 'CAT', 'shares': 150}, {'name': 'MSFT', 'shares': 200}, {'name': 'GE', 'shares': 95}, {'name': 'MSFT', 'shares': 50}, {'name': 'IBM', 'shares': 100}]
>>>

Você já explorou isso no Exercício 2.24. Você precisará inserir o seguinte fragmento de código em sua solução:

...
if types:
    row = [func(val) for func, val in zip(types, row) ]
...

Exercício 3.6: Trabalhando sem Cabeçalhos

Alguns arquivos CSV não incluem nenhuma informação de cabeçalho. Por exemplo, o arquivo prices.csv se parece com isto:

"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...

Modifique a função parse_csv() em /home/labex/project/fileparse_3.6.py para que ela possa trabalhar com esses arquivos, criando uma lista de tuplas em vez disso. Por exemplo:

>>> prices = parse_csv('/home/labex/project/prices.csv', types=[str,float], has_headers=False)
>>> prices
[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ('BAC', 11.27), ('C', 3.72), ('CAT', 35.46), ('CVX', 66.67), ('DD', 28.47), ('DIS', 24.22), ('GE', 13.48), ('GM', 0.75), ('HD', 23.16), ('HPQ', 34.35), ('IBM', 106.28), ('INTC', 15.72), ('JNJ', 55.16), ('JPM', 36.9), ('KFT', 26.11), ('KO', 49.16), ('MCD', 58.99), ('MMM', 57.1), ('MRK', 27.58), ('MSFT', 20.89), ('PFE', 15.19), ('PG', 51.94), ('T', 24.79), ('UTX', 52.61), ('VZ', 29.26), ('WMT', 49.74), ('XOM', 69.35)]
>>>

Para fazer essa alteração, você precisará modificar o código para que a primeira linha de dados não seja interpretada como uma linha de cabeçalho. Além disso, você precisará garantir que não crie dicionários, pois não haverá mais nomes de colunas para usar como chaves.

Exercício 3.7: Escolhendo um delimitador de coluna diferente

Embora os arquivos CSV sejam bastante comuns, também é possível que você encontre um arquivo que use um separador de coluna diferente, como uma tabulação ou espaço. Por exemplo, o arquivo portfolio.dat se parece com isto:

name shares price
"AA" 100 32.20
"IBM" 50 91.10
"CAT" 150 83.44
"MSFT" 200 51.23
"GE" 95 40.37
"MSFT" 50 65.10
"IBM" 100 70.44

A função csv.reader() permite que um delimitador de coluna diferente seja fornecido da seguinte forma:

rows = csv.reader(f, delimiter=' ')

Modifique sua função parse_csv() em /home/labex/project/fileparse_3.7.py para que ela também permita que o delimitador seja alterado.

Por exemplo:

>>> portfolio = parse_csv('/home/labex/project/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'shares': 200, 'price': 51.23}, {'name': 'GE', 'shares': 95, 'price': 40.37}, {'name': 'MSFT', 'shares': 50, 'price': 65.1}, {'name': 'IBM', 'shares': 100, 'price': 70.44}]
>>>

Comentário

Se você chegou até aqui, você criou uma boa função de biblioteca que é genuinamente útil. Você pode usá-la para analisar arquivos CSV arbitrários, selecionar colunas de interesse, realizar conversões de tipo, sem ter que se preocupar muito com o funcionamento interno de arquivos ou do módulo csv.

Resumo

Parabéns! Você concluiu o laboratório "Mais sobre Funções". Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.