Coisas bizarras de Python

June 10, 2008

Venho trabalhado nos meus projetos pessoais com Python há um certo tempo e estou bem satisfeito. Esta linguagem tem sido um fator importantíssimo na minha produtividade, e tem me permitido por minhas idéias no ar de forma estruturada e com qualidade em tempos recordes.

Mas acho importante identificar pontos falhos que possam ser melhorados. Ter uma visão crítica de uma linguagem não a desmerece e não me faz ser menos entusiasta. Foi assim com Java, é assim com Python.

Até agora, a única caracteristica de Python que realmente me incomodava era o fato de variáveis locais não terem escopo de bloco. Ou seja: uma variável criada dentro de um if existe após ele. Muitos consideram isso como um ponto que pode gerar vários erros bestas (e de fato pode), mas se bem utilizado, pode ser bem poderoso. Resumindo: requer cuidado, mas pode ser útil. Ninguém é perfeito…

Mas existem alguns limites…e hoje descobri uma característica que me incomoda. Não a ponto de eu deixar a linguagem, porque ainda acho que os benefícios dela são gigantescos. Mas incomoda. E fico feliz de ter descoberto isso antes de ter passado horas e horas quebrando a cabeça com comportamentos bizarros.

Chega de preâmbulos, vamos ao ponto:

def f(item, lista=[]):

 lista.append(item)

 print lista

Se executarmos a a função uma primeira vez, passando apenas um argumento, o resultado é o esperado: f(1) imprime [1] na tela. Mas se executarmos uma segunda vez, em vez de imprimir [2], a função f imprime [1,2]. Eu custei a acreditar nisso, e tive que confirmar pessoalmente.

Fazendo uma busca no Google, descobri na documentação oficial (http://docs.python.org/ref/function.html) que isto realmente é uma característica mapeada da linguagem:

Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that that same “pre-computed” value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function [...]

Ok, pelo menos está mapeado. Mas a frase texto acima merece “This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function” merece destaque. Afinal, logo após a documentação descrever a funcionalidade, ela reconhece que o efeito é indesejado (afinal, parâmetro de função tem escopo local), e ainda por cima descreve a Solução Rápida de Eficiência Duvidosa, a famosa Gambiarra (ou workaround, no jargão técnico). Ou seja, o problema poderia ser resolvido na fonte e as funções de Python poderiam ter o mesmo comportamento das funções de outras linguagens.

Infelizmente, acabei de dar uma olhada na doc do futuro Python 3.0, e a feature permanece……

tags:
posted in Desenvolvimento, Opinião by Miguel Galves

Follow comments via the RSS Feed | Leave a comment | Trackback URL

  • http://rnaufal.livejournal.com Rafael Naufal

    É uma feature que poderia ser evitada desde o início, por sinal, pelo fato de poder gerar um bug em que o desenvolvedor tenha que passar horas e horas e não conseguir achá-lo. Se está documentado na API que

    “This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function”
    , isto já deveria ser abstraído do desenvolvedor. Para resolver o problema, teria que ser feito:

    def f(item, lista=[]): lista = lista or [] lista.append(item) print lista

    ou

    def f(item, lista=None): if lista is None: lista = [] lista = lista or [] lista.append(item) print lista

    O que acho que às vistas do desenvolvedor não fica muito claro. Principalmente se isto se espalhar pelo sistema e alguém novo na equipe esquecer de fazer a verificação no início do método.

  • Miguel

    Eu chamo isso de erro conceitual….processar a lista de parametros inicialmente eh uma coisa. Permitir que estes parametros sejam acessados a cada chamada eh bem diferente…

  • http://caioromao.com Caio Romão

    A impressão que dá é que a passagem de valor padrão para parâmetro não fazia parte da especificação inicial (e não, não seria trivial adicionar tal funcionalidade sem quebrar outras coisas).

    Eu particularmente nunca gostei de valor padrão passado na assinatura, não me parece coeso.

    Enfim, só um comentário sobre o que o Rafael escreveu: None avalia pra falso, então a seguinte já basta para resolver o “problema”

    def f(item, lista=None): lista = lista or [] lista.append(item) print lista

  • http://marinho.webdoisonline.com/ Marinho Brandão

    Bom… não entendi onde está o problema. Nós temos aí uma questão referencia X valor. Quando se passa um objeto e se acessa o metodo ou atributo deste objeto do outro lado, é natural que o mesmo seja acessado e não uma cópia dele.

    E eu não sei quanto a outras linguagens, mas o Delphi tem esse mesmo comportamento.

  • Miguel

    Marinho,

    o grande problema é que eu suponho que se eu não estou passando o argumento lista (usando o codigo acima como exemplo), então a função deveria ter acesso à uma cópia nova do valor default. Se eu passo um objeto mutavel como parâmetro, é natural que ele sofra modificações no retorno. Mas quando eu passo o objeto, EU tenho a referência a este objeto. No caso discutido aqui, a referência a este objeto fica perdida no limbo, e só pode ser acessada de dentro da função, o que realmente me parece inútil. Resumindo: temos uma referencia perdida por ai armazenando lixo…

  • http://swen.uwaterloo.ca/~ttonelli/ Thiago

    Faz anos que eu nao programo em python (meu ultimo toy example foi em 2001), mas eu suponho que tenha garbage collection, certo? E se a referencia pra essa lista fica perdida e nunca pode ser coletada, significa que se eu fizer algo como o que voce fez eu nunca mais vou poder coletar o que tiver referenciado por item? Quer dizer:

    def f(item, lista=[]): lista.append(item)

    causa item ficar pra sempre na lista e entao ele nao pode ser coletado. Ou python trata esses tipos de referencia como weak reference? Ou eu nao to prestando atencao em algum detalhe?

  • http://cafeina.pro.br Pedro de Medeiros
    No caso discutido aqui, a referência a este objeto fica perdida no limbo, e só pode ser acessada de dentro da função, o que realmente me parece inútil. Resumindo: temos uma referencia perdida por ai armazenando lixo…

    Ela não está realmente perdida, nem me parece tão inútil assim. Esta variação do código retorna a tal lista (que acredito ser mais útil do que só imprimi-la):

    >>> def f(item, lista=[]): >>> lista.append(item) >>> return lista

    Uma utilidade para tal função é criar um objeto de lista “anônima” que só pode ser obtida quando você executa a função acima. Um tipo de “lista singleton”, se desejar. Acho que é possível pensar numa utilidade pra isso, especialmente em closures (onde é interessante obter um objeto com uma referência fixa) ou como uma alternativa para funções com variáveis locais estáticas (como no C).

  • http://marinho.webdoisonline.com/ Marinho Brandão

    Miguel,

    sim, eu entendo, mas a base do raciocínio é a mesmo. Eu discordo e acho que você está fazendo tempestade em copo d’água.

  • Miguel

    Marinho,

    acho que estou apenas comentando um ponto que me desagrada e usando este espaço para sucitar discussão. Isto não é fazer tempestade em copo d’agua, é ter senso crítico e discutir com a comunidade sobre caracteristicas da linguagem.

  • http://1up4dev.wordpress.com/ Andre

    Miguel,

    Eu não fiquei tão surpreso quando vi esse comportamento. No Java também é assim. E acho que na maioria das linguagens OO.

    public class Listas { public void f(Integer item, ArrayList lista){ lista.add(item); System.out.println(lista); }

    public static void main( String[] args) { Listas lista = new Listas(); ArrayList array = new ArrayList(); lista.f(new Integer(1), array); lista.f(new Integer(2), array); } }

    Isso imprime [1] e [1, 2]. Acho que isso é um problema da Orientação a Objeto (se isso for realmente um problema), porque é passado a referencia do Objeto (como em Python tudo é Objeto). Na verdade é passado para o método a referencia ao Objeto, então se o objeto for alterado as variáveis com referencia a ele sofrerão o impacto.

  • http://swen.uwaterloo.ca/~ttonelli/ Thiago

    @Andre

    Pelo que entendi o problema que o Miguel tah comentando eh outro. Em orientação objetos (e ateh em C mesmo) a semântica correta eh que quando o método (ou procedimento) recebe uma referencia (ou ponteiro) e altera o resultado, vai estar alterando a fonte.

    O problema que ele tah falando eh quando o método tem um valor padrão. Imagine que voce pudesse fazer isso em Java (baseado no seu código):

    // NAO eh java valido public class Listas { public void f(Integer item, ArrayList lista = new ArrayList()){ lista.add(item); System.out.println(lista); }

    Quando voce usa esse codigo com:

    public static void main(String[] args) { Listas lista = new Listas(); lista.f(new Integer(1)); lista.f(new Integer(2)); }

    voce esperaria que em cada invocação do método f uma nova lista fosse criada (pois esta na assinatura do método que lista vazia eh o valor pra quando voce nao passa o argumento), mas nao eh o que acontece em Python: ele cria a lista na primeira chamada e reusa pra todas as outras chamadas.

  • http://1up4dev.wordpress.com/ Andre

    Thiago,

    Então eu entendi a “problematica”, mas sendo um valor default, teoricamente só será executado o new ArrayList() se a variável lista for nula.

  • http://swen.uwaterloo.ca/~ttonelli/ Thiago

    “… sendo um valor default, teoricamente só será executado o new ArrayList() se a variável lista for nula.”

    Exatamente esse eh o problema. Isso NAO acontece. O “new ArrayList()” nao eh executado se a variável lista for nula (ou nao existir). Ele eh executado “somente da 1a vez em que a variável lista for nula”. Na 2a vez, mesmo que voce mande um null, jah vai ter uma lista criada lah, possivelmente com dados.

  • http://1up4dev.wordpress.com/ Andre

    Entendido.

    Mas para mim isso é um comportamento Esperado, e não um comportamento Bizarro.

    >>> lst1 = [] >>> f(1, lst1) [1] >>> f(2, lst1) [1, 2] >>> f(1) [1] >>> f(2) [1, 2] >>>

    Cada variável com sua referencia.

  • http://cafeina.pro.br Pedro de Medeiros

    De qualquer forma, a assinatura da função é processada (executada) apenas uma vez e a lista criada na assinatura é declarada e referenciada nessa única vez. Pra quem conhece Python bem, isso não é realmente nenhuma surpresa.

  • Miguel

    Pedro,

    vendo o teu primeiro comentário, de fato a coisa parece ser menos ruim do que me pareceu no início. E de fato, como vc disse pra quem conhece Python bem, isso não é uma surpresa.

    Fato é que, para as situações que você citou como possibilidades para esta caracteristica de Python, eu prefiro uma aproximação usando OO, que tá aí justamente para encapsular este tipo de dados. E eu continuo achando que preferiria se a semantica de valor default fosse criado a cada vez, uma copia limpa, cujo escopo ficasse restrito àquela chamada. Gosto pessoal.

    Anyway, como eu mencionei no meu post, isto não é motivo algum para alguém deixar de utilizar a linguagem. Não existe linguagem perfeita, existe a linguagem que se adapta melhor ao seu estilo e às suas necessidades de projeto, e em algum momento sempre temos que fazer algumas concessões.

  • http://programandosemcafeina.blogspot.com Tiago Albineli Motta

    Nossa! Pra uma pessoa começando em Python esse comportamento é um chute lá embaixo.

  • http://ark4n.wordpress.com/2008/06/16/python-anomalias/ Python: Anomalias « A r k 4 n
  • http://bpfurtado.livejournal.com/ Bruno

    Meu mega chute agora: a origem do problema deve ser o fato de que o python “processa” a função como se fosse um objeto e o param default deve ser apenas um atributo (como seria em um obj qq) dai a preservação de seu estado entre as invocações. :)

  • Miguel

    Bruno, é bem isso mesmo: ao carregar o codigo, python faz o bind do codigo da função para uma variavel, e ja processa a lista de parametros, criando todas as instancias necessarias…..

  • nosklo

    Normal! Voce pode até acessar a lista depois, ela não é garbage collected pq fica armazenada na funçao:

    >>> def inclui(valor, lista=[]): … lista.append(valor) … >>> inclui(1) >>> inclui(2) … >>> print inclui.func_defaults ([1, 2],)

  • http://marcospereira.wordpress.com Marcos Silva Pereira

    Comportamento estranho mesmo. Para mim esse caso corrompe o principio da menor surpresa: http://en.wikipedia.org/wiki/Principleofleast_astonishment

    O que eu esperaria normalmente é que “lista” tivesse o mesmo ciclo de vida de qualquer outra variável dentro da função.

    Abraço…

  • http://bpfurtado.livejournal.com Bruno

    Wise words Marcos!

  • http://alicebob.cryptoland.net/pegadinha-com-closures-ilustrado-em-python/pt/ Pegadinha com closures (ilustrado em Python) | Alice and Bob in Cryptoland

    [...] possui instâncias diferentes para cada closure diferente criada. Outra alternativa é utilizar um “recurso” polêmico do Python, que é o fato de que os valores de parâmetros com valores padrão são avaliados quando a [...]

blog comments powered by Disqus

Switch to our mobile site

 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org