Published in 00:37 of 04/19/2014 by Bruno Rocha
Bruno Rocha There is more to life than increasing its speed!

Published in 00:37 of 04/19/2014

←Home

Usando o Flask Cache

Como framework o Flask não tem nenhuma funcionalidade de cache embutida, porém existe a cache API do werkzeug e uma excelente extensão para prover essas funcionalidades de cache para suas aplicacoes Flask, esta extensão foi criado pelo @thadeusb e é bastante fácil de implementar e utilizar.

instalando

Em sua env instale pelo PyPI (recommended)

pip install Flask-Cache  

Você também pode instalar diretamente através do github se quiser pegar a última versao de desenvolvimento.

pip install https://github.com/thadeusb/flask-cache/tarball/master

Configurando

Existe uma série de chaves de configuração que podem ser setadas, mas a mais importante é a que define o backend de cache CACHE_TYPE.

A chave CACHE_TYPE pode ser uma string resolvendo um caminho de importação de uma classe que implemente a api de cache do werkzeug, mas existem alguns alias que mapeam as implemetações default que estão no werkzeug.contrib.cache

Por padrão o CACHE_TYPE é NUll o que significa que o cache não terá efeito, então você precisa escolher um backend para o cache.

chave | Cache type

  • null | Sem Cache - NullCache
  • simple | Usará pickle em memória e é recomendado apenas em servidor single process durante o desenvolvimento
  • memcached | Requer pylibmc ou memcached e precisa das configs do memcached no settings
  • redis | Requer redis, werkzeug 0.7 e configurações do Redis no settings
  • filesystem | O mesmo que o simple, porém armazena o pickle em filesystem ao inves da memoria
  • gaememcached | Para o Google AppEngine
  • saslmemcached | Mesmo que o memcached mas para instalações em SASL - requer pylibmc

Todas as opções e outras chaves de config em http://pythonhosted.org/Flask-Cache/#configuring-flask-cache

App Flask simples

em um arquivo nomeado app.py

import time
from flask import Flask

app = Flask(__name__)

@app.route("/")
def view():
    return time.ctime()

if __name__ == "__main__":
    app.run(port=5000, debug=True, host='0.0.0.0')

Execute com python app.py e abra a url http://localhost:5000 no seu browser, aperte F5 (refresh) para ver a data e hora correntes.

Habilitando o cache para a view

agora vamos habilitar o cache para esta pequena app e evitar que a hora seja atualizada a cada request. (neste exemplo usamos a data atual mas imagine que isso poderia ser o carregamento de um grande conjunto de dados ou cálculos complexos)

em um arquivo nomeado cached_app.py

import time
from flask import Flask

# importe a extensao
from flask.ext.cache import Cache   

app = Flask(__name__)

# defina a configuração do cache (isso pode ser feito em um arquivo de settings)
app.config['CACHE_TYPE'] = 'simple'

# instancie o cache e atribua a sua aplicação
app.cache = Cache(app)   

@app.route("/")
@app.cache.cached(timeout=300)  # cache this view for 5 minutes
def cached_view():
    return time.ctime()

if __name__ == "__main__":
    app.run(port=5000, debug=True, host='0.0.0.0')

Execute com python cached_app.py e abra http://localhost:5000 em seu browser e aperte F5 (refresh) para ver a data e hora cacheadas durante 5 minutos.

O decorator @cache.cached utiliza o request.path para a view como chave de cache, se por algum motivo você precisar especificar uma chave diferente poderá passar o argumento key_prefix para o decorator. Neste caso, se o seu argumento incluir um placeholder "%s" ele será substituido pelo request.path.

O exemplo acima é a mais simples e regular app Flask com o uso de cache, mas se sua app é projetada usando app factories, blueprints, class based views ou views separadas em arquivos diferentes você precisará usar abordagens avançadas.

Cacheando funções

O mesmo decorator cached pode ser usado para cachear funções (que não são views), neste caso será preciso especificar um key_prefix, caso contrário o request.path será usado e então poderá cair em conflito quando existir mais de uma função cacheada.

Neste exemplo usaremos o módulo this para extrair citações do Zen do Python.

Em um arquivo nomeado cached_function_app.py

import time
import random

from this import s, d
from string import translate, maketrans

from flask.ext.cache import Cache
from flask import Flask

app = Flask(__name__)
app.config['CACHE_TYPE'] = 'simple'
app.cache = Cache(app)

@app.cache.cached(timeout=10, key_prefix="current_time")
def get_current_time():
    return time.ctime()

def random_zen_quote():
    """Pega uma citação aleatória do Zen do Python""" 
    transtable = maketrans("".join(d.keys()), "".join(d.values()))
    return random.choice(translate(s, transtable).split("\n")[2:])

@app.route("/")
def zen():
    return """
    <ul>
        <li><strong>Com cache:</strong> {cached}</li>
        <li><strong>Sem cache:</strong> {not_cached}</li>
    </ul>
    """.format(
        cached=get_current_time(),
        not_cached=random_zen_quote()
    )

if __name__ == "__main__":
    app.run(debug=True, port=5000, host='0.0.0.0')

Execute com python cached_function_app.py acesse http://localhost:5000 apertando F5 (refresh) você verá que a data atual é cacheada mas a citação continua sendo atualizada, você pode alternar o cache para ver como fica.

def get_current_time():
    return time.ctime()

@app.cache.cached(timeout=10, key_prefix="zen_quote")
def random_zen_quote():
    transtable = maketrans("".join(d.keys()), "".join(d.values()))
    return random.choice(translate(s, transtable).split("\n")[2:])

@app.route("/")
def zen():
    return """
    <ul>
        <li><strong>Sem cache:</strong> {cached}</li>
        <li><strong>Com cache:</strong> {not_cached}</li>
    </ul>
    """.format(
        cached=get_current_time(),
        not_cached=random_zen_quote()
    )

NOTE: por ter usado o módulo this você verá algumas citações serem impressas em seu terminal mas isso não tem nenhum problema.

Cacheando views modulares

agora um exemplo com views separadas em um arquivo diferente para melhor organização

Em um pasta chamada app coloque 3 arquivos __init__.py, app.py e views.py

app/__init__.py um arquivo vazio

app/views.py

import time
import random
from this import s, d
from string import translate, maketrans

def get_current_time():
    return time.ctime()

def random_zen_quote():
    transtable = maketrans("".join(d.keys()), "".join(d.values()))
    return random.choice(translate(s, transtable).split("\n")[2:])

def zen_view():
    return """
    <h1>No cache por 10 segundos!</h1>
    <ul>
        <li>{time}</li>
        <li>{quote}</li>
    </ul>
    """.format(
        time=get_current_time(),
        quote=random_zen_quote()
    )

Como pode ver no exemplo acims definimos a view em um arquivo separado. Para evitar import circular não seria recomendado usar @app.route nem o @app.cache portanto esta view será agnostica com relação a app pois iremos registrar seu mapeamento de urls e seu cache no arquivo principal da app.

Este tipo de estrutura é necessária quando se tem muitas views e é preciso organizar em vários arquivos separados.

NOTE: Para uma melhor organização é recomendado o uso de blueprints que será explicado em seguida.

app/app.py

agora na app principal importaremos as views e explicitamente decoraremos com o cache e também registraremos a regra de url.

from flask import Flask
from flask.ext.cache import Cache
from views import zen_view

app = Flask(__name__)
app.config['CACHE_TYPE'] = 'simple'
app.cache = Cache(app)

# aplique o cache usando o velho estilo de uso dos decorators
cached_zen_view = app.cache.cached(timeout=10)(zen_view)

# registre o mapeamento de url
app.add_url_rule("/", view_func=cached_zen_view)

if __name__ == "__main__":
    app.run(debug=True, port=5000, host='0.0.0.0')

NOTE: Tambem é possivel colocar o cache em um arquivo separado como veremos em seguida.

Cacheando views em Blueprints

Como foi mencionado anteriormente, a melhor maneira de organizar uma app Flask é com o uso de Blueprints, que é uma maneira de criar ' meta-apps' que serão conectadas ao app principal no momento da inicialização, O problema aqui é que o Blueprint é feito pensando em criar módulos reutilizaveis por diferentes apps, portanto o controle do cache precisa ser delegado dinamicamente.

Para evitar import circular criaremos uma instancia de cache em um arquivo separado (considere usar application factory se estiver criando algo mais complexo)

Crie uma pasta chamada blueprint_app com os seguintes arquivos

cached_blueprint_app/
├── app.py
├── cache.py
├── blueprints
│   ├── __init__.py
│   └── zen_blueprint.py
└── __init__.py

No cache.py

from flask.ext.cache import Cache    
cache = Cache()

Criamos uma instancia burra do Cache, ela ainda não foi inicializada mas já pode ser usada, no futuro ela será inicializada junto com a app e estará disponivel quando a view for chamada. Para isso reimportaremos a mesma instancia de cache e chamaremos o metodo init_app.

Um blueprint básico blueprints/zen_blueprint.py

import time
import random
from this import s, d
from string import translate, maketrans
from flask import Blueprint
from cache import cache

zen = Blueprint('zen', __name__)

def get_current_time():
    return time.ctime()

def random_zen_quote():
    transtable = maketrans("".join(d.keys()), "".join(d.values()))
    return random.choice(translate(s, transtable).split("\n")[2:])

@zen.route("/")
@cache.cached(timeout=20)
def zen_view():
    return """
    <h1>Cacheado por 20 segundos!</h1>
    <ul>
        <li>{time}</li>
        <li>{quote}</li>
    </ul>
    """.format(
        time=get_current_time(),
        quote=random_zen_quote()
    )

NOTE: Em uma aplicação real é recomendado separar mais o blueprint em arquivos diferents para views, urls e helpers, para isso você poderá promover este blueprint para um pacote Python.

A app principal app.py

from flask import Flask

from blueprints.zen_blueprint import zen
from cache import cache

app = Flask(__name__)
app.config['CACHE_TYPE'] = 'simple'
cache.init_app(app)

app.register_blueprint(zen)

if __name__ == "__main__":
    app.run(debug=True, port=5000, host='0.0.0.0')

Repare que criamos a instancia burra do cache no arquivo cache.py e usamos esta instancia para decorar as views do blueprint, então o cache foi inicializado no app.py utilizando o método init_app. Isso e possivel por conta do ciclo de inicialização do Flask e também pelo fato da extensão Flask-Cache ter sido criada pensando nesses casos. Se um dia pretender escrever sua própria extensão para o Flask aconselho dar uma olhada no código fonte da Flask-Cache.

Execute com python cached_blueprint_app/app.py acesse http://localhost:5000 e a view será cacheada por 20 segundos

Cacheando MethodViews

Vamos usar o mesmo exemplo cached_blueprint_app transformado a view zen_view em uma MethodView

Altere o zen_blueprint.py para:

import time
import random
from this import s, d
from string import translate, maketrans
from flask import Blueprint
from flask.views import MethodView
from cache import cache

zen = Blueprint('zen', __name__)

class ZenView(MethodView):

    @cache.cached(30)
    def get(self):
        return """
        <h1>Cacheado por 30 segundos!</h1>
        <ul>
            <li>{time}</li>
            <li>{quote}</li>
        </ul>
        """.format(
            time=self.get_current_time(),
            quote=self.random_zen_quote()
        )

    @staticmethod
    def get_current_time():
        return time.ctime()

    @staticmethod
    def random_zen_quote():
        transtable = maketrans("".join(d.keys()), "".join(d.values()))
        return random.choice(translate(s, transtable).split("\n")[2:])


zen.add_url_rule("/", view_func=ZenView.as_view('zen'))

MethodViews mapeiam os métodos HTTP como GET, POST e DELETE para métodos da classe como get, post, delete etc, então neste caso criamos um método chamado get e decoramos com o @cache.cached.

NOTE: Você geralmente não pode usar decorators em métodos individuais de MethodViews, porém o Flask-Cache foi implementado de forma que permite este uso.

alternativamente você pode querer cachear todos os métodos de uma mesma view, para isso basta cachear o dispatch_request ou melhor ainda cachear a view completa usando um decorator explicito.

Cacheando o dispatcher
class ZenView(MethodView):
    @cache.cached(timeout=30)
    def dispatch_request(self):
        return super(ZenView, self).dispatch_request()

    ...
Cacheando a view com decorator explicito (recomendado)
zen = Blueprint('zen', __name__)

class ZenView(MethodView):
    ...

cached_zen_view = cache.cached(timeout=50)(ZenView.as_view('zen'))
zen.add_url_rule("/", view_func=cached_zen_view)

Cacheando blocos de template

O Flask-Cache vem com uma template tag que permite cachear blocos de template, vamos mudar a ZenView e faze-la renderizar um template com Jinja2

No zen_blueprint.py

import time
import random
from this import s, d
from string import translate, maketrans
from flask import Blueprint, render_template
from flask.views import MethodView

zen = Blueprint('zen', __name__)

class ZenView(MethodView):

    def get(self):
        return render_template(
            'zen.html',
            get_random_quote=self.random_zen_quote
        )

    @staticmethod
    def get_current_time():
        return time.ctime()

    @staticmethod
    def random_zen_quote():
        transtable = maketrans("".join(d.keys()), "".join(d.values()))
        return random.choice(translate(s, transtable).split("\n")[2:])

zen.add_url_rule("/", view_func=ZenView.as_view('zen'))

Agora teremos que criar o template cached_blueprint_app/templates/zen.html

<h3> Zen of Python Aleatório </h3>
<strong>{{get_random_quote()}}</strong>

Execute com python cached_blueprint_app/app.py e abra http://localhost:5000 você verá a citação ser atualizada a cada request, vamos evitar isso cacheando durante 30 segundos.

Change the zen.html template

{% cache 30 %}
<h3> Zen of Python Aleatório </h3>
<strong>{{get_random_quote()}}</strong>
{% endcache %}

Salve o template e de o refresh varias vezes e você verá que a citação estará cacheada por 30 segundos.

Cacheando funções com argumentos variaveis usando o decorator memoize

as vezes as views ou funções recebem argumentos através do mapeamento de urls ou diretamente passados, você pode querer cachear utilizando esses argumentos como chave do cache para ter caches diferentes para cada chamada. No Flask-Cache tem o decorator memoize para isso.

NOTE: Para funções que não recebem argumentos cached e memoize terão o mesmo efeito

Com uma app simples memoize_app.py

import time
from flask.ext.cache import Cache
from flask import Flask

app = Flask(__name__)
app.config['CACHE_TYPE'] = 'simple'
app.cache = Cache(app)

@app.cache.memoize(timeout=5)
def get_current_time_and_name(name):
    return "%s - %s" % (name, time.ctime())

@app.route("/<name>")
def view(name):
    return get_current_time_and_name(name)

if __name__ == "__main__":
    app.run(debug=True, port=5000, host='0.0.0.0')

Rode com python memoize_app.py abra http://localhost:5000/seunome e repare que a chamada é cacheada separadamente para cada argumento <name> passado.

Cacheando objetos diretamente

As vezes não podemos usar decorators e precisamos cachear objetos diretamente.

Dentro de uma view ou blueprint é possivel usar o current_app

from flask import current_app

def some_function():
    cached = current_app.cache.get('a_key')
    if cached:
        return cached
    result = do_some_stuff()
    current_app.cache.set('a_key', result, timeout=300)
    return result

Ou se estiver usando uma instancia separada de cache (recomendado)

from cache import cache

def function():
    cached = cache.get('a_key')
    if cached:
        return cached
    result = do_some_stuff()
    cache.set('a_key', result, timeout=300)
    return result

Limpando o cache

Você pode criar um script para limpar o cache ou uma função para ser chamada quando preciso.

from flask.ext.cache import Cache    
from yourapp import app
cache = Cache()

def main():
    cache.init_app(app)

    with app.app_context():
        cache.clear()

if __name__ == '__main__':
    main()

AVISO: Em alguns backends a limpeza completa do cache não é suportada. Se não estiver usando key_prefix em alguns backends como redis irá fazer com que todo o database seja apagado.

Existe uma série de exemplos e uma API bem documentada no site do Flask-Cache http://pythonhosted.org/Flask-Cache/ também é possivel criar um novo backend de cache seguindo este exemplo.

  • Publish your Python packages easily using flit in python · 20:19 of 08/22/2017
  • The quality of the python ecosystem in Slides · 21:23 of 06/27/2017
  • Consumindo e Publicando web APIs - PyData São Paulo - 2017 in Slides · 00:58 of 03/29/2017

  • comments powered by Disqus Go Top