Back-End

27 abr, 2012

Dicas, truques e hacks de Python – Parte 4

Publicidade

Esta é a última parte da série (artigo anterior aqui) de truques e dicas do Python, espero que tenha sido proveitoso até agora. Falaremos aqui sobre a declaração de funções e de classes de objeto.

5 – Funções

5.1 – Valores padrão de argumento são avaliados apenas uma vez

Vamos começar
esta seção com um aviso. Aqui temos um problema que tem confundido muitos
escritores novos no Python, incluindo eu mesmo, repetidamente, e até mesmo
depois de eu ter entendido qual era o problema… É fácil ser estúpido com
relação a isso (nota que este não é o melhor exemplo do mundo, mas mostra onde
quero chegar):

1def function(item, stuff = []):
2 stuff.append(item)
3 print stuff
4
5function(1)
6# prints '[1]'
7
8function(2)
9# prints '[1,2]' !!!

O valor padrão
para um argumento de função só é avaliado uma vez, quando a função é definida.
O Python simplesmente atribuiu esse valor para o nome da sua variável correta
quando a função foi chamada.

O Python não
checa se aquele valor (aquele local na memória) foi modificado. Ele
simplesmente continua a atribuir aquele valor para qualquer um que precisar
chamá-lo. Então, se o valor é modificado, a mudança vai persistir em todos as chamadas de funções. Acima, quando
anexamos um valor para a lista representada por stuff, nós de fato mudamos o valor padrão para toda a eternidade.
Quando chamarmos a função de novo, procurando pelo valor padrão, o valor padrão
modificado terá sido entregue.

A solução: não
use objetos mutáveis como padrão de
funções. Você pode até conseguir fazer funcionar, se não modificá-los, mas
mesmo assim não é uma boa idéia.

Uma
maneira melhor de escrever o código acima seria:

1def function(item, stuff = None):
2 if stuff is None:
3 stuff = []
4 stuff.append(item)
5 print stuff
6
7function(1)
8# prints '[1]'
9
10function(2)
11# prints '[2]', as expected

None é imutável (e nós não estamos tentando mudá-los, de qualquer maneira), então estamos seguros contra uma mudança acidental no valor padrão.

Vendo pelo lado positivo, um programador esperto provavelmente faria disso um truque, criando “variáveis estatísticas” no C-style.

Por outro lado, um programador mais esperto provavelmente transformaria
isso em um truque, criando “variáveis estáveis” em C-style.

5.1.1 – Forçar argumentos padrão a sempre serem avaliados

Se você prefere funções menos desordenadas com a mesma
claridade, você pode reavaliar fortemente os argumentos padrão antes de cada
chamada de função. O decorator a seguir armazena os
valores originais de cada argumento padrão. Ele pode ser usado para amarrar uma
função e para resetar os argumentos padrão antes de cada chamada.

1from copy import deepcopy
2
3def resetDefaults(f):
4 defaults = f.func_defaults
5 def resetter(*args, **kwds):
6 f.func_defaults = deepcopy(defaults)
7 return f(*args, **kwds)
8 resetter.__name__ = f.__name__
9 return resetter

Simplesmente aplique esse decorator à sua função para ter os resultados esperados.

 1@resetDefaults # This is how you apply a decorator
2def function(item, stuff = []):
3 stuff.append(item)
4 print stuff
5
6function(1)
7# prints '[1]'
8
9function(2)
10# prints '[2]', as expected

5.2 – Números arbitrários de argumentos

O Python permite que você tenha números arbitrários de
argumentos nas suas funções. Primeiramente, defina qualquer argumento
necessário (se tiver algum), e então use a variável com um ‘*’ precedido a ela.
O Python vai pegar o resto dos argumentos não-palavras-chave, colocá-los em uma
lista ou tupla, e adicionar a eles esta variável:

1def do_something(a, b, c, *args):
2 print a, b, c, args
3
4do_something(1,2,3,4,5,6,7,8,9)
5# prints '1, 2, 3, (4, 5, 6, 7, 8, 9)'

Por que você
gostaria de fazer isso? Um motivo comum é porque sua função aceita um número de
itens, e faz a mesma coisa com todos os outros (digamos, os soma). Você poderia
forçar o usuário a passar a lista: sum_all([1,2,3]) ou você poderia permitir que eles usassem um
número arbitrário de argumentos, o que gera um código mais limpo: sum_all(1,2,3).

Você
também pode ter números arbitrários de argumentos de palavras-chve. Depois que
você definiu todos os outros argumentos, use a variável com ‘**’ anexada a ela.
O Python vai pegar o resto dos argumentos de palavra-chave, colocá-los em um
dicionário, e atribuí-los a esta variável:

1def do_something_else(a, b, c, *args, **kwargs):
2 print a, b, c, args, kwargs
3
4do_something_else(1,2,3,4,5,6,7,8,9, timeout=1.5)
5# prints '1, 2, 3, (4, 5, 6, 7, 8, 9), {"timeout": 1.5}'

Por que você gostaria de fazer isso? Acredito que o
motivo mais comum é se sua função é um wrapper (“invólucro”) para outra função
ou funções, e quaisquer argumentos de palavras-chave que você usar podem ser
retirados do dicionário, e o restante dos argumentos de palavras-chave pode ser
passados para outra (s) função (ões).

5.2.1 – Caveat
Passar argumentos de palavras-chave
arbitrários e argumentos de palavras-chave nomeados (não arbitrários) em uma
mesma função é aparentemente impossível.
Isso porque os argumentos de palavras-chave nomeados devem ser definidos
antes do parâmetro ‘*’ na definição da função, e são preenchidos antes de o
parâmetro ser preenchido. Por exemplo, imagine a
função:

1def do_something(a, b, c, actually_print = True, *args):
2 if actually_print:
3 print a, b, c, args

Agora nós temos
um problema: não tem jeito de especificarmos actually_print como um argumento de
palavras-chave nomeado enquanto, simultaneamente, fornecemos argumentos de não-palavras-chave arbitrários. Ambos os erros irão
ocorrer:

1do_something(1, 2, 3, 4, 5, actually_print = True)
2# actually_print is initially set to 4 (see why?) and then re-set,
3# causing a TypeError ('got multiple values for keyword argument')
4
5do_something(1, 2, 3, actually_print = True, 4, 5, 6)
6# This is not allowed as keyword arguments may not precede non-keyword arguments. A SyntaxError is raised.

A única maneira de passar actually_print nessa situação é passá-lo
como um argumento de não-palavra-chave.

1do_something(1, 2, 3, True, 4, 5, 6)
2# Result is '1, 2, 3, (4, 5, 6)'

5.3 – Passando uma lista ou um dicionário como argumentos

Como você pode
receber argumentos como listas ou dicionarios, não é terrivelmente
surpreendente, eu acredito, que você possa enviar argumentos para uma função de
uma lista ou dicionário. A sixtaxe é exatamente a mesma que a mostrada acima.

Para
enviar uma lista como argumentos de não-palavra-chave, apenas a anexe com um
‘*’:

1args = [5,2]
2pow(*args)
3# returns pow(5,2), meaning 5^2 which is 25

E, claro, para enviar um dicionário como argumento de
palavras-chave (isso é provavelmente mais comum) anexe a ele um ‘*’:

 1def do_something(actually_do_something=True, print_a_bunch_of_numbers=False):
2 if actually_do_something:
3 print 'Something has been done'
4 #
5 if print_a_bunch_of_numbers:
6 print range(10)
7
8kwargs = {'actually_do_something': True, 'print_a_bunch_of_numbers': True}
9do_something(**kwargs)
10
11# prints 'Something has been done', then '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'

Nota histórica:
em versões antigas do Python (pre-2.3) você chamava funções com argumentos
arbitrários usando o (function, arg_list, keyword_arg_dict).

5.4 – Decorators

Function decorators são muito simples, mas, se você nunca as viu antes, não terá a
menor idéia do que está acontecendo, pois diferentemente da maioria das
sintaxes no Python, ela não é muito clara. Um decorator é uma função que envolve outra função: a função principal
é chamada e seu valor retornado é passado para o decorator. O decorator então
retorna a função que substitui a função envolvida para o resto da programação.

Aqui
está a sintaxe:

 1def decorator1(func):
2 return lambda: func() + 1
3
4def decorator2(func):
5 def print_func():
6 print func()
7 return print_func
8
9@decorator2
10@decorator1
11def function():
12 return 41
13
14function()
15# prints '42'

Neste exemplo, ‘function’ é passado para ‘decorator1’. ‘decorator1’ devolve uma função que
chama ‘function’ e adiciona 1. Essa função é então passada para ‘decorator2’, que devolve uma função
que chama a função retornada pelo ‘decorator1’ e imprime o resultado.
Essa ultima função é a que você está de fato chamando quando você chama
‘function’. Ufa.

Esse exemplo faz
exatamente a mesma coisa, porém é mais prolixo e sem decorators

 1def decorator1(func):
2 return lambda: func() + 1
3
4def decorator2(func):
5 def print_func():
6 print func()
7 return print_func
8
9def function():
10 return 41
11
12function = decorator2(decorator1(function))
13
14function()
15# prints '42'

Decorators são tipicamente usados para adicionar
habilidades à suas funções. Mais tipicamente ainda, eles não são
usados. Mas é bom saber o que você está vendo.

5.5  – Switch Statements usando dictionários de funções

Sentindo saudade
do switch statement? Como você já
deve saber, o Python não tem um equivalente de sintaxe, a não ser que você
conte elif’s repetidos. O que você não
deve saber, no entanto, é que você pode replicar o comportamento (se não a
limpeza) do switch statement ao criar
um dicionário de funções unido pelo valor que você quer switch.

Por
exemplo, digamos que você está lidando com keystrokes e você precisa chamar uma
função diferente para cada keystroke. E digamos também que você já definiu
essas três funções:

 1def key_1_pressed():
2 print 'Key 1 Pressed'
3
4def key_2_pressed():
5 print 'Key 2 Pressed'
6
7def key_3_pressed():
8 print 'Key 3 Pressed'
9
10def unknown_key_pressed():
11 print 'Unknown Key Pressed'

No Python, você normalmente usaria o elif’s para escolher uma função:

 1keycode = 2
2if keycode == 1:
3 key_1_pressed()
4elif keycode == 2:
5 key_2_pressed()
6elif number == 3:
7 key_3_pressed()
8else:
9 unknown_key_pressed()
10# prints 'Key 2 Pressed'

Mas você também poderia jogar todas as funções em um
dicionário, e as codificar ao valor em que você está usando o swtich. Você
poderia até checar para ver se a chave existe, e executar o mesmo código se não
existir:

1keycode = 2
2functions = {1: key_1_pressed, 2: key_2_pressed, 3: key_3_pressed}
3functions.get(keycode, unknown_key_pressed)()

Você pode ver que isso é bem mais limpo que o exemplo
com elif,
para grande quantidades de funções.

6 – Classes

Métodos são apenas funções regulares que, quando são
chamadas a partir de uma instância, são passados por essa instância como o
primeiro argumento (geralmente chamado ‘self’). Se por alguma razão você não
está chamando a função de uma instância, você pode passá-la manualmente como o
primeiro argumento. Por exemplo:

 1class Class:
2 def a_method(self):
3 print 'Hey a method'
4
5instance = Class()
6
7instance.a_method()
8# prints 'Hey a method', somewhat unsuprisingly. You can also do:
9
10Class.a_method(instance)
11# prints 'Hey a method'

Internamente, essas demonstrações são exatamente
iguais.

6.2 – Checando propriedades e existência de métodos

Precisa saber se uma classe ou instância tem uma
propriedade ou método em particular? Você pode usar a função ‘hasattr’
para checar; ela aceita o objeto e o atributo (como uma string) para fazer a checagem. Você a usa da mesma maneira que o
método de dicionário ‘has_key’
(apesar de que ela funciona de maneira completamente diferente).

1class Class:
2 answer = 42
3
4hasattr(Class, 'answer')
5# returns True
6hasattr(Class, 'question')
7# returns False

Você também pode checar a existência e o acesso à
propriedade em um passo usando a função getattr. A getattr também aceita o objeto e o atributo, como uma string, para fazer a checagem. Ela tem
um terceiro argumento opcional, entregando o default se o atributo não é encontrado. Diferentemente do método de
dicionário get, com o qual você deve
ter mais familiaridade, se o default não é entregue e o atributo não é encontrado, você tem o erro AttributeError.

1class Class:
2 answer = 42
3
4getattr(Class, 'answer')
5# returns 42
6getattr(Class, 'question', 'What is six times nine?')
7# returns 'What is six times nine?'
8getattr(Class, 'question')
9# raises AttributeError

Não use hasattr e getattr
demais. Se você escreveu suas classes de modo que precisa continuar verificando para ver se uma propriedade existe, você as
escreveu erroneamente. Sempre faça o valor existir, e o configure como Nenhum
(ou qualquer outra coisa), se ele não está sendo usado. Essas funções são
melhores usadas para ligar com polimorfismo, isto é, permitir que sua
função/classe/seja o que for suporte diferentes tipos de objetos.

6.3 – Modificando classes depois de criadas

Você pode adicionar, modificar ou deletar uma
propriedade ou um método de classe muito depois de a classe ter sido criada, e mesmo
depois que ela tenha sido instanciada. Apenas acesse a propriedade ou o método
como Class.attribute.
Não importa quando eles foram criados, as intâncias da classe respeitarão estas
mudanças:

 1class Class:
2 def method(self):
3 print 'Hey a method'
4
5instance = Class()
6instance.method()
7# prints 'Hey a method'
8
9def new_method(self):
10 print 'New method wins!'
11
12Class.method = new_method
13instance.method()
14# prints 'New method wins!'

Super legal. Mas não se empolgue em modificar métodos
pré-existentes, é uma prática ruim e pode confundir os objetos que estão
utilizando aquela classe. Por outro lado, adicionar métodos é bem menos (mas
ainda sim, um pouco) perigoso.

6.4 – Criando métodos de classe

Ocasionalmente, ao
escrever uma classe, você quer incluir uma função que é chamada da classe, e
não da instância. Talvez esse método crie novas instâncias, ou talvez ele seja
independente de quaisquer propriedades de qualquer instância individual. O
Python nos dá duas maneiras de fazer isso, dependendo se o seu método precisa
(ou deveria) saber qual classe o chamou. Ambas envolvem a utilização de decorators em seus métodos.

Um método de classe recebe a classe como
primeiro argumento, da mesma maneira que um método de instância recebe a
instância como primeiro argumento. Portanto, o método está ciente de que está
sendo chamado de sua própria classe ou de uma subclasse.

Um método estático não recebe nenhuma
informação sobre de onde ele foi chamado; ele é, essencialmente, uma função
normal, apenas em um escopo diferente.

Métodos de classe e estáticos podem ser chamados
diretamente da classe, como Class.method(), ou
de uma instância, como Class().method(). A instância é ignorada, exceto por sua
classe. Aqui temos um exemplo de cada um deles, com um instance method regular:

 1class Class:
2 @classmethod
3 def a_class_method(cls):
4 print 'I was called from class %s' % cls
5 #
6 @staticmethod
7 def a_static_method():
8 print 'I have no idea where I was called from'
9 #
10 def an_instance_method(self):
11 print 'I was called from the instance %s' % self
12
13instance = Class()
14
15Class.a_class_method()
16instance.a_class_method()
17# both print 'I was called from class __main__.Class'
18
19Class.a_static_method()
20instance.a_static_method()
21# both print 'I have no idea where I was called from'
22
23Class.an_instance_method()
24
25# raises TypeError
26instance.an_instance_method()
27# prints something like 'I was called from the instance <__main__.Class instance at 0x2e80d0>'

7 – Conclusão

Precisa de mais
inspiração? Um bom lugar para procurá-la é na página Python Built-in Functions. Há um monte de funções legais de que você provavelmente nunca
ouviu falar. Se você crirar algum truque ou algo que deve ser compartilhado,
sinta se a vontade para adicioná-lo a este artigo.

(Você pode
conseguir uma conta Siafoo em http://www.siafoo.net/new/user)

Boa
codificação!

?

Texto original licenciado sob Creative Commons e disponível em http://www.siafoo.net/article/52