Sempre fiquei surpreso que, para trabalhar com argumentos de funções Python, você só precisa entender
*argse **kwargs. E não fiquei surpreso em vão. Acontece que os argumentos estão longe de ser fáceis. Nesta postagem, quero dar uma visão geral de tudo relacionado a argumentos de função em Python. Espero que no final eu realmente consiga mostrar o quadro geral do trabalho com argumentos, e que este artigo não se transforme em mais uma publicação na qual o leitor não possa encontrar nada de novo. E agora - direto ao ponto.
A maioria dos leitores deste artigo, acredito, entende a essência dos argumentos de função. Para iniciantes, deixe-me explicar que esses são objetos enviados a uma função pelo iniciador de sua chamada. Ao passar argumentos para uma função, várias ações são executadas, dependendo de que tipo de objetos são despachados para a função (objetos mutáveis ou imutáveis). Um iniciador de chamada de função é uma entidade que chama uma função e passa argumentos para ela. Por falar em funções de chamada, há algumas coisas a considerar que discutiremos agora.
Os argumentos, cujos nomes são especificados quando a função é declarada, armazenam os objetos passados para a função quando chamados. Além disso, se algo é atribuído às variáveis locais correspondentes de funções, seus parâmetros, esta operação não afeta os objetos imutáveis passados para as funções. Por exemplo:
def foo(a):
a = a+5
print(a) # 15
a = 10
foo(a)
print(a) # 10
Como você pode ver, a chamada da função não afetou a variável de forma alguma
a. Isso é exatamente o que acontece quando um objeto imutável é passado para uma função.
E se objetos mutáveis são passados para funções, você pode encontrar um comportamento do sistema diferente do acima.
def foo(lst):
lst = lst + ['new entry']
print(lst) # ['Book', 'Pen', 'new entry']
lst = ['Book', 'Pen']
print(lst) # ['Book', 'Pen']
foo(lst)
print(lst) # ['Book', 'Pen']
Você notou algo novo aqui? Se você responder “Não”, você está certo. Mas se de alguma forma influenciarmos os elementos do objeto mutável passado para a função, testemunharemos algo diferente.
def foo(lst):
lst[1] = 'new entry'
print(lst) # ['Book', 'new entry']
lst = ['Book', 'Pen']
print(lst) # ['Book', 'Pen']
foo(lst)
print(lst) # ['Book', 'new entry']
Como você pode ver, o objeto do parâmetro
lstfoi alterado após a chamada da função. Isso aconteceu devido ao fato de estarmos trabalhando com uma referência a um objeto armazenado em um parâmetro lst. Como resultado, alterar o conteúdo desse objeto está fora do escopo da função. Você pode evitar isso simplesmente fazendo cópias profundas de tais objetos e gravando-as nas variáveis locais da função.
def foo(lst):
lst = lst[:]
lst[1] = 'new entry'
print(lst) # ['Book', 'new entry']
lst = ['Book', 'Pen']
print(lst) # ['Book', 'Pen']
foo(lst)
print(lst) # ['Book', 'Pen']
Isso não te surpreendeu ainda? Se não, gostaria de ter certeza de que você pula o que sabe e passa imediatamente para um novo material para você. E se sim, então, marque minhas palavras, à medida que conhecer melhor os argumentos, você aprenderá muito mais coisas interessantes.
Então, aqui está o que você deve saber sobre argumentos de função:
- A ordem em que os argumentos posicionais são passados para as funções.
- A ordem em que os argumentos nomeados são passados para as funções.
- Atribuindo valores de argumento padrão.
- Organização do processamento de conjuntos de argumentos de comprimento variável.
- Descompactando argumentos.
- Usando argumentos que só podem ser transmitidos por nome (apenas por palavra-chave).
Vejamos cada um desses pontos.
1. Ordem de passagem de argumentos posicionais para funções
Os argumentos posicionais são processados da esquerda para a direita. Ou seja, verifica-se que a posição do argumento passado para a função está em correspondência direta com a posição do parâmetro usado no cabeçalho da função quando ela foi declarada.
def foo(d, e, f):
print(d, e, f)
a, b, c = 1, 2, 3
foo(a, b, c) # 1, 2, 3
foo(b, a, c) # 2, 1, 3
foo(c, b, a) # 3, 2, 1
As variáveis
a, be ctêm os valores 1, 2 e 3. Essas variáveis desempenham o papel dos argumentos com os quais a função é chamada foo. Eles, na primeira chamada da função, correspondem aos parâmetros d, ee f. Este mecanismo se aplica a quase todos os 6 pontos acima sobre o que você precisa saber sobre argumentos de função em Python. A localização do argumento posicional passado para a função quando chamada desempenha um papel importante na atribuição de valores aos parâmetros da função.
2. Ordem de passagem de argumentos nomeados para funções
Argumentos nomeados são passados para funções com os nomes desses argumentos correspondendo aos nomes que foram atribuídos a eles quando a função foi declarada.
def foo(arg1=0, arg2=0, arg3=0):
print(arg1, arg2, arg3)
a, b, c = 1, 2, 3
foo(a,b,c) # 1 2 3
foo(arg1=a, arg2=b, arg3=c) # 1 2 3
foo(arg3=c, arg2=b, arg1=a) # 1 2 3
foo(arg2=b, arg1=a, arg3=c) # 1 2 3
Como você pode ver, a função
fooleva 3 argumentos. Esses argumentos são nomeados arg1, arg2e arg3. Preste atenção em como alteramos a posição dos argumentos ao chamar a função. Os argumentos nomeados são tratados de maneira diferente dos argumentos posicionais, embora o sistema continue a lê-los da esquerda para a direita. Python considera os nomes dos argumentos, não suas posições, ao atribuir valores apropriados aos parâmetros da função. Como resultado, a função produz a mesma coisa, independentemente das posições dos argumentos passados para ela. É sempre assim 1 2 3.
Observe que os mecanismos descritos no parágrafo 1 continuam a operar aqui.
3. Atribuição de valores de argumento padrão
Os valores padrão podem ser atribuídos a argumentos nomeados. Ao usar esse mecanismo em uma função, certos argumentos se tornam opcionais. A declaração de tais funções se parece com o que consideramos no ponto # 2. A única diferença é como essas funções são chamadas.
def foo(arg1=0, arg2=0, arg3=0):
print(arg1, arg2, arg3)
a, b, c = 1, 2, 3
foo(arg1=a) # 1 0 0
foo(arg1=a, arg2=b ) # 1 2 0
foo(arg1=a, arg2=b, arg3=c) # 1 2 3
Observe que, neste exemplo, não estamos passando todos os argumentos para a função conforme descrito em sua declaração. Nestes casos, os parâmetros correspondentes são atribuídos aos valores padrão. Vamos continuar com este exemplo:
foo(arg2=b) # 0 2 0
foo(arg2=b, arg3=c ) # 0 2 3
foo(arg3=c) # 0 0 3
foo(arg3=c, arg1=a ) # 1 0 3
Estes são exemplos simples e compreensíveis de como usar os mecanismos descritos acima para chamar funções com a passagem de argumentos nomeados para elas. Agora vamos complicar nossos experimentos combinando o que falamos até agora nos pontos 1, 2 e 3:
foo(a, arg2=b) # 1 2 0
foo(a, arg2=b, arg3=c) # 1 2 3
foo(a, b, arg3=c) # 1 2 3
foo(a) # 1 0 0
foo(a,b) # 1 2 0
Aqui, os argumentos posicionais e nomeados são usados ao chamar a função. Ao usar argumentos posicionais, a ordem em que são especificados continua a desempenhar um papel crítico na transmissão correta da entrada para a função.
Aqui, gostaria de chamar sua atenção para um detalhe notável. Consiste em que argumentos posicionais não podem ser especificados após argumentos nomeados. Aqui está um exemplo para ajudá-lo a entender melhor essa ideia:
foo(arg1=a, b)
>>>
foo(arg1=a, b)
^
SyntaxError: positional argument follows keyword argument
foo(a, arg2=b, c)
>>>
foo(a, arg2=b, c)
^
SyntaxError: positional argument follows keyword argument
Você pode aceitar isso como uma regra. Os argumentos posicionais não precisam seguir os argumentos nomeados ao chamar uma função.
4. Organização do processamento de conjuntos de argumentos de comprimento variável
Aqui vamos falar sobre construções
*argse **kwargs. Quando essas construções são usadas em uma declaração de função, esperamos que, quando a função for chamada, conjuntos de argumentos de comprimentos arbitrários sejam representados como parâmetros argse kwargs. Quando a construção é aplicada *args, o parâmetro argsrecebe argumentos posicionais representados como uma tupla. Quando aplicado **kwargsno kwargsoutono, argumentos nomeados, listados em um dicionário.
def foo(*args):
print(args)
a, b, c = 1, 2, 3
foo(a, b, c) # (1, 2, 3)
foo(a, b) # (1, 2)
foo(a) # (1)
foo(b, c) # (2, 3)
Este código prova que o parâmetro
argsarmazena uma tupla contendo o que foi passado para a função quando ela foi chamada.
def foo(**kwargs):
print(kwargs)
foo(a=1, b=2, c=3) # {'a': 1, 'b': 2, 'c': 3}
foo(a=1, b=2) # {'a': 1, 'b': 2}
foo(a=1) # {'a': 1}
foo(b=2, c=3) # {'b': 2, 'c': 3}
O código acima mostra que o parâmetro
kwargsarmazena um dicionário de pares chave-valor que representam os argumentos nomeados passados para a função quando chamada.
No entanto, deve-se observar que uma função projetada para aceitar argumentos posicionais não pode receber argumentos nomeados (e vice-versa).
def foo(*args):
print(args)
foo(a=1, b=2, c=3)
>>>
foo(a=1, b=2, c=3)
TypeError: foo() got an unexpected keyword argument 'a'
#########################################################
def foo(**kwargs):
print(kwargs)
a, b, c = 1, 2, 3
foo(a, b, c)
>>>
TypeError: foo() takes 0 positional arguments but 3 were given
Agora vamos juntar tudo o que analisamos nos pontos 1, 2, 3 e 4 e experimentar tudo isso, examinando diferentes combinações de argumentos que podem ser passados para funções quando são chamadas.
def foo(*args,**kwargs):
print(args, kwargs)
foo(a=1,)
# () {'a': 1}
foo(a=1, b=2, c=3)
# () {'a': 1, 'b': 2, 'c': 3}
foo(1, 2, a=1, b=2)
# (1, 2) {'a': 1, 'b': 2}
foo(1, 2)
# (1, 2) {}
Como você pode ver, temos uma tupla
argse um dicionário à nossa disposição kwargs.
E aqui está outra regra. Reside no fato de que a estrutura
*argsnão pode ser usada depois da estrutura **kwargs.
def foo(**kwargs, *args):
print(kwargs, args)
>>>
def foo(**kwargs, *args):
^
SyntaxError: invalid syntax
A mesma regra se aplica à ordem em que os argumentos são especificados ao chamar funções. Argumentos posicionais não devem seguir argumentos nomeados.
foo(a=1, 1)
>>>
foo(a=1, 1)
^
SyntaxError: positional argument follows keyword argument
foo(1, a=1, 2)
>>>
foo(1, a=1, 2)
^
SyntaxError: positional argument follows keyword argument
Ao declarar funções, você pode combinar argumentos posicionais,
*argse *kwagrscomo segue:
def foo(var, *args,**kwargs):
print(var, args, kwargs)
foo(1, a=1,) # 1
# 1 () {'a': 1}
foo(1, a=1, b=2, c=3) # 2
# 1 () {'a': 1, 'b': 2, 'c': 3}
foo(1, 2, a=1, b=2) # 3
# 1 (2,) {'a': 1, 'b': 2}
foo(1, 2, 3, a=1, b=2) # 4
# 1 (2, 3) {'a': 1, 'b': 2}
foo(1, 2) # 5
# 1 (2,) {}
Ao declarar uma função,
foopresumimos que ela deve ter um argumento posicional obrigatório. Ele é seguido por um conjunto de argumentos posicionais de comprimento variável e esse conjunto é seguido por um conjunto de argumentos nomeados de comprimento variável. Sabendo disso, podemos facilmente "descriptografar" cada uma das chamadas de função acima.
A
1função recebe argumentos 1e a=1. Esses são, respectivamente, argumentos posicionais e nomeados. 2É uma variedade 1. Aqui, o comprimento do conjunto de argumentos posicionais é zero.
Em
3nós passamos funções 1, 2e a=1,b=2. Isso significa que agora ele aceita dois argumentos posicionais e dois argumentos nomeados. De acordo com a declaração da função, verifica-se que1tomado como um argumento posicional obrigatório, 2entra em um conjunto de argumentos posicionais de comprimento variável a=1e b=2termina em um conjunto de argumentos nomeados de comprimento variável.
Para chamar essa função corretamente, devemos passar pelo menos um argumento posicional para ela. Caso contrário, enfrentaremos um erro.
def foo(var, *args,**kwargs):
print(var, args, kwargs)
foo(a=1)
>>>
foo(a=1)
TypeError: foo() missing 1 required positional argument: 'var'
Outra variação dessa função é uma função que declara que leva um argumento posicional obrigatório e um argumento nomeado, seguido por conjuntos de comprimento variável de argumentos posicionais e nomeados.
def foo(var, kvar=0, *args,**kwargs):
print(var, kvar, args, kwargs)
foo(1, a=1,) # 1
# 1 0 () {'a': 1}
foo(1, 2, a=1, b=2, c=3) # 2
# 1 0 () {'a': 1, 'b': 2, 'c': 3}
foo(1, 2, 3, a=1, b=2) # 3
# 1 2 () {'a': 1, 'b': 2}
foo(1, 2, 3, 4, a=1, b=2) # 4
# 1 2 (3,) {'a': 1, 'b': 2}
foo(1, kvar=2) # 5
# 1 2 () {}
As chamadas para esta função podem ser "descriptografadas" da mesma forma que foi feito ao analisar a função anterior.
Ao chamar esta função, ela deve receber pelo menos um argumento posicional. Caso contrário, encontraremos um erro:
foo()
>>>
foo()
TypeError: foo() missing 1 required positional argument: 'var'
foo(1)
# 1 0 () {}
Observe que a chamada
foo(1)funciona bem. O ponto aqui é que se uma função for chamada sem especificar um valor para um argumento nomeado, o valor será automaticamente atribuído a ela.
E aqui estão mais alguns erros que podem ser encontrados se esta função for chamada incorretamente:
foo(kvar=1) # 1
>>>
TypeError: foo() missing 1 required positional argument: 'var'
foo(kvar=1, 1, a=1) # 2
>>>
SyntaxError: positional argument follows keyword argument
foo(1, kvar=2, 3, a=2) # 3
>>>
SyntaxError: positional argument follows keyword argument
Preste atenção especial ao erro de tempo de execução
3.
5. Descompactando argumentos
Nas seções anteriores, falamos sobre como coletar conjuntos de argumentos passados para funções em tuplas e dicionários. E aqui vamos discutir a operação reversa. A saber, analisaremos o mecanismo que permite descompactar os argumentos fornecidos para a entrada da função.
args = (1, 2, 3, 4)
print(*args) # 1 2 3 4
print(args) # (1, 2, 3, 4)
kwargs = { 'a':1, 'b':2}
print(kwargs) # {'a': 1, 'b': 2}
print(*kwargs) # a b
As variáveis podem ser descompactadas usando a sintaxe
*e **. É assim que eles são usados ao passar tuplas, listas e dicionários para funções.
def foo(a, b=0, *args, **kwargs):
print(a, b, args, kwargs)
tup = (1, 2, 3, 4)
lst = [1, 2, 3, 4]
d = {'e':1, 'f':2, 'g':'3'}
foo(*tup) # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}
foo(*lst) # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}
foo(1, *tup) # foo(1, 1, 2, 3, 4)
# 1 1 (2, 3, 4) {}
foo(1, 5, *tup) # foo(1, 5, 1, 2, 3, 4)
# 1 5 (1, 2, 3, 4) {}
foo(1, *tup, **d) # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 1 (2, 3, 4) {'e': 1, 'f': 2, 'g': '3'}
foo(*tup, **d) # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 2 (3, 4) {'e': 1, 'f': 2, 'g': '3'}
d['b'] = 45
foo(2, **d) # foo(1, e=1 ,f=2, g=3, b=45)
# 2 45 () {'e': 1, 'f': 2, 'g': '3'}
Desconstrua cada uma das chamadas de função mostradas aqui usando o desempacotamento de argumento e observe como as chamadas correspondentes ficariam sem usar
*e **. Tente entender o que acontece quando você faz essas chamadas e como as várias estruturas de dados são descompactadas.
Ao tentar desempacotar argumentos, você pode encontrar um novo erro:
foo(1, *tup, b=5)
>>>
TypeError: foo() got multiple values for argument 'b'
foo(1, b=5, *tup)
>>>
TypeError: foo() got multiple values for argument 'b'
Este erro ocorre devido a um conflito entre o argumento nomeado ,,
b=5e o argumento posicional. Como descobrimos na seção 2, a ordem dos argumentos nomeados não importa quando passados. Como resultado, o mesmo erro ocorre em ambos os casos.
6. Usando argumentos que só podem ser transmitidos por nome (apenas por palavra-chave)
Em alguns casos, você precisa fazer a função aceitar os argumentos nomeados necessários. Se, ao declarar uma função, eles descrevem argumentos que podem ser passados apenas por nome, então tais argumentos devem ser passados a ela sempre que for chamada.
def foo(a, *args, b):
print(a, args, b)
tup = (1, 2, 3, 4)
foo(*tup, b=35)
# 1 (2, 3, 4) 35
foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35
foo(1, 5, *tup, b=35)
# 1 (5, 1, 2, 3, 4) 35
foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35
foo(1, b=35)
# 1 () 35
foo(1, 2, b=35)
# 1 (2,) 35
foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'b'
foo(1, 2, 3)
# TypeError: foo() missing 1 required keyword-only argument: 'b'
Como você pode ver, espera-se que a função necessariamente receba um argumento nomeado
b, que, na declaração da função, é especificado depois *args. Nesse caso, na declaração da função, você pode simplesmente usar um símbolo *, após o qual, separados por vírgulas, existem identificadores de argumentos nomeados que podem ser passados para a função apenas pelo nome. Tal função não seria projetada para aceitar um conjunto de argumentos posicionais de comprimento variável.
def foo(a, *, b, c):
print(a, b, c)
tup = (1, 2, 3, 4)
foo(1, b=35, c=55)
# 1 35 55
foo(c= 55, b=35, a=1)
# 1 35 55
foo(1, 2, 3)
# TypeError: foo() takes 1 positional argument but 3 were given
foo(*tup, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given
foo(1, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given
A função declarada no exemplo anterior leva um argumento posicional e dois argumentos nomeados, que só podem ser passados por nome. Isso leva ao fato de que para uma função ser chamada corretamente, ela precisa passar os dois argumentos nomeados. Depois disso,
*você também pode descrever os argumentos nomeados, que recebem os valores padrão. Isso nos dá uma certa liberdade ao chamar essas funções.
def foo(a, *, b=0, c, d=0):
print(a, b, c, d)
foo(1, c=55)
# 1 0 55 0
foo(1, c=55, b=35)
# 1 35 55 0
foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'c'
Observe que a função pode ser chamada normalmente sem passar nenhum argumento para ela
be dporque eles receberam valores padrão.
Resultado
Talvez tenhamos, de fato, uma longa história sobre argumentos. Espero que os leitores deste material tenham aprendido algo novo por si próprios. E, por falar nisso, a história dos argumentos de função em Python continua. Talvez falemos sobre eles mais tarde.
Você aprendeu algo novo sobre os argumentos de função em Python com este material?
