Complementando:
Sobre o escopo de variáveis, existe uma "brecha": acessar uma variável que foi declarada fora da função (desde que não haja uma atribuição a ela) é permitido. Exemplo:
def f():
print('dentro da função', x)
x = 10
f()
print('fora da função', x)
A saída deste código é:
dentro da função 10
fora da função 10
Ou seja, o x
dentro da função refere-se à variável que foi criada fora dela. Isso é possível porque dentro da função não é feita nenhuma atribuição à variável x
.
Agora se eu mudar para:
def f():
x = 2
print('dentro da função', x)
x = 10
f()
print('fora da função', x)
Agora a saída é:
dentro da função 2
fora da função 10
No caso, a atribuição x = 2
feita dentro da função poderia ser interpretada como a criação de uma variável local, ou uma alteração da variável x
criada fora da função. Mas de acordo com as regras da linguagem, optaram pela primeira opção (ou seja, o x
dentro da função não é o mesmo que está fora dela).
Se eu quisesse que a variável x
externa fosse alterada dentro da função, teria que usar a declaração global
:
def f():
global x # quero usar a variável x que está definida fora da função
x = 2
print('dentro da função', x)
x = 10
f()
print('fora da função', x)
Agora a saída será:
dentro da função 2
fora da função 2
Sobre o parâmetro com valor default, tem uma pegadinha. Se usar tipos mutáveis, como listas ou dicionários, podem ocorrer situações inesperadas. Por exemplo:
# função recebe uma lista, e se não for passada, usa uma lista vazia
def f(lista=[]):
lista.append(1)
print(lista)
# chama a função 3 vezes
f()
f()
f()
O resultado será:
[1]
[1, 1]
[1, 1, 1]
Isso porque o valor default só é criado uma vez, quando a função é definida. Ou seja, cada vez que a função é chamada, ela acaba adicionando um elemento na mesma lista. Por isso que a cada chamada ela aumenta. A solução, neste caso, seria mudar para:
def f(lista=None):
if lista is None:
lista = []
lista.append(1)
print(lista)
Assim, a cada chamada sem parâmetros, uma nova lista é criada.
Por fim, sobre o *args
e **kwargs
, vale lembrar que também é possível usar ambos:
def f(*args, **kwargs):
print('args:', args)
print('kwargs:', kwargs)
print('somente posicionais:')
f(1, 2)
print('somente nomeados:')
f(a=3, b=4)
print('misturado:')
f(1, 2, a=3, b=4)
Assim, se eu chamar a função somente com argumentos posicionais, apenas args
será preenchido e kwargs
ficará vazio. E se chamar somente com argumentos nomeados, ocorre o contrário. E se chamar com ambos, tanto args
quanto kwargs
estarão preenchidos. A saída do código acima é:
somente posicionais:
args: (1, 2)
kwargs: {}
somente nomeados:
args: ()
kwargs: {'a': 3, 'b': 4}
misturado:
args: (1, 2)
kwargs: {'a': 3, 'b': 4}
Lembrando que os argumentos nomeados devem vir no final, ou seja, se fizer algo como f(a=1, b=2, 3, 4)
ou ainda f(1, b=2, 3)
, dará erro.