Fugindo Para as Colinas Com Python
2017-09-18 #python #companion post"Hello World" não mostra exatamente o poder de qualquer linguagem. Por isso resolvi fazer uma "introdução" ao Python com um problema de fujir para as colinas. Ou quase isso.
Um dos problemas de qualquer iniciante em qualquer linguagem é pegar o "feeling" da linguagem. E, realcionado com isso, é o fato que a maior parte dos códigos introdutórios de uma linguagem é o "hello world". Por exemplo:
print('hello world')
O que isso diz de Python? No máximo que print
precisa de parentêses,
strings podem ser geradas com aspas simples e não precisa de pronto-e-vírgula
no final.
O que não é muita coisa.
Uma coisa que eu sempre digito quando acontece algum problema é
Fujam para as colinas!
Só que repetir isso toda hora não me faz um cara muito popular. É por isso que eu fico mudando essa frase para coisas do tipo
Funam para as colijas!
Ou ainda
Lunam para as jocifas!
Obviamente eu não paro para ficar pensando em todas as possibilidades e fico alterando letras randomicamente manualmente. Eu tenho um script para isso. Um script em Python.
O Básico
print('Fujam para as colinas!')
Assim já podemos irritar as pessoas repetindo a mesma informação.
O próximo passo é preparar o terreno para a randomicidade de frase.
print('{}u{}am para as {}o{}i{}as!'.format('f', 'j', 'c', 'l', 'n'))
Agora já temos algumas coisas pra estudar.
Help incluso e format
Primeiro, format
. format
é um método dos objetos do tipo string. Como
eu sei disso? Porque, um dia, estava eu muito belo e folgado, me perguntando
"O que as strings em Python podem fazer?", abri o interpretador do Python e
digitei:
help(str)
E, lá no meio...
| format(...)
| S.format(*args, **kwargs) -> string
|
| Return a formatted version of S, using substitutions from args and kwargs.
| The substitutions are identified by braces ('{' and '}').
E uma das coisas legais do Python é que ele é capaz de buscar o tipo através
de um dado; e o que eu quero dizer com isso é que eu não precisaria saber que
o tipo de uma string é str
, eu poderia simplesmente fazer help('fujam para as colinas')
e o interpretador mostraria o mesmo help.
Aqui temos mais uma informação importante: *args
e **kwargs
. O que são
esses dois desgraçados?
Definindo Funções
Em outras linguagens esses são os chamados "variable arguments" ou "argumentos variáveis" ou ainda "varargs". Ao invés de definir uma função que tenha um número definido de parâmetros, varargs permite que a função tenha um número indefinido de parâmetros. E eles funcionam da seguinte forma:
Vamos começar definindo uma função:
def soma(primeiro, segundo):
total = primeiro + segundo
return total
Uma pequena pausa para falar de uma coisa que acabamos de ver de Python, que não tínhamos visto ainda: definição de funções e blocos.
Primeiro, funções são definidas com def
, seguidas do nome da função, um
parênteses, a lista de argumentos separados por vírgulas, fecha parênteses e
dois pontos. Em Python, os dois pontos indicam que haverá um início de bloco.
Segundo, ao contrário de outras linguagens, Python não usa colchetes para definir os blocos. Isso é feito através da identação (e, obviamente, os dois pontos).
Terceiro, Python é uma linguagem de tipagem dinâmica, o que significa que não se define o tipo do parâmetro, simplesmente se usa.
Em Python 3, é possível definir um "hint" para o tipo, da seguinte forma:
def soma(primeiro: Int, segundo: Int) -> Int:
return primeiro + segundo
A única coisa a se cuidar é que isso é só um hint e que se for passado uma string, não irá ocorrer qualquer erro.
Chamando Funções
Ainda, existem duas formas de passar valores para uma função:
A primeira é só chamar a função passando os argumentos:
soma(1, 2)
A segunda é que o Python aceita que sejam nomeados os argumentos:
soma(primeiro=1, segundo=2)
O interessante de se nomear os argumentos é que é possível passar os mesmos fora da ordem original da função:
soma(segundo=2, primeiro=1)
(Essa parte de nomear os argumentos é importante para entender o **kwargs
.)
De volta a Varargs
Mas voltando aos varargs, o important é notar que a função acima tem dois parâmetros. Se eu tentar chamar a função com um número diferente de argumentos, o Python vai reclamar:
>>> soma(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: soma() takes exactly 2 arguments (1 given)
varargs remove essa limitação. Se eu mudar a função para:
def soma(*args):
print(args)
O que *args
faz é pegar todos os argumentos e transformar numa lista. No
caso, se eu chamar:
soma(1, 2, 3)
O resultado seria:
[1, 2, 3]
E se eu chamar da forma original, com soma(1, 2)
, eu tenho: [#fixo]_
[1, 2]
Também é possível criar funções com parâmetros fixos e uma parte
variável, com algo do tipo def fun(param1, param2, *outros)
; se a
função for chamada com fun(1, 2)
, outros
ficará como uma lista
vazia ([]
); se for passado fun(1, 2, 3, 4)
, outros
ficará com 3
e 4, já que 1 pertence à param1
e 2 pertence à param2
.
O que nós temos aqui é uma lista de elementos. Para fazer o soma
funcionar
com uma lista ao invés de argumentos, teríamos que fazer o seguinte:
def soma(*argumentos):
total = 0
for valor in argumentos:
total = total + valor
return total
De novo, coisas novas:
De novo, blocos são marcados com dois-pontos e uma identação. Assim, o bloco
do for
tem uma linha só, porque o return
está no mesmo nível do
for
, ele só vai ser executado depois que o for
terminar.
E aqui vemos como percorrer elementos de uma lista: for/in
faz com que
seja percorrido cada elemento de argumentos
e o valor será colocado em
valor
.
Agora que vimos varargs e listas, existe uma coisa mágica do Python que o
*
, além de servir para receber um número variável de argumentos e
transformar numa lista, também serve para fazer o contrário: converter uma
lista para uma lista de argumentos.
De novo, com o nosso soma
original:
def soma(primeiro, segundo):
return primeiro + segundo
Eu posso chamar com:
soma(1, 2)
Mas eu também posso chamar com:
argumentos = [1, 2]
soma(*argumentos)
Varargs de kwargs
Nós vimos duas coisas relacionadas a chamadas de função:
- É possível criar funções com número variável de parâmetros, usando
*
. - É possível chamar funcões passando o nome do parâmetro.
O que acontece quando juntamos os dois?
>>> def fun(*args):
... print args
>>> fun(args=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fun() got an unexpected keyword argument 'args'
O problema aqui é que *
recolhe todos os argumentos sem nome. Para
recolher os com nomes, é preciso usar **
. Ele funciona praticamente da
mesma forma que *
mas ao invés de uma lista, ele irá conter um dicionário
-- também conhecido como "array associativo", "objeto", "mapa" e outros nomes,
dependendo da linguagem.
Por exemplo:
def fun(**names):
print names
fun(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fun() takes exactly 0 arguments (1 given)
O problema aqui é que não foi passado nenhum argumento nomeado. Obviamente o Python não sabe o que fazer com um valor qualquer e deu erro.
Agora, se a função for chamada com:
fun(name='Julio', age=41)
{'age': 41, 'name': 'Julio'}
Ou seja, é possível criar uma função que só aceita parâmetro nomeados, mas é preciso que os valores sejam buscados do dicionário ao invés de "aparecerem" no bloco pelos parâmetros.
Colocando tudo junto
Por que tudo isso é importante?
Porque, como foi visto no nosso primeiro código com o format
, o que a
gente precisa é passar um número variável de elementos
print('{}u{}am para as {}o{}i{}as!'.format('f', 'j', 'c', 'l', 'n'))
E nós precisamos alterar a ordem dos argumentos e a única forma que temos de fazer isso é usando o varargs reverso:
consoantes = ['f', 'j', 'c', 'l', 'n']
print('{}u{}am para as {}o{}i{}as!'.format(*consoantes)
Nesse momento, os dois códigos vão fazer a mesma coisa. A questão é que agora temos uma lista que podemos mexer no conteúdo.
O que precisamos fazer agora: Embaralhar o conteúdo de consoantes
. O resto
do código continua o mesmo, já que ele imprime as consoantes nos lugares
marcados e nós estamos passando a lista para isso.
Para randomizar o conteúdo, nós vamos utilizar uma das bibliotecas disponíveis
pelo próprio Python: random
.
Para usar uma biblioteca -- que no Python são chamadas de "módulos" --, é só
fazer import
e o nome da biblioteca. No nosso caso
import random
Mas o que diabos tem dentro de random
? Bom, dá pra ver tudo no site
oficial do Python, onde tem a documentação, ou nós podemos fazer o mesmo
help(random)
para ver o help ou ainda usar dir(random)
para ver o
conteúdo do módulo.
>>> import random
>>> dir(random)
['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'Random', 'SG_MAGICCONST',
'SystemRandom', 'TWOPI', 'WichmannHill', '_BuiltinMethodType',
'_MethodType', '__all__', '__builtins__', '__doc__', '__file__',
'__name__', '__package__', '_acos', '_ceil', '_cos', '_e', '_exp',
'_hashlib', '_hexlify', '_inst', '_log', '_pi', '_random', '_sin', '_sqrt',
'_test', '_test_generator', '_urandom', '_warn', 'betavariate', 'choice',
'division', 'expovariate', 'gammavariate', 'gauss', 'getrandbits',
'getstate', 'jumpahead', 'lognormvariate', 'normalvariate',
'paretovariate', 'randint', 'random', 'randrange', 'sample', 'seed',
'setstate', 'shuffle', 'triangular', 'uniform', 'vonmisesvariate',
'weibullvariate']
No caso, o que nós queremos é o shuffle
(como eu sei? Porque eu olhei a
documentação, oras!)
E assim nós temos o código:
import random
consoantes = ['f', 'j', 'c', 'l', 'n']
random.shuffle(consoantes)
print('{}u{}am para as {}o{}i{}as!'.format(*consoantes)
E está feito nosso randomizador de Fugir para as Colinas.
Embora aqui tenhamos alcançado nosso objetivo, existem algumas outras coisinhas que são interessantes de se ver.
In-place
Uma das coisas que random.shuffle
faz é alterar a ordem do conteúdo, não
retornando nada no resultado. Por exemplo
>>> import random
>>> lista = [1, 2, 3, 4]
>>> random.shuffle(lista)
>>> print(lista)
[2, 4, 1, 3]
Isso não é um problema caso a lista não seja mais necessária depois do uso (ou
a ordem original não seja mais necessária). Se fosse necessária, seria preciso
fazer uma cópia da lista antes de usar o shuffle
. Existe um módulo chamado
copy
para fazer cópias tanto de listas quanto de dicionários.
Entretamento, para este caso, existe uma forma mais simples.
Slices
Para acessar um elemento de uma lista, basta usar a posição do element (começando em zero, obviamente).
>>> lista = ['a', 'b', 'c', 'd']
>>> print(lista[1])
'b'
Também é possível acessar um grupo de elementos usando :
, com a posição
inicial e a posição final (que é exclusiva, ou seja, antes de chegar no
elemento indicado).
>>> lista = ['a', 'b', 'c', 'd']
>>> print(lista[1:3])
['b', 'c']
Existe ainda um terceiro parâmetro para slides, que é o "step". Por exemplo,
>>> lista = [1, 2, 3, 4]
>>> print(lista[::2])
[1, 3]
Aqui foi indicado que é pra ir do começo da lista até o final, mas pulando de dois em dois.
Embora não muito usado, a parte que realmente importa é que step também aceita valores negativos, indicando que é pra seguir na ordem inversa. E o uso mais comum é criar uma cópia da lista, mas com os valores invertidos.
>>> lista = [1, 2, 3, 4]
>>> print(lista[::-1])
[4, 3, 2, 1]
Também é possível omitir as posições: Se for omitida a primeira posição, significa "desde o começo"; se for omitida a posição final, significa "até o fim".
>>> lista = ['a', 'b', 'c', 'd']
>>> print(lista[:3])
['a', 'b', 'c']
>>> lista = ['a', 'b', 'c', 'd']
>>> print(lista[1:])
['b', 'c', 'd']
Também é possível usar índices negativos, tanto na posição inicial quanto final, indica que é "a partir do fim da lista".
>>> lista = ['a', 'b', 'c', 'd']
>>> print(lista[-2:])
['c', 'd']
Essas operações de "pegar pedaços de uma lista a partir de uma posição inicial e final" são chamados de slides.
Copiando listas por Slices
Mas porque eu comentei de slices? Porque, se você reparar, quando é utilizada uma faixa, o Python retorna o resultado como uma lista. Na verdade, não é um pedaço da lista original, é uma nova lista.
Considerando que:
- Sem uma posição inicial, significa que é pra começar do começo da lista.
- Sem uma posição final, significa que é ir até o final da lista.
- Slices são cópias de uma lista.
O que você acha que acontece se não forem passadas as duas posições ao mesmo tempo?
Sim, você cria uma cópia da lista.
>>> import random
>>> lista = [1, 2, 3, 4]
>>> copia = lista[:]
>>> random.shuffle(copia)
>>> print(copia)
[2, 4, 1, 3]
>>> print(lista)
[1, 2, 3, 4]
E, com essa cópia, evitamos de termos problemas com a lista passado pelo
shuffle
, porque a lista original vai ter sempre os dados na mesma ordem, sem
nunca ser alterada -- desde que o shuffle
seja feita na cópia.