Manipulação de erros com Python
Tecnica inspirada em Go para manipular erros em Python (ou em qualquer outra linguagem de sua preferência) que “desenvolvi”.
Antes de mais nada, preciso deixar claro que sou apenas um hobbysta e tenho 0 experiência proficional no mercado, portanto, não tenho certeza se isso é uma boa prática ou não, se vale a pena usar esse método ou não. Mas mesmo assim, achei essa ideia interessante e gostaria de compartilhá-la neste artigo, porquê acredito que essa solução pra esse problema seja bem limpa e legível.
Irei explicar em JavaScript, no geral, não vai fazer tanta diferença porquê as features do Python estão presentes em outras linguagens, mas tem um detalhe que gostaria de comentar no final a respeito dessa minha solução em Python.
Introdução do meu problema
Recentemente me deparei com o seguinte problema: minha fução não funciona as
vezes. Lógico que não foi só isso, manipular erros é comum no universo de
software. No entanto, eu fico um tanto incomodado com syntaxes similares à do
try
catch
do Javascript. Na minha opinão, a parte do catch
deveria ser
abstraída em uma outra função, separada do código que tenta executar a função
de caráter duvidoso, que pode dar erro a qualquer momento.
Na hora lembrei de Go, onde error
é um tipo da linguagem e as funções podem
retornar um valor desse tipo, indicando que ela deu errado. Ai você teria que
lidar com isso manualmente (que, na minha opinião, é válido de crítica, mas
isso está muito fora do escopo deste artigo).
1func main() {
2 foo, err := foo.Connect("id")
3 if err != nil {
4 log.Fatal(err)
5 }
6}
Mas isso não é totalmente ruim, nada me impede de simplesmente ignorar que esse
erro existe trocando o err
por um _
e pronto! Essa liberdade que é
interessante. Mas não vejo necessidade disso em uma linguagem que tenha o try
catch
.
Exemplo de solução com Javascript
Ok, agora vamos para o Javascript. E se na nessa função hipotética
Foo.connect()
podesse tratar do sucesso, mas dar a responsabilidade do erro
para outra função? E que função? Acredito que o usuário – que pra todos os
efeitos e propósitos, também será um desenvolvedor – queira fazer isso do jeito
dele, portanto, a função que pode manipular o erro pode ser passada atravéz de
um dos parâmetros.
Antes disso, um conceito muito importante que fez diferença nesse assunto: todas
as funções em Javascript retornam o tipo undefined
por padrão (e no caso do
Python, é None
). Isso tudo talvez faça mais sentido com um exemplo prático:
1Foo = {
2 connect(id, handler) {
3 try {
4 /* corpo principal da função (que pode dar errado) */
5 } catch (err) {
6 handler(err);
7 }
8 },
9};
10
11Foo.connect("id", (err) => {
12 throw err;
13});
Acontece que ainda tem um problema, e se o usuário não quiser lidar com o erro? Então, o título dessa sessão não se refere à função anônima na chamada da função, e sim em sua definição.
Note que no exemplo abaixo, o usuário escolhe se vai querer tratar o erro ou
não, se não ele retorna undefined
– que é justamente o que função normalmente
faria –, mas se ele quiser tratar, a função vai retornar o que a função que
está tratando do erro quiser retornar.
1Handler = {
2 fooCouldNotConnect(err) {
3 throw err;
4 },
5};
6
7Foo = {
8 connect(id, handler = () => undefined) {
9 try {
10 /* corpo principal da função (que pode dar errado) */
11 } catch (err) {
12 return handler(err);
13 }
14 },
15};
16
17// manipulando o erro manualmente
18Foo.connect("id", Handler.fooCouldNotConnect);
19
20// "não há problema se ele não existir"
21Foo.connect("id");
Vamos tentar brincar um pouquinho com isso, e se eu quiser que uma função, que
possa dar errado e foi construída com essa estrutura do try
catch
, persista
em ficar executando ela denovo e denovo, até finalmente ela funcionar? I don’t
know man, let’s try it!
1Handler = {
2 fooCouldNotConnect(err) {
3 console.log(err)
4 return true;
5 },
6};
7
8Foo = {
9 connect(id, handler = () => undefined) {
10 try {
11 /* corpo principal da função (que pode dar errado) */
12 } catch (err) {
13 return handler(err);
14 }
15 },
16};
17
18while (Foo.connect("id", Handler.fooCouldNotConnect)) {
19 /* nesse bloco o programa não fazer nada,
20 esperar um tempinho, ou fazer outra coisa
21 enquanto a a Foo.connect() tá de birra */
22}
O que torna Python especial
Isso pode ser facilmente adptado para funcionar em Python, já que ele têm uma syntaxe parecida. Mas uma feature legal que o Python têm é os decorators, e é justamente essa feature que vou explorar pra tornar isso tudo mais legível.
Em resumo, bem resumido, um decorator é só uma função que é executada antes da função que está sendo “decorada” (se me permite falar assim). Então dá pra abstrair mais ainda o código. Veja o exemplo baixo:
1class Decorators:
2 @staticmethod
3 def handle_func(handler=lambda: None):
4 # handler é a função que vai tratar o erro
5 def decorator(func):
6 # func é a função decorada
7 def wrapper(*args):
8 # e *args é os argumentos da função decorada
9 try:
10 func()
11 except Exception as err:
12 return handler(err)
13 return wrapper
14 return decorator
15
16class Handler:
17 @staticmethod
18 def foo_could_not_connect(err):
19 raise err
20
21class Foo:
22 @staticmethod
23 @Decorators.handle_func(Handler.foo_could_not_connect)
24 def connect(id):
25 # corpo da função
Você pode argumentar que o decorator não está bonito, e serei obrigado a concordar. A função responsável por testar, e executar o handler que o usuário quer, tá um horror – talvez usar classes pra criar decorators seja o ideal. Mas o importante é que esse horror tá separado do código principal, que seria as duas classes de baixo nesse exemplo.
Vamos ver o nosso resultado com aquele exemplo da função persistente que mostrei com Javascript e comparar com essa minha soulão em Python fazendo o uso dos decorators:
1class Handler:
2 @staticmethod
3 def foo_could_not_connect(err):
4 print(err)
5 return True
6
7class Foo:
8 @staticmethod
9 @Decorators.handle_func(Handler.foo_could_not_connect)
10 def connect(id):
11 # corpo da função, sem blocos estranhos aqui
12
13while Foo.connect("id"):
14 pass
Exelente! Agora se você quiser executar a função Handler.foo_could_not_connect
caso a função Foo.connect
não funcione, é questão de adicionar uma linha a
mais, indicando qual função rodar se a outra falhar.
Mas lógico, isso não é totalmente igual aos outros exemplos, porquê o usuário
não terá a mesma liberdade de passar uma outra função como parâmetro, caso ele
não tenha acesso ou definido a função Foo.connect
ele mesmo. Mas mesmo assim,
não deixa de ser uma solução elegante e eficiênte, talvez não tão perfomática do
ponto de vista computacional, mas ainda assim, eficiênte.
Conclusão
Com essa brincadiera, percebi como Python é uma linguagem poderosa pro desenvolvedor, não só por ser fácil de entender e a syntaxe ser simplificada, mas também por ter esse tipo de feature que permite ser “preguiçoso”, no sentido de se esforçar agora pra depois ser mais produtivo no futuro. Apesar disso, Python tem seus defeitos (vários, cá entre nós), mas é inegável que ela foi desenhada pra tornar os desenvolvedores mais eficiêntes e ela faz isso muito bem.
#Programming #Python #Idea #Personal #Archived
comments powered by Disqus