sábado, 27 de janeiro de 2018

Python 3 OOP Parte 1

Objetos e tipos

Traduzido de http://blog.thedigitalcatonline.com/blog/2014/08/20/python-3-oop-part-1-objects-and-types/ publicado por Leonardo Giordani em 20/08/2014

Sobre esta série

A programação orientada a objetos (OOP) tem sido o principal paradigma de programação há várias décadas, desde as tentativas iniciais nos anos 60 até chegar em algumas das linguagens mais importantes usadas atualmente. Sendo um conjunto de conceitos de programação e metodologias de design, não dá para dizer que exista uma linguagem que a implementou "corretamente" ou "perfeitamente", na realidade existe tantas implementações diferentes, como existem linguagens diferentes.
Portanto, um dos aspectos mais interessantes das linguagens OOP é o entendimento de como elas implementam esses conceitos. Nesta publicação, vou tentar dar início à analise da implementação OOP da linguagem Python. Devido à riqueza do tópico, considero essa tentativa como um conjunto de ideias para os iniciantes de Python que estão tentando encontrar o caminho para essa linguagem bonita (e às vezes peculiar).
Esta série de postagens pretende apresentar o leitor à implementação do Python 3 dos conceitos de programação orientada a objetos. O conteúdo desta e das próximas postagens não serão completamente diferentes da série anterior de "OOP Concepts in Python 2.x", no entanto. O ponto é que, embora algumas das estruturas internas mudaram muito, a filosofia global não se alterou, sendo Python 3 uma evolução do Python 2 e não um novo idioma.
Então escolhi dividir a série anterior e adaptar seu conteúdo ao Python 3 em vez de publicar uma mera lista de correções. Acho que esta maneira deve ser mais útil para novos leitores, que, de outra forma, seriam forçados a ler a série anterior.

Print

Uma das mudanças mais visíveis introduzidas pelo Python 3 é a transformação da palavra-chave print na função print(). Esta é, de fato, uma mudança muito pequena, em comparação com outras modificações feitas nas estruturas internas, mas é a mais impressionante e será a fonte de 80% de seus erros de sintaxe quando você começar a escrever o código Python 3.
Lembre-se de que a saída de tela é agora uma função e escreva print(a) e não print a. Vale observar que Python 2.7 aceita o uso de print(a) então esta parte do código em Python 3, funciona também em Python 2.7.

De volta para o objeto

A ciência da computação lida com dados e com procedimentos para manipular esses dados. Tudo, desde os primeiros programas Fortran até as últimas aplicações móveis, é sobre dados e suas manipulações.
Então, se os dados são os ingredientes e os procedimentos são as receitas, parece (e pode ser) razoável mantê-las separadas.
Vamos fazer alguma programação procedutal em Python
In [1]:
# Alguns dados
data = (13, 63, 5, 378, 58, 40)

# Um procedimento que calcula a média
def avg(d):
    return sum(d)/len(d)
    
avg(data)
Out[1]:
92.83333333333333
Como você pode ver, o código é simples: o procedimento (função) atua numa seqüência de dados e retorna a média dos itens de lista. Até agora, tudo certo: calcular a média dos números deixando-os intocados e criar novos dados.
A observação do mundo cotidiano, no entanto, mostra que dados complexos mudam: um dispositivo elétrico está ligado ou desligado, uma porta está aberta ou fechada, o conteúdo de uma estante de livros em seu quarto muda à medida que você compra novos livros.
Você ainda pode gerenciá-lo mantendo os dados e procedimentos separados, por exemplo:
In [2]:
# Há duas portas numeradas, inicialmente fechadas
porta1 = [1, 'fechada']
porta2 = [2, 'fechada']

# Procedimento para abrir a porta
def abre_porta(porta):
    porta[1] = 'aberta'
In [3]:
abre_porta(porta1)
porta1
Out[3]:
[1, 'aberta']
In [4]:
abre_porta(porta2)
porta2
Out[4]:
[2, 'aberta']
Descrevi uma porta como uma estrutura contendo um número e o status da porta (como faria em linguagens como LISP, por exemplo). O procedimento sabe como essa estrutura é feita e pode alterá-la.
Isso também funciona corretamente. Surge, contudo, alguns problemas, quando começo a criar tipos especializados de dados. O que acontece, por exemplo, se eu introduzo um tipo de dados "porta com fechadura", que só pode ser aberta quando não está trancada? Vamos ver:
In [5]:
# Há duas portas numeradas, inicialmente fechadas
porta1 = [1, 'fechada']
porta2 = [2, 'fechada']

# O procedimento para abrir a porta
def abre_porta(porta):
    porta[1] = 'aberta'

# Esta é uma porta com fechadura, inicialmente destrancada
fporta1 = [1, 'fechada', 'destrancada']

# Procedimento para abrir uma porta padrão
def abre_porta(porta):
    porta[1] = 'aberta'

# Procedimento para abrir uma porta com fechadura
def abre_fporta(porta):
    if porta[2] == 'destrancada':
        porta[1] = 'aberta'
In [6]:
abre_porta(porta1)
porta1
Out[6]:
[1, 'aberta']
In [7]:
abre_fporta(fporta1)
fporta1
Out[7]:
[1, 'aberta', 'destrancada']
Embora tudo ainda funcione sem surpresas neste código. Como você pode ver, tive que encontrar um nome diferente para o procedimento que abre uma porta com fechadura, uma vez que sua implementação difere do procedimento que abre uma porta padrão. Mas, espere... ainda estou abrindo uma porta, a ação é a mesma, e isso muda o status da própria porta. Então, por que tenho que lembro que uma porta fechada deve ser aberta com, em abre_fporta()vez de abre_porta() se o verbo, a ação, é o mesmo?
Há chances de que esta separação entre dados e procedimentos não se encaixem perfeitamente em algumas situações. O problema principal é que a ação "aberta" não está realmente usando a porta; antes está mudando seu estado. Assim como ocorre com os botões de controle de volume do seu telefone, que estão no próprio telefone, o procedimento "aberto" deve se referir à "porta".
Isso é exatamente o que leva ao conceito de objeto: um objeto, no contexto OOP, é uma estrutura que contém dados e procedimentos que operam neles.

Nosso querido tipo?

Quando se fala sobre dados, precisamos imediatamente apresentar o conceito de tipo. Este conceito pode ter dois significados na ciência da computação que merecem ser mencionados: o comportamental e o estrutural.
O significado comportamental representa o fato de que você sabe o que é descrevendo como ele age. Esta é a base do chamado "duck typing", numa tradução literal: "tipagem de pato": se algo age como um pato, é um pato (https://osantana.me/duck-typing/).
O significado estrutural identifica o tipo da coisa, observando sua estrutura interna. Portanto, duas coisas que atuam da mesma maneira, mas são internamente diferentes, são de tipos diferentes.
Ambos os pontos de vista podem ser válidos e diferentes linguagens podem implementar e enfatizar um significado ou outro, e até mesmo ambos.

Jogos de Classes

Os objetos em Python podem ser construídos descrevendo sua estrutura através de uma classe . Uma classe é a representação de programação de um objeto genérico, como "um livro", "um carro", "uma porta": quando falo sobre "uma porta", todos podem entender o que estou dizendo, sem a necessidade de referir para uma porta específica na sala.
Em Python, o tipo de objeto é representado pela classe utilizada para construir o objeto: isto é, em Python o tipo de palavra tem o mesmo significado da classe de palavras .
Por exemplo, uma das classes internas de Python é int, que representa um número inteiro
In [8]:
a = 6
a
Out[8]:
6
In [9]:
print(type(a))
<class 'int'>
In [10]:
print(a.__class__)
<class 'int'>
Como você pode ver, a função incorporada type() retorna o conteúdo do atributo mágico class (a magia aqui significa que seu valor é gerenciado internamente pelo próprio Python). O tipo da variável a, ou sua classe, é int. (Esta é uma descrição muito imprecisa de um tópico bastante complexo, então lembre-se de que, no momento, estamos apenas arranhando a superfície).
Depois de ter uma classe, você pode instanciá-la para obter um objeto concreto (uma instância) desse tipo, ou seja, um objeto construído de acordo com a estrutura dessa classe. A sintaxe Python para instanciar uma classe é a mesma de uma chamada de função.
In [11]:
b = int()
print(type(b))
<class 'int'>
Ao criar uma instância, você pode passar, de acordo com a definição da classe, alguns valores para inicializá-la.
In [12]:
b = int()
b
Out[12]:
0
In [13]:
c = int(7)
c
Out[13]:
7
Neste exemplo, a classe int cria um número inteiro com o valor 0 quando é chamada sem argumentos, caso contrário, usa o argumento dado para inicializar o objeto recém-criado.
Vamos escrever uma classe que represente uma porta, para combinar os exemplos procedurais da primeira seção.
In [14]:
class Porta:
    def __init__(self, numero, status):
        self.numero = numero
        self.status = status
        
    def abrir(self):
        self.status = 'aberta'
        
    def fechar(self):
        self.status = 'fechada'
A palavra chave class define uma nova classe chamada Porta; todo o código indentado abaixo de class faz parte desta classe. As funções que você escreve dentro do objeto são chamadas de métodos e não diferem de uma função padrão; a nomenclatura muda apenas para destacar o fato de que essas funções agora fazem parte de um objeto.
Os métodos de uma classe devem aceitar como primeiro argumento um valor especial chamado self(o nome é uma convenção, mas sempre deve ser usado).
A classe pode receber um método especial chamado __init__() que é executado quando a classe é instanciada, recebendo os argumentos passados quando esta é chamada; é o que normalmente, no contexto OOP, chama-se de construtor. Vale observar que o método __init__() não é a única parte desse mecanismo em Python.
As variáveis self.numero e self.status são chamadas de atributos do objeto. Em Python, os métodos e os atributos são ambos membros do objeto e são acessíveis com a sintaxe ponto (porta.numero); a diferença entre atributos e métodos é que este último pode ser chamado (porta.abrir()).
Como você pode ver, o método __init__() deve criar e inicializar os atributos, uma vez que eles não são declarados em nenhum outro lugar. Isso é muito importante em Python e está estritamente ligado à forma como a linguagem lida com os tipos de variáveis. Eu detalharei esses conceitos ao lidar com o polimorfismo em uma postagem posterior.
A classe pode ser usada para criar um objeto concreto.
In [15]:
porta1 = Porta(1, 'fechada')
print(type(porta1))
<class '__main__.Porta'>
In [16]:
porta1.numero
Out[16]:
1
In [17]:
porta1.status
Out[17]:
'fechada'
Agora porta1 uma instância da classe Porta; type() retorna a classe __main__.Porta já que a classe foi definida diretamente no shell interativo, que está no módulo principal atual.
Para chamar um método de um objeto, isto é, executar uma das suas funções internas, você apenas acessa como um atributo com a sintaxe ponto e o chama como qualquer função.
In [18]:
porta1.abrir()
porta1.numero
Out[18]:
1
In [19]:
porta1.status
Out[19]:
'aberta'
Nesse caso, o método abrir() da instância porta1 foi chamado. Nenhum argumento foi passado para ele, mas se você rever a declaração da classe, pode verificar que foi configurado para receber um argumento (self). Quando você chama um método de uma instância, o Python passa automaticamente a instância para o método como primeiro argumento.
Mais sobre self explícito em Python: http://wiki.python.org.br/SignificadoDoSelf.
Você pode criar quantas instâncias forem necessárias e elas não estarão completamente relacionadas. Ou seja, as alterações que você faz em uma instância não refletem em outra instância da mesma classe.

Recapitulando

Os objetos são descritos por uma classe, que pode gerar uma ou mais instâncias não relacionadas entre si. Uma classe contém métodos, que são funções, e eles aceitam pelo menos um argumento chamado self, que é a instância atual na qual o método foi chamado. Um método especial, init() lida com a inicialização do objeto, definindo o valor inicial dos atributos.

Curiosidades dos títulos

Os títulos desta seção provêm dos seguintes filmes: De volta para o Futuro (1985), Nosso querido Bob? (1991), Jogos de Guera (1983) .

Fontes

Você encontrará uma grande quantidade de documentação nesta postagem Reddit. A maioria das informações contidas nesta série provêm dessas fontes.

Jupyter Notebook

Jupyter Notebook contendo o código utilizado neste post: https://github.com/FranciscoACLima/python3_oop.

Continua...

Francisco AC Lima.

sexta-feira, 22 de dezembro de 2017

Decorators em Python - parte 3


Decorando métodos e passando parâmetros nos decorators

Continuação de Decorators em Python - parte 2 

Decorando Métodos

Em Python, métodos são funções que esperam que seu primeiro parâmetro seja uma referência ao objeto atual. Métodos são funções dentro de classes. Podemos criar decoradores para métodos da mesma maneira que criamos para uma função simples. Vamos usar o p_decorate usado nos nossos exemplos anteriores para decorar uma função que retorna o nome completo de uma pessoa.

def p_decorate(func):
   def func_wrapper(self):
       return "<p>{}</p>".format(func(self))
   return func_wrapper

class Pessoa(object):
    def __init__(self, nome, sobrenome):
        self.nome = nome
        self.sobrenome = sobrenome

    @p_decorate
    def get_nome_completo(self):
        return self.nome + " " + self.sobrenome

pessoa = Pessoa('Francisco', 'Lima')
print(pessoa.get_nome_completo())
<p>Francisco Lima</p>

Passando argumentos nos decoradores

Uma abordagem bem melhor seria tornar nosso decorador útil para funções e métodos. Isso pode ser feito colocando *args e *kwargs como parâmetros para o wrapper, então ele pode aceitar qualquer número arbitrário de argumentos e argumentos de palavras-chave.

def p_decorate(func):
    def func_wrapper(*args, **kwargs):
       return "<p>{0}</p>".format(func(*args, **kwargs))
    return func_wrapper

class Pessoa(object):
    def __init__(self, nome, sobrenome):
        self.nome = nome
        self.sobrenome = sobrenome

    @p_decorate
    def get_nome_completo(self):
        return self.nome + " " + self.sobrenome

pessoa = Pessoa('Francisco', 'Lima')
print(pessoa.get_nome_completo())
<p>Francisco Lima</p>

Vamos ver novamente o exemplo dos 3 decoradores, p, strong e div que usamos na parte 2 deste Post.

def p_decorate(func):
    def func_wrapper(nome):
        return "<p>{}</p>".format(func(nome))
    return func_wrapper

def strong_decorate(func):
    def func_wrapper(name):
        return "<strong>{0}</strong>".format(func(name))
    return func_wrapper

def div_decorate(func):
    def func_wrapper(name):
        return "<div>{0}</div>".format(func(name))
    return func_wrapper

@div_decorate
@p_decorate
@strong_decorate
def get_text(nome):
    return "Olá {0}, seja bem vindo ao nosso site!".format(nome)
print (get_text("John"))
<div><p><strong>Olá John, seja bem vindo ao nosso site!</strong></p></div>

Olhando para ele, podemos notar que os 3 decoradores, div_decorate, p_decorate, strong_decorate, são redundantes, cada um possuí basicamente a mesma funcionalidade, apenas envolvendo a string com diferentes tags.
Dá para fazer muito melhor do que isso. Por que não ter uma implementação mais geral para um decorator que leva a tag para envolver a string? Pois é isso que vamos fazer.

def tags(tag_name):
    def tags_decorator(func):
        def func_wrapper(name):
            return "<{0}>{1}</{0}>".format(tag_name, func(name))
        return func_wrapper
    return tags_decorator

@tags("div")
@tags("p")
@tags("strong")
def get_text(nome):
    return "Olá {0}, seja bem vindo ao nosso site!".format(nome)

print(get_text("Francisco"))
<div><p><strong>Olá Francisco, seja bem vindo ao nosso site!</strong></p></div>

Usando um único decorator, substituimos os 3 anteriores. É claro que tivemos um pouco mais de trabalho neste caso. Os decoradores esperam receber uma função como argumento, e é por isso que tivemos que construir uma função que receba esses argumentos extras para gerar o nosso decorador sobre ela.
No exemplo acima, @tags , é o nosso gerador do decorador.

Jupyter notebook contendo o código deste estudo:
github.com/FranciscoACLima/Python-decorators

Mais sobre o uso de decoradores em Python:
thecodeship.com/patterns/guide-to-python-function-decorators/

Até...
Francisco ACLima

quinta-feira, 14 de dezembro de 2017

Decorators em Python - parte 2

Aplicando mais de um decorador numa função

No exemplo abaixo, temos a função get_text decorada com @p_decorate que envolve o texto numa tag p.

def p_decorate(func):
    def func_wrapper(nome):
        return "<p>{}</p>".format(func(nome))
    return func_wrapper

@p_decorate
def get_text(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print(get_text('Francisco'))
<p>Olá Francisco, seja bem vindo ao nosso site!</p>

Agora, considerem que queiramos decorar nossa função get_text com outras 2 funções para envolver uma div e uma label strong em torno da saída do paragráfo.

def get_text(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

def p_decorate(func):
    def func_wrapper(nome):
        return "<p>{}</p>".format(func(nome))
    return func_wrapper

def strong_decorate(func):
    def func_wrapper(name):
        return "<strong>{0}</strong>".format(func(name))
    return func_wrapper

def div_decorate(func):
    def func_wrapper(name):
        return "<div>{0}</div>".format(func(name))
    return func_wrapper

Com a abordagem básica, decorar get_text() seria com a linha de comando abaixo:

get_text = div_decorate(p_decorate(strong_decorate(get_text)))
print (get_text("John"))
<div><p><strong>Olá John, seja bem vindo ao nosso site!</strong></p></div>

Com a sintaxe do decorador do Python, a mesma coisa pode ser alcançada de uma forma mais clara:

@div_decorate
@p_decorate
@strong_decorate
def get_text(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print (get_text("John"))
<div><p><strong>Olá John, seja bem vindo ao nosso site!</strong></p></div>

Uma coisa importante a observar é que a ordem dos nossos decoradores é importante. O primeiro decorador é o último a ser aplicado.
Se, por exemplo, colocarmos @strong_decorate em primeiro, todo o texto restante é inserido dentro da tag strong.

@strong_decorate
@div_decorate
@p_decorate
def get_text(nome):
    return "lorem ipsum, {0} dolor sit amet".format(nome)

# Basicamente, o que está sendo feito com o decorador é:
# get_text = div_decorate(p_decorate(strong_decorate(get_text)))

print (get_text("John"))
<strong><div><p>Olá John, seja bem vindo ao nosso site!</p></div></strong>


Jupyter notebook contendo o código deste estudo:
github.com/FranciscoACLima/Python-decorators

Mais sobre o uso de decoradores em Python:
thecodeship.com/patterns/guide-to-python-function-decorators/

Continua em:
Decorators em Python - parte 3

Até...
Francisco ACLima

domingo, 3 de dezembro de 2017

Decorators em Python - parte 1

Decorators em Python - Parte 1


Python @decorator
Python é rico em recursos poderosos. Um dos mais interessantes são os decoradores, decorators. No contexto dos padrões de design, decoradores alteram dinamicamente a funcionalidade de uma função, método ou classe sem ter que usar diretamente subclasses. Isso é ideal quando você precisa ampliar a funcionalidade de uma função que não deseja modificar. Podemos implementar o padrão decorator em qualquer lugar, mas Python facilita sua implementação fornecendo uma sintaxe expressiva para isso: @.
Essencialmente, os decoradores funcionam como wrappers, modificando o comportamento do código antes e depois da execução de uma função alvo, sem a necessidade de modificar a própria função, aumentando a funcionalidade original, decorando-a.
Mas antes de mergulhar no assunto de @decorators, alguns pontos devem ser explicados. Em Python, funções são cidadãos de primeira classe, são objetos e isso significa que podemos fazer muitas coisas úteis com elas.

Vejamos, por exemplo, as funções abaixo e como elas se comportam.

Uma função pode ser atribuída a uma variável

def cumprimentar(nome):
    return "Olá " + nome

cumprimentar_alguem = cumprimentar
print(cumprimentar_alguem("Francisco"))
Olá Francisco

Uma função pode ser criada dentro de outra

def cumprimentar(nome):
    def get_mensagem():
        return "Olá "
    result = get_mensagem() + nome
    return result

print(cumprimentar("Francisco"))
Olá Francisco

Uma função pode ser passada como parâmetros para outra função

def cumprimentar(nome):
    return "Olá " + nome

def testar_funcao(func):
    nome = 'Francisco'
    return func(nome)

print(testar_funcao(cumprimentar))
Olá Francisco

Uma função pode retornar outra função

Em outras palavras, funções podem gerar outras funções.

def compor_cumprimento():
    def get_mensagem():
        return "Olá! Tudo bem?"
    return get_mensagem

cumprimentar = compor_cumprimento()
print(cumprimentar())
Olá! Tudo bem?

Uma função interna tem acesso a atributos da função envolvente

Uma função interna pode usar atributos da função externa, a função de escopo superior. Assim, mudamos a função get_mensagem() para receber o parâmetro nome recebido pela função compor_cumprimento(), retornando get_mensagem().

def compor_cumprimento(nome):
    def get_mensagem():
        return "Olá " + nome + "! Tudo bem?"
    return get_mensagem

cumprimentar = compor_cumprimento("Francisco")
print(cumprimentar())
Olá Francisco! Tudo bem?

Quando, como no exemplo acima, estão presentes os itens abaixo:
  • uma função aninhada (função dentro de uma função)
  • a função aninhada usando um valor definido na função de inclusão
  • a função de inclusão retornar a função interna, aninhada
Temos o que chamamos de closure, encerramento, (https://www.programiz.com/python-programming/closure). Um padrão muito encontrado durante a construção de decoradores.
Outro ponto é que Python só permite o acesso de leitura ao escopo externo e não atribuição. A não ser que isso seja declarado de forma explícita usando a palavra chave nonlocal (https://www.programiz.com/python-programming/keyword-list#nonlocal).

Compondo nosso primeiro decorador

Usando as ideias acima podemos criar nosso primeiro decorador. Vamos usar uma função que retorna uma frase de bem_vindo() e ampliá-la colocando esse texto numa tag html p

def bem_vindo(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print(bem_vindo('Francisco'))
Olá Francisco, seja bem vindo ao nosso site!
def p_decorate(func):
    def func_modificada(nome):
        return "<p>{}</p>".format(func(nome))
    return func_modificada

my_bem_vindo = p_decorate(bem_vindo)

print(my_bem_vindo("Francisco"))
<p>Olá Francisco, seja bem vindo ao nosso site!</p>

Esse foi o nosso primeiro decorador. Uma função que recebe outra como argumento, gera uma nova função aumentando o trabalho da função original e retorna a função gerada para que possamos usá-la em qualquer lugar.

Para que bem_vindo() seja decorado por p_decorate() podemos redefini-la passando-a dentro do próprio decorador

def bem_vindo(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)
bem_vindo = p_decorate(bem_vindo)
print(bem_vindo('Francisco'))
<p>Olá Francisco, seja bem vindo ao nosso site!</p>

Outra observação é que, como nossa função decorada recebe o argumento "nome", tudo o que precisamos fazer no decorador é permitir que o invólucro de bem_vindo() receba e passe esse argumento para a função original.

Sintaxe do decorador em Python

Python torna a criação e o uso de decoradores um agradável para o programador através de um açúcar sintático. Para decorar bem_vindo não temos que usar bem_vindo = p_decorator(bem_vindo). Há um atalho para isso, que é mencionar o nome da função decoradora antes da função a ser decorada com um símbolo @. Neste caso: @p_decorate.

def p_decorador(func):
    def func_modificada(nome):
        return "<p>{}</p>".format(func(nome))
    return func_modificada

@p_decorador
def bem_vindo(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print(bem_vindo('José'))
<p>Olá José, seja bem vindo ao nosso site!</p>
Agora sim, nosso "decorator" ficou com uma sintaxe mais interessante.


Jupyter notebook contendo o código deste estudo:
github.com/FranciscoACLima/Python-decorators

Mais sobre o uso de decoradores em Python:
thecodeship.com/patterns/guide-to-python-function-decorators/

Continua em:
Decorators em Python - parte 2

Até...
Francisco ACLima