Eventualmente observo dificuldades de algumas pessoas em usar corretamente alguma função, seja porque a função deveria ser executada isoladamente, e utilizado a própria variável que foi passada como argumento posteriormente, seja porque deveria se atribuir o retorno da função a alguma variável, e utilizar essa nova variável. No Python, essa diferença pode ser observada nos métodos das listas sort
e reverse
para as funções sorted
e reversed
, que são implementadas com padrões diferentes, in place e cópia de valor respectivamente. Assim pretendo discutir esses dois padrões de funções, comentando qual a diferença e o melhor caso de aplicação de cada padrão.
Função de exemplo
Para demonstrar como esses padrões funcionam, será implementado uma função que recebe uma lista e calcula o dobro dos valores dessa lista. Exemplo:
entrada = [5, 2, 8, 6, 4]
# Execução da função
resultado = [10, 4, 16, 12, 8]
Função com in place
A ideia do padrão in place é alterar a própria variável recebida pela função (ou o próprio objeto, caso esteja lidando com orientação a objetos). Neste caso, bastaria calcular o dobro do valor de cada posição da lista, e sobrescrever a posição com seu resultado. Exemplo:
from typing import List
def dobro_inplace(lista: List[int]) -> None:
for i in range(len(lista)):
lista[i] = 2 * lista[i]
valores = [5, 2, 8, 6, 4]
retorno = dobro_inplace(valores)
print(f'Variável: valores | Tipo: {type(valores)} | Valor: {valores}')
print(f'Variável: retorno | Tipo: {type(retorno)} | Valor: {retorno}')
Resultado da execução:
Variável: valores | Tipo: <class 'list'> | Valor: [10, 4, 16, 12, 8]
Variável: retorno | Tipo: <class 'NoneType'> | Valor: None
Com essa execução é possível observar que os valores da lista foram alterados, e que o retorno da função é nulo (None
), ou seja, a função alterou a própria lista passada como argumento. Outro ponto importante a ser observado é a assinatura da função (tipo dos argumentos e do retorno da função), que recebe uma lista de inteiros e não tem retorno ou é nulo (None
). Dessa forma embora seja possível chamar essa função diretamente quando está se informando os argumentos de outra função, como print(dobro_inplace(valores))
, a função print
receberia None
e não a lista como argumento.
Função com cópia de valor
A ideia do padrão cópia de valor é criar uma cópia do valor passado como argumento e retornar essa cópia, sem alterar a variável recebida (ou criando um novo objeto, no caso de orientação a objetos). Neste caso, é necessário criar uma nova lista e adicionar nela os valores calculados. Exemplo:
from typing import List
def dobro_copia(lista: List[int]) -> List[int]:
nova_lista = []
for i in range(len(lista)):
nova_lista.append(2 * lista[i])
return nova_lista
valores = [5, 2, 8, 6, 4]
retorno = dobro_copia(valores)
print(f'Variável: valores | Tipo: {type(valores)} | Valor: {valores}')
print(f'Variável: retorno | Tipo: {type(retorno)} | Valor: {retorno}')
Resultado da execução:
Variável: valores | Tipo: <class 'list'> | Valor: [5, 2, 8, 6, 4]
Variável: retorno | Tipo: <class 'list'> | Valor: [10, 4, 16, 12, 8]
Com essa execução é possível observar que a variável valores
continua com os valores que tinha antes da execução da função, e a variável retorno apresenta uma lista com os dobros, ou seja, a função não altera a lista passada como argumento e retorna uma nova lista com os valores calculados. Observado a assinatura da função, ela recebe uma lista de inteiros e retorna uma lista de inteiros. Isso permite chamar essa função diretamente nos argumentos para outra função, como print(dobro_copia(valores))
, nesse caso a função print
receberia a lista de dobros como argumento. Porém caso o retorno da função não seja armazenado, parecerá que a função não fez nada, ou não funcionou. Então em alguns casos, quando o valor anterior não é mais necessário, pode-se reatribuir o retorno da função a própria variável passada como argumento:
valores = dobro_copia(valores)
Função híbrida
Ainda é possível mesclar os dois padrões de função, alterando o valor passado e retornando-o. Exemplo:
from typing import List
def dobro_hibrido(lista: List[int]) -> List[int]:
for i in range(len(lista)):
lista[i] = 2 * lista[i]
return lista
valores = [5, 2, 8, 6, 4]
retorno = dobro_hibrido(valores)
print(f'Variável: valores | Tipo: {type(valores)} | Valor: {valores}')
print(f'Variável: retorno | Tipo: {type(retorno)} | Valor: {retorno}')
Resultado da execução:
Variável: valores | Tipo: <class 'list'> | Valor: [10, 4, 16, 12, 8]
Variável: retorno | Tipo: <class 'list'> | Valor: [10, 4, 16, 12, 8]
Nesse caso, pode-se apenas chamar a função, como também utilizá-la nos argumentos de outras funções. Porém para se ter os valores originais, deve-se fazer uma cópia manualmente antes de executar a função.
Exemplo na biblioteca padrão
Na biblioteca padrão do Python, existem os métodos sort
e reverse
que seguem o padrão in place, e as funções sorted
e reversed
que seguem o padrão cópia de valor, podendo ser utilizados para ordenar e inverter os valores de uma lista, por exemplo. Quando não é mais necessário uma cópia da lista com a ordem original, é preferível utilizar funções in place, que alteram a própria lista, e como não criam uma cópia da lista, utilizam menos memória. Exemplo:
valores = [5, 2, 8, 6, 4]
valores.sort()
valores.reverse()
print(valores)
Se for necessário manter uma cópia da lista inalterada, deve-se optar pelas funções de cópia de valor. Exemplo:
valores = [5, 2, 8, 6, 4]
novos_valores = reversed(sorted(valores))
print(novos_valores)
Porém esse exemplo cria duas cópias da lista, uma em cada função. Para criar apenas uma cópia, pode-se misturar funções in place com cópia de valor. Exemplo:
valores = [5, 2, 8, 6, 4]
novos_valores = sorted(valores)
novos_valores.reverse()
print(novos_valores)
Também vale observar que algumas utilizações dessas funções podem dar a impressão de que elas não funcionaram, como:
valores = [5, 2, 8, 6, 4]
sorted(valores)
print(valores) # Imprime a lista original, e não a ordenada
print(valores.sort()) # Imprime None e não a lista
Considerações
Nem sempre é possível utilizar o padrão desejado, strings no Python (str
) são imutáveis, logo todas as funções que manipulam elas seguiram o padrão cópia de valor, e para outros tipos, pode ocorrer de só existir funções in place, sendo necessário fazer uma cópia manualmente antes de chamar a função, caso necessário. Para saber qual padrão a função implementa, é necessário consultar sua documentação, ou verificando sua assinatura, embora ainda possa existir uma dúvida entre cópia de valor e híbrida, visto que a assinatura dos dois padrões são iguais.
Os exemplos aqui dados são didáticos. Caso deseja-se ordenar de forma reversa, tanto o método sort
, quanto a função sorted
podem receber como argumento reverse=True
, e assim já fazer a ordenação reversa. Assim como é possível criar uma nova lista já com os valores, sem precisar adicionar manualmente item por item, como os exemplos:
valores = [5, 2, 8, 6, 4]
partes_dos_valores = valores[2:]
novos_valores = [2 * valor for valor in valores]