|
| 1 | +title: A armadilha dos argumentos com valores padrão |
| 2 | +Slug: a-armadilha-dos-argumentos-com-valores-padrao |
| 3 | +Date: 2015-06-07 11:00 |
| 4 | +Tags: python,mutable,function,class,anti-pattern |
| 5 | +Author: Diego Garcia |
| 6 | +Email: drgarcia1986@gmail.com |
| 7 | +Github: drgarcia1986 |
| 8 | +Site: http://www.codeforcloud.info |
| 9 | +Twitter: drgarcia1986 |
| 10 | +Linkedin: drgarcia1986 |
| 11 | +Category: anti-patterns |
| 12 | + |
| 13 | + |
| 14 | +<figure style="float:left;"> |
| 15 | +<img src="/images/drgarcia1986/is_a_trap.png"> |
| 16 | +</figure> |
| 17 | +</br> |
| 18 | +Algo muito comum em várias linguagens de programação é a possibilidade de definir _valores default_ (valores padrão) para argumentos de funções e métodos, tornando a utilização desses opcional. |
| 19 | +Isso é ótimo, principalmente para manter retrocompatibilidade, porém, o python possui uma pequena armadilha que caso passe despercebida, pode causar sérios problemas, muitas vezes difíceis de serem detectados. |
| 20 | +Essa armadilha ocorre quando usamos valores de tipos `mutáveis` como valor default de argumentos. |
| 21 | + |
| 22 | +<!-- MORE --> |
| 23 | + |
| 24 | +### O que são tipos mutáveis e imutáveis? |
| 25 | +Segundo a [documentação oficial do python](https://docs.python.org/3.4/reference/datamodel.html), o valor de alguns objetos pode mudar, esses objetos que podem ter seu valor alterado após serem criados são chamados de mutáveis, enquanto que os objetos que não podem ter seus valores alterados após serem criados são chamados de imutáveis (simples assim). |
| 26 | + |
| 27 | +* **Tipos mutáveis**: |
| 28 | + |
| 29 | +Listas, Dicionários e tipos definidos pelo usuário. |
| 30 | + |
| 31 | +* **Tipos imutáveis**: |
| 32 | + |
| 33 | +Numeros, Strings e Tuplas. |
| 34 | + |
| 35 | +> Apesar de serem imutáveis, a utilização de um valor mutável (uma lista por exemplo) dentro de uma tupla, pode causar o efeito _[tuplas mutáveis](http://pythonclub.com.br/tuplas-mutantes-em-python.html)_, onde visualmente o valor da tupla é alterado, mas por trás dos panos o valor da tupla não muda, o que muda é o valor do objeto pelo qual a tupla está se referenciando. |
| 36 | +
|
| 37 | +### A armadilha |
| 38 | +Como disse no começo desse blogpost, é muito comum a utilização de valores default em agurmentos de funções e métodos, por essa razão, nos sentimos seguros em fazer algo desse tipo: |
| 39 | + |
| 40 | +```python |
| 41 | +def my_function(my_list=[]): |
| 42 | + my_list.append(1) |
| 43 | + print(my_list) |
| 44 | +``` |
| 45 | + |
| 46 | +Porém, levando esse exemplo em consideração, o que irá acontecer se invocarmos essa função 3 vezes? |
| 47 | + |
| 48 | +```python |
| 49 | +>>> my_function() |
| 50 | +[1] |
| 51 | +>>> my_function() |
| 52 | +[1, 1] |
| 53 | +>>> my_function() |
| 54 | +[1, 1, 1] |
| 55 | +``` |
| 56 | +Sim, o valor do argumento `my_list` mudou em cada vez que executamos a função sem passar algum valor para ele. |
| 57 | + |
| 58 | +### Por que isso acontece? |
| 59 | +Isso acontece porque o python processa os valores default de cada argumentos de uma função (ou método) quando essa for definida, após esse processamento o valor é atribuido ao objeto da função. |
| 60 | +Ou seja, por questões de optimização, seguindo nosso exemplo, o python não cria uma lista vazia para o argumento `my_list` a cada vez que a função `my_function` for invocada, ele reaproveita uma lista que foi criada no momento em que essa função foi importada. |
| 61 | + |
| 62 | +```python |
| 63 | +>>> my_function.func_defaults |
| 64 | +([],) |
| 65 | +>>> id(my_function.func_defaults[0]) |
| 66 | +140634243738080 |
| 67 | +>>> my_function() |
| 68 | +[1] |
| 69 | +>>> my_function.func_defaults |
| 70 | +([1],) |
| 71 | +>>> id(my_function.func_defaults[0]) |
| 72 | +140634243738080 |
| 73 | +>>> my_function() |
| 74 | +[1, 1] |
| 75 | +>>> my_function.func_defaults |
| 76 | +([1, 1],) |
| 77 | +>>> id(my_function.func_defaults[0]) |
| 78 | +140634243738080 |
| 79 | +``` |
| 80 | +> Note que a identificação do argumento (no caso `my_list`) não muda, mesmo executando a função várias vezes. |
| 81 | +
|
| 82 | +Outro exemplo seria utilizar o resultado de funções como valores default de argumentos, por exemplo, uma função com um argumento que recebe como default o valor de `datetime.now()`. |
| 83 | + |
| 84 | +```python |
| 85 | +def what_time_is_it(dt=datetime.now()): |
| 86 | + print(dt.strftime('%d/%m/%Y %H:%M:%S')) |
| 87 | +``` |
| 88 | +O valor do argumento `dt` sempre será o _datetime_ do momento em que o python carregou a função e não o _datetime_ de quando a função foi invocada. |
| 89 | + |
| 90 | +```python |
| 91 | +>>> what_time_is_it() |
| 92 | +07/06/2015 08:43:55 |
| 93 | +>>> time.sleep(60) |
| 94 | +>>> what_time_is_it() |
| 95 | +07/06/2015 08:43:55 |
| 96 | +``` |
| 97 | + |
| 98 | +### Isso também acontece com classes? |
| 99 | +Sim e de uma forma ainda mais perigosa. |
| 100 | + |
| 101 | +```python |
| 102 | +class ListNumbers(): |
| 103 | + def __init__(self, numbers=[]): |
| 104 | + self.numbers = numbers |
| 105 | + |
| 106 | + def add_number(self, number): |
| 107 | + self.numbers.append(number) |
| 108 | + |
| 109 | + def show_numbers(self): |
| 110 | + print(numbers) |
| 111 | +``` |
| 112 | +Assim como no caso das funções, no exemplo acima o argumento `numbers` é definido no momento em que o python importa a classe, ou seja, a cada nova instância da classe `ListNumbers`, será aproveitada a mesma lista no argumento `numbers`. |
| 113 | + |
| 114 | +```python |
| 115 | +>>> list1 = ListNumbers() |
| 116 | +>>> list2 = ListNumbers() |
| 117 | +>>> list1.show_numbers() |
| 118 | +[] |
| 119 | +>>> list2.show_numbers() |
| 120 | +[] |
| 121 | +>>> list2.add_number(1) |
| 122 | +>>> list1.show_numbers() |
| 123 | +[1] |
| 124 | +>>> list2.show_numbers() |
| 125 | +[1] |
| 126 | +>>> list1.numbers is list2.numbers |
| 127 | +True |
| 128 | +``` |
| 129 | + |
| 130 | +### Por que isso não acontece com Strings? |
| 131 | +Porque strings são `imutáveis`, o que significa que a cada alteração de valor em uma variavel que armazena uma strings, o python cria uma nova instância para essa variável. |
| 132 | + |
| 133 | +```python |
| 134 | +>>> a = 'foo' |
| 135 | +>>> id(a) |
| 136 | +140398402003832 |
| 137 | +>>> a = 'bar' |
| 138 | +>>> id(a) |
| 139 | +140398402003872 # o penúltimo número muda :) |
| 140 | +``` |
| 141 | + |
| 142 | +Em argumentos com valores default, não é diferente. |
| 143 | + |
| 144 | +``` |
| 145 | +def my_function(my_str='abc'): |
| 146 | + my_str += 'd' |
| 147 | + print(my_str) |
| 148 | +``` |
| 149 | +No exemplo acima, sempre que for executado o `inplace add` (`+=`) será criada outra váriavel para `my_str` sem alterar o valor default do argumento. |
| 150 | + |
| 151 | +```python |
| 152 | +>>> my_function() |
| 153 | +abcd |
| 154 | +>>> my_function.func_defaults |
| 155 | +('abc',) |
| 156 | +>>> my_function() |
| 157 | +abcd |
| 158 | +>>> my_function.func_defaults |
| 159 | +('abc',) |
| 160 | +``` |
| 161 | + |
| 162 | +### Como se proteger? |
| 163 | +A maneira mais simples de evitar esse tipo de surpresa é utilizar um [valor sentinela](http://en.wikipedia.org/wiki/Sentinel_value) como por exemplo `None`, nos argumentos opcionais que esperam tipos mutáveis: |
| 164 | + |
| 165 | +```python |
| 166 | +def my_function(my_list=None): |
| 167 | + if my_list is None: |
| 168 | + my_list = [] |
| 169 | + my_list.append(1) |
| 170 | + print(my_list) |
| 171 | +``` |
| 172 | + |
| 173 | +Ou, para deixar o código ainda mais elegante, podemos simplificar a condicional com um simples `or`: |
| 174 | + |
| 175 | +```python |
| 176 | +def my_function(my_list=None): |
| 177 | + my_list = my_list or [] |
| 178 | + my_list.append(1) |
| 179 | + print(my_list) |
| 180 | +``` |
| 181 | +> Obrigado [Bruno Rocha](http://pythonclub.com.br/author/bruno-cezar-rocha.html) pela sugestão. |
| 182 | +
|
| 183 | +Pronto, sem surpresas e sem armadilhas :). |
| 184 | + |
| 185 | +```python |
| 186 | +>>> my_function() |
| 187 | +[1] |
| 188 | +>>> my_function() |
| 189 | +[1] |
| 190 | +>>> my_function() |
| 191 | +[1] |
| 192 | +``` |
| 193 | + |
| 194 | +### Referências |
| 195 | + |
| 196 | +* [Fluent Python (Mutable types as parameter defaults: bad idea)](http://shop.oreilly.com/product/0636920032519.do) |
| 197 | +* [Python Anti-Patterns (Using a mutable default value as an argument)](http://docs.quantifiedcode.com/python-anti-patterns/correctness/mutable_default_value_as_argument.html) |
0 commit comments