Já escrevi aqui no site sobre curto-circuito nos operadores or
e and
da linguagem Python. Neste artigo, vamos verificar que o conceito de curto-circuito também se aplica às funções all
e any
, presentes numa instalação padrão da linguagem. E, mais, vamos ver a utilidade de cada uma dessas funções e cuidados ao utilizá-las com base em uma análise de complexidade.
Tópicos
all
Vamos começar verificando o que a documentação informa sobre a função:
help(all)
Help on built-in function all in module builtins: all(iterable, /) Return True if bool(x) is True for all values x in the iterable. If the iterable is empty, return True.
Como indica documentação, se cada valor x
de um iterável passado para a função retornar True
em bool(x)
, a função retorna True
. Um iterável, de acordo com a documentação, nada mais é que um objeto que pode retornar seus membros um de cada vez como, por exemplo, tipos sequenciais como listas, strings e tuplas. Usando uma lista como exemplo, podemos verificar que é um iterável através de um simples loop for
:
lista_exemplo = [1, 2, 3, 4]
for numero in lista_exemplo:
print(numero)
1 2 3 4
O que for
faz é passar o iterável, no exemplo a lista, para a função iter()
, que retorna um iterador, do qual o for
solicita cada item sucessivamente através de uma chamada next()
até que o o iterável seja completamente consumido. No exemplo, a lista é consumida passando cada item sucessivamente para a função print
.
Caso ainda tenha ficado em dúvida, recomendo fortemente a leitura desse artigo sobre iteradores e iteráveis onde é explicado detalhadamente o conceito com diversos exemplos.
Vamos para um exemplo simples, passando algumas sequências para a função all
:
todos_true = (True, True, True)
all(todos_true)
True
Se pelo menos um valor for False
, o retorno da função será False
:
alguns_true = (True, False, True)
all(alguns_true)
False
Já vimos em outros artigos que o interpretador considera alguns casos casos como False
. Eis alguns exemplos:
print(bool(False)) # a própria palavra reservada False
print(bool(None)) # a palavra reservada None
print(bool(0)) # o inteiro 0 (zero)
print(bool("")) # uma string vazia
print(bool(())) # uma tupla vazia
print(bool([])) # uma lista vazia
print(bool({})) # um dicionário vazio
False False False False False False False
Assim, podemos fazer um exemplo mais completo no qual o retorno da função all
é True
:
todos_true = (True, "Chico", 1, (1, 2), [1, 2], {'a': 1, 'b': 2})
all(todos_true)
True
O exemplo a seguir retorna False
por possir uma string vazia entre os valores passados:
alguns_true = (True, "", 1, (1, 2), [1, 2], {'a': 1, 'b': 2})
all(alguns_true)
False
Já o seguinte exemplo retorna False
por possir um inteiro de valor zero entre os valores passados:
alguns_true = (True, "Chico", 0, (1, 2), [1, 2], {'a': 1, 'b': 2})
all(alguns_true)
False
Espero que esses exemplos tenham sido o suficiente para entender o retorno da função. Nos dois últimos, por existir ao menos um elemento que retorna False
ao ser passado como argumento para bool()
, o retorno de all()
será False
. Agora, será que é necessário analisar cada valor passado para ter o retorno? Para avaliar isso, vamos criar a seguinte função:
def funcao_executada(valor):
print("Fui executada!")
return valor
Observe que é uma função bem simples, que recebe um determinado valor, que pode ser de qualquer tipo, faz o print
da string “Fui executada!” e retorna o mesmo valor passado. Vamos fazer um pequeno teste passando a string “oi” como argumento da função:
funcao_executada('oi')
Fui executada!
'oi'
Observe a presença da mensagem “Fui executada!” e do retorno da string passada para a função. Mas qual o objetivo desse função?
Considere a célula de código abaixo. Nela há uma lista denominada lista_teste
. Veja que todos os itens dela retornam True
caso sejam passados como argumento da função bool
, exceto o terceiro item, por ser o inteiro zero. Isso significa que o retorno da passagem de tal lista par a função all
deve ser False
. Mas será que a função all
precisa avaliar todos os valores?
Para descobrir, vamos criar um gerador usando uma generator expression. Tal expressão origina um gerador que, ao invés de retornar de uma única vez todos os valores, os retorna sob demanda. Para maiores detalhes veja este artigo do site sobre geradores. Veja nas células abaixo como a expressao_geradora
retorna cada valor sob demanda da função next
:
lista_teste = [True, "Chico", 0, (1, 2), [1, 2], {'a': 1, 'b': 2}]
expressao_geradora = (funcao_executada(item) for item in lista_teste)
expressao_geradora
<generator object <genexpr> at 0x7f2b1d5eec10>
next(expressao_geradora)
Fui executada!
True
next(expressao_geradora)
Fui executada!
'Chico'
next(expressao_geradora)
Fui executada!
0
Acho que já deu para entender, poderíamos continuar solicitando cada item até a lista ser toda consumida. Caso queira todos os valores restantes de uma única vez, basta passar o gerador para um container como, por exemplo, uma lista:
list(expressao_geradora)
Fui executada! Fui executada! Fui executada!
[(1, 2), [1, 2], {'a': 1, 'b': 2}]
Agora o gerador foi exaurido, de forma que um novo next
retornará uma exceção StopIteration
:
next(expressao_geradora)
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-16-7ac23c2b0c8d> in <module> ----> 1 next(expressao_geradora) StopIteration:
Observe que o comportamento de um gerador é de um iterável, conforme conceito do início do artigo. Como o gerador foi exaurido, vamos criá-lo novamente e passá-lo para a função all
:
expressao_geradora = (funcao_executada(item) for item in lista_teste)
all(expressao_geradora)
Fui executada! Fui executada! Fui executada!
False
Observe que foram apresentadas três vezes a mensagem “Fui executada!”. Faz sentido, pois os dois primeiros itens retornam True
ao serem passados como argumento da função bool
, enquanto que o terceiro, o inteiro zero, retorna False
. Como o objetivo da função all
é retornar True
apenas se todos os itens retornarem True
, não é necessário avaliar os demais itens após achar um caso de False
. Assim, demonstramos que a função all
apresenta comportamento de curto-circuito.
OK, mas o que fazer com essa informação? Qual a consequência prática disso? Vamos ver o que acontece se for passado um grande iterável, no caso uma lista de um milhão de valores:
lista_gigante_todos_true = [True] * 1_000_000
Todos os valores dessa lista são True
, isso significa que o retorno da função all
deve ser True
. Mas, pelo que vimos anteriormente, a função passa por cada valor antes de dar seu retorno. Já vimos nesse artigo que o IPython e o Jupyter Notebook têm funções próprias (“mágicas” na documentação) que permitem fazer algumas análises interessantes. Uma delas é a %timeit
, que permite medir o tempo que um dado comando demora. Como esse artigo está sendo escrito em um Jupyter Notebook, podemos usar essa função. Vamos ver o tempo passando essa lista com um milhão de valores True
:
%timeit all(lista_gigante_todos_true)
3.72 ms ± 74.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Vamos comparar esse dado com o tempo de listas que possuem ao menos um valor False
. Já sabemos que o retorno será False
, mas em quanto tempo? Comecemos gerando uma lista, também com um milhão de itens, com valor False
logo no início, no índice 10:
lista_gigante_com_false_indice_10 = lista_gigante_todos_true.copy()
lista_gigante_com_false_indice_10[10] = False
%timeit all(lista_gigante_com_false_indice_10)
98.1 ns ± 0.44 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Observe a grande diferença! Saímos de milisegundos (10-3 segundos) para nanossegundos (10-9 segundos). Uma melhoria de um milhão de vezes! Mas isso porque o valor estava logo no início, vamos criar outra lista de um milhão de itens, sendo apenas um False
mas na última posição:
lista_gigante_com_false_ultimo_indice = lista_gigante_todos_true.copy()
lista_gigante_com_false_ultimo_indice[-1] = False
%timeit all(lista_gigante_com_false_ultimo_indice)
3.8 ms ± 123 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Observe que voltamos novamente para a casa de milisegundos, valor quase igual ao da lista com todos os valores True
. Faz sentido, cada valor foi analisado sequencialmente até chegar ao último.
Apenas para fechar o raciocínio, vamos colocar um único False
, mas exatamente no meio da lista:
lista_gigante_com_false_indice_meio = lista_gigante_todos_true.copy()
lista_gigante_com_false_indice_meio[499_999] = False
%timeit all(lista_gigante_com_false_indice_meio)
1.83 ms ± 3.31 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Como esperado, obtivemos um valor que é praticamente a metade do anterior.
Ou seja, temos uma função de comportamento linear no que diz respeito à uma análise de complexidade temporal. No pior caso possível, terá que avaliar todos os itens para conseguir ter retorno. Tenha isso em mente se pretende usar tal função em uma grande quantidade de dados, especialmente se houver motivos para acreditar que haverá poucos False
, pois se estiverem no final do iterável um grande tempo será necessário. Caso queira ver o código fonte em C dessa função e se certificar da implementação clique aqui.
any
Vamos seguir o mesmo roteiro feito anteriormente para a função all
. Começando pela documentação:
help(any)
Help on built-in function any in module builtins: any(iterable, /) Return True if bool(x) is True for any x in the iterable. If the iterable is empty, return False.
Novamente, tal função recebe um iterável. Mas, neste caso, retorna True
se ao menos um dos itens tiver retorno True
ao ser passado como argumento da função bool
. Vamos ver dois exemplos. No primeiro, passamos apenas valores False
:
todos_false = (False, False, False)
any(todos_false)
False
Como era de se esperar, tivemos o retorno False
. Se ao menos um dos valores passados for True
, já é o suficiente para termos retorno True
:
alguns_false = (False, True, False)
any(alguns_false)
True
Como já visto, não necessariamente precisamos passar diretamente True
ou False
, podemos passar outros tipos que terão retornos True
ou False
ao serem passados para a função bool
. Vamos redefinir nossa variável lista_teste
a seguir. Observe que os três primeiros itens retornam False
ao serem passados para a função bool
(veja a célula de código 5 acima caso tenha dificuldade de entender). Vamos passar tal lista para nossa expressão geradora:
lista_teste = [False, "", 0, (1, 2), [1, 2], {'a': 1, 'b': 2}]
expressao_geradora = (funcao_executada(item) for item in lista_teste)
expressao_geradora
<generator object <genexpr> at 0x7f2b1d5eef20>
E passar tal expressão para a função any
:
any(expressao_geradora)
Fui executada! Fui executada! Fui executada! Fui executada!
True
Veja que a função foi executada 4 vezes. Ao encontrar o primeiro item que retorna True
não é mais necessário continuar consumindo o gerador, pois um True
é o suficiente para o retorno da função como True
. Novamente, um comportamento de curto-circuito.
Assim, como fizemos para all
, bamos ver o que acontece se for passado um grande iterável, no caso uma lista de um milhão de valores:
lista_gigante_todos_false = [False] * 1_000_000
Todos os valores dessa lista são False
, isso significa que o retorno da função any
deve ser False
. Mas, pelo que vimos anteriormente, a função passa por cada valor antes de dar seu retorno. Vamos ver o tempo passando essa lista com um milhão de valores False
:
%timeit any(lista_gigante_todos_false)
3.18 ms ± 93.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Vamos comparar esse dado com o tempo de listas que possuem ao menos um valor True
. Já sabemos que o retorno será True
, mas em quanto tempo? Comecemos gerando uma lista, também com um milhão de itens, com valor True
logo no início, no índice 10:
lista_gigante_com_true_indice_10 = lista_gigante_todos_false.copy()
lista_gigante_com_true_indice_10[10] = True
%timeit any(lista_gigante_com_true_indice_10)
94.4 ns ± 0.355 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Observe a grande diferença! Saímos de milisegundos (10-3 segundos) para nanossegundos (10-9 segundos). Uma melhoria de um milhão de vezes! Mas isso porque o valor estava logo no início, vamos criar outra lista de um milhão de itens, sendo apenas um True
mas na última posição:
lista_gigante_com_true_ultimo_indice = lista_gigante_todos_false.copy()
lista_gigante_com_true_ultimo_indice[-1] = True
%timeit any(lista_gigante_com_true_ultimo_indice)
3.18 ms ± 41.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Observe que voltamos novamente para a casa de milisegundos, valor quase igual ao da lista com todos os valores False
. Faz sentido, cada valor foi analisado sequencialmente até chegar ao último.
Apenas para fechar o raciocínio, vamos colocar um único True
, mas exatamente no meio da lista:
lista_gigante_com_true_indice_meio = lista_gigante_todos_false.copy()
lista_gigante_com_true_indice_meio[499_999] = True
%timeit any(lista_gigante_com_true_indice_meio)
1.54 ms ± 3.18 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Como esperado, obtivemos um valor que é praticamente a metade do anterior.
Ou seja, assim como a all
, a any
também é uma função de comportamento linear no que diz respeito à uma análise de complexidade temporal. No pior caso possível, terá que avaliar todos os itens para conseguir ter retorno. Tenha isso em mente se pretende usar tal função em uma grande quantidade de dados, especialmente se houver motivos para acreditar que haverá poucos True
, pois se estiverem no final do iterável um grande tempo será necessário. Caso queira ver o código fonte em C dessa função e se certificar da implementação clique aqui.
Conclusão
Por esse artigo é isso. Espero que tenha compreendido e aprendido algo novo. Passamos por diversos conceitos, como iteráveis, iteradores, funções geradores e fizemos um pouco de análise de complexidade temporal de algoritmos. Muito valor agregado, tenho certeza.
Aproveite para acompanhar o Ciência Programada nas redes sociais.
Gostou desse artigo? Ele faz parte do Python Drops, um conjunto de posts mais curtos voltados para fundamentos falando sobre alguns aspectos da linguagem Python e de programação em geral. Você pode ler mais desses artigos buscando a tag “drops” aqui no site. Até a próxima!
Observação: o tempo gerado pela %timeit
pode variar de máquina para máquina, não necessariamente você obterá os mesmos valores. No entanto, a diferença de ordens de grandeza deve se manter.