====== Python - xtra ======
//Pequeños consejos, utilidades del lenguaje...//
===== Patrones de diseño =====
==== Método factoría ====
class ProjectType(object):
build_cmd = ""
markers = []
@classmethod
def make_project(cls, path):
prj_types = (PythonProject, AutoconfProject, MakefileOnly, ProjectType)
for p in prj_types:
markers = p.markers
if any(path.isfile(path.join(path, x)) for x in markers):
return p()
class PythonProject(ProjectType):
build_cmd = "python setup.py develop --user"
markers = ['setup.py']
class AutoconfProject(ProjectType):
#TODO: there should be also a way to configure it
build_cmd = "./configure && make -j3"
markers = ['configure.in', 'configure.ac', 'makefile.am']
class MakefileOnly(ProjectType):
build_cmd = "make"
markers = ['Makefile']
Este código elige para los ficheros en una ruta a qué proyecto pertenece a partir de los ''markers'' (ficheros concretos para cada tipo de proyecto). Devolverá un objeto del tipo de sub-clase correspondiente para tratar dicho proyecto. \\
Realmente no es necesario que ''make_project'' sea un ''@classmethod'' debido a que no utiliza el atributo ''cks''.
===== Pequeñas librerías =====
==== PyYAML ====
* [[http://pyyaml.org/wiki/PyYAML]]
* {{script:python:pyyaml-3.10.zip|PyYAML}}
Para parsear documentos [[tags:yaml|YAML]].
=== Uso ===
Agregaremos la librería.
import yaml
Cargar un texto yaml:
print yaml.load("""
name: Vorlin Laruknuzum
sex: Male
class: Priest
title: Acolyte
hp: [32, 71]
sp: [1, 13]
gold: 423
inventory:
- a Holy Book of Prayers (Words of Wisdom)
- an Azure Potion of Cure Light Wounds
- a Silver Wand of Wonder
""")
Crear un texto yaml:
print yaml.dump({'name': "The Cloak 'Colluin'", 'depth': 5, 'rarity': 45, 'weight': 10, 'cost': 50000, 'flags': ['INT', 'WIS', 'SPEED', 'STEALTH']})
=== Notas ===
* Para instalar, en el directorio:
$ sudo python setup.py install
==== PyWin ====
* [[http://sourceforge.net/projects/pywin32/|Web del proyecto]]
Esta librería es únicamente para Windows.
=== API Win32 ===
Una vez instalado el paquete PyWin podremos acceder a la API de Windows mediante el módulo ''win32gui''. Las constantes para este las encontraremos en ''win32con''.
>>> import win32gui as gui
>>> import win32con as con
>>> bsp = gui.FindWindow("BSPlayer", None)
>>> bsp
525126
>>> bsp_cmd = con.WM_USER + 2
>>> gui.SendMessage(bsp, bsp_cmd, 0x10000, 0)
=== COM ===
Mediante Python podemos acceder a objetos COM, esto nos permite desarrollar para programas (ya sean tareas automatizadas, plugins, scripts...). Para ello al instalar Python en Windows se nos agrega un paquete denominado PythonCOM que nos permite enlazar fácilmente estas dos tecnologías, viene también con una herramienta denominada ''Makepy'' que genera código Python que adapta y enlaza la interface COM elegida a nuestro código. \\ \\
Hay dos formas de ejecutar Makepy:
* Lanzando el script ''make.py'' dentro del directorio ''win32com''.
* Desde ''PythonWin'' -> //Tools// -> ''COM Makepy utility''. Esta agregaráa autocompletado al código que escribiesemos.
Para enlazar código COM... :?:
import win32com.client
Visum = win32com.client.Dispatch ("visum.visum.11")
==== BeautifulSoup ====
* [[http://www.crummy.com/software/BeautifulSoup/|Web del proyecto]]
* [[http://www.crummy.com/software/BeautifulSoup/documentation.html|Documentación]]
* {{script:python:new:beautifulsoup-3.2.0.tar.gz|Paquete}}
Es un parser muy sencillo de HTML. \\
Uso básico:
from BeautifulSoup import BeautifulSoup # Para procesar HTML
from BeautifulSoup import BeautifulStoneSoup # Para procesar XML
soup.contents[0].name # u'html'
soup.contents[0].contents[0].name # u'head'
head = soup.contents[0].contents[0]
head.parent.name # u'html'
head.next # Page title
head.next.string # Page title
head.nextSibling.name # u'body'
titleTag = soup.html.head.title
Coger una web y procesarla (búsqueda de elementos):
import urllib2
from BeautifulSoup import BeautifulSoup
page = urllib2.urlopen("http://www.icc-ccs.org/prc/piracyreport.php")
soup = BeautifulSoup(page)
for incident in soup('td', width="90%"):
...
Visualizar html en un formato aceptable:
print soup.prettify()
==== pydaemon ====
* [[http://wookr.com/pydaemon/doc/|Documentación]]
Se utiliza como wrap a un código python que hará de daemon. Para ello se ha de heredar de la clase ''Daemon'' y sobreescribir el método ''run'':
import logging
import time
from pydaemon import Daemon
class FooDaemon(Daemon):
def run(self):
while True:
logging.debug("I'm here...")
time.sleep(1)
def bar(self):
logging.debug("bar")
if __name__ == "__main__":
logging.basicConfig(filename="foodaemon.log",level=logging.DEBUG)
daemon = FooDaemon("/tmp/foodaemon.pid", "FooDaemon")
daemon.main(extended_args={"bar": daemon.bar})
==== PyMongo ====
* [[http://api.mongodb.org/python/2.7rc0/]]
It's a library to access Mongo data bases. \\
Connection:
from pymongo import MongoClient
client = MongoClient()
client = MongoClient('localhost', 27017)
client = MongoClient('mongodb://localhost:27017/')
Getting a db:
db = client.test_database
db = client['test-database']
Getting a collection:
collection = db.test_collection
collection = db['test-collection']
Posting an object:
import datetime
post = {
"author": "Mike",
"text": "My first blog post!",
"tags": ["mongodb", "python", "pymongo"],
"date": datetime.datetime.utcnow()
}
posts = db.posts
post_id = posts.insert(post)
Querying:
posts.find()
posts.find_one({"author": "Mike"})
posts.find_one({"_id": post_id})
for post in posts.find({"date": {"$lt": d}}).sort("author"): ...
Other actions:
posts.count()
posts.find({"author": "Mike"}).count()
==== Message Pack ====
import msgpack
import array
import json
data = raw_input('Insert data > ')
data = json.loads(data)
packed_data = msgpack.packb(data)
bin_data = array.array('B', packed_data).tolist()
unpacked_data = msgpack.unpackb(array.array('B', bin_data).tostring())
print 'data:', data
print 'binary data:', bin_data
print 'unpacked_data:', unpacked_data
==== Otras ====
* [[http://code.google.com/p/psutil/|psutil]], para controlar el rendimiento de la máquina.
* [[https://pypi.python.org/pypi/PyHamcrest|PyHamcrest]], para hacer comprobaciones entre instancias en los tests.
* [[https://pypi.python.org/pypi/termcolor|termcolor]], para mostrar colores por consola.
* [[https://dataset.readthedocs.org/en/latest/|dataset]], ORM para emular una NoSQL con SQLite.
* [[https://pypi.python.org/pypi/tabulate|tabulate]], para mostrar datos en tablas por consola.
=== No utilizadas ===
* [[https://github.com/kennethreitz/envoy|envoy]], para ejecutar programas más fácilmente.
* [[https://github.com/ansible/ansible|Ansible]], configuration-management, application deployment, task-execution, and multinode orchestration engine.
* [[https://github.com/jakubroztocil/httpie|httpie]], a cURL, easy-to-use, alternative.
* [[https://github.com/kennethreitz/requests|requests]], a library to manage HTTP requests.
* [[https://github.com/saltstack/salt|Salt]], is an infrastructure management. Easy enough to get running in minutes, scalable enough to manage tens of thousands of servers, and fast enough to communicate with them in seconds.
* [[https://github.com/getpelican/pelican|pelican]], static site generator.
===== Utilidades =====
==== easy_install ====
Es un gestor de paquetes para Python. Permite instalar librerías de una forma muy sencilla. Para instalarlo simplemente haremos:
wget http://peak.telecommunity.com/dist/ez_setup.py
sudo python ez_setup.py
Puede que exista el paquete instalable (nombre ''python-setuptools'') para el sistema operativo utilizado. \\
A partir de entonces podremos instalar los paquetes con simplemente poner el nombre (esto hará que lo busque en el repositorio [[http://pypi.python.org/pypi|PyPi]], lo baje y lo instale]]:
easy_install SQLObject
O a partir de una ruta, de un .tgz, de un .egg...
easy_install http://example.com/path/to/MyPackage-1.2.3.tgz
easy_install .
==== pip y virtualenv ====
Para instalar virtualenv:
$ sudo apt-get install python-virtualenv
$ sudo apt-get install python-pip
=== pip ===
Al igual que easy_install, ''pip'' es un comando para instalar paquetes desde el repositorio Pypy. Para instalarlo haremos:
$ sudo easy_install pip
Para instalar un paquete haremos:
$ sudo pip install django
Si queremos actualizar un paquete haremos:
$ sudo pip install --upgrade django
Aún así los paquetes no deberían ser instalados globalmente (en el directorio //site-packages// donde todo script python puede acceder) debido a que pueden coexistir problemas entre versiones y paquetes.
=== virtualenv ===
... ''virtualenv'' permite instalar los paquetes no-globalmente. Crea un entorno aislado para un programa donde incluye una copia del binario de pyton, una copia del directorio //site-packages//, una copia del instalador ''pip'' con el cual instalará los paquetes únicamente en dicho entorno. \\
Realmente sólo serían necesarios estos dos paquetes de forma global (pip y easyinstall). \\ \\
Para instalarlo:
$ sudo pip install virtualenv
Para crear un entorno (denominado ''env''):
$ virtualenv env
Cuando lo hagamos se crearán tres directorios: //bin//, //include// y //lib//. Recuerda ignorarlos en el control de versiones. El directorio //bin// es donde se copiarán los binarios python, para usar su comando pip (ya no es necesario ''sudo''):
$ env/bin/pip install requests
Para no tener que escribir toda la ruta podemos llamar al comando ''source'' que la cambiará temporalmente:
$ which python
/usr/bin/python
$ source env/bin/activate
$ which python
/Users/jamie/code/myproject/env/bin/python
Para dejar de usar el entorno llamaremos al comando ''deactivate''.
=== Fichero requirements ===
En el fichero ''requirements'' se guardan los paquetes instalados con pip.
env/bin/pip install -r requirements.txt
En el fichero requirements podremos añadir repositorios svn, git, mercurial, Bazaar (svn+, git+, hg+ o bzr+)... Para ello los agregaremos con ''-e'' (puedes incluir ''@'' (por ejemplo @275) para indicar la revisión deseada):
-e svn+http://myrepo/svn/MyApp#egg=MyApp
Para crear un fichero requirements desde un entorno:
$ pip freeze > stable-req.txt
Copiar un fichero requirements:
$ pip freeze -r devel-req.txt > stable-req.txt
=== virtualenvwrapper ===
* [[http://virtualenvwrapper.readthedocs.org/en/latest/]]
Es una herramienta para hacer más cómodo el uso de ''virtualenv''. Para instalarla haz:
$ pip install virtualenvwrapper
Y añade al .bashrc:
export WORKON_HOME=$HOME/.virtualenvs
export PROJECT_HOME=$HOME/Devel
source /usr/local/bin/virtualenvwrapper.sh
* ''$ workon'' lista los virtualenv creados.
* ''$ mkvirtualenv '' crea un virtualenv.
* ''$ workon '' usa dicho virtualenv.
==== Crear un instalador ====
* [[http://docs.python.org/2/distutils/setupscript.html|Documentación]]
Imaginemos un proyecto con la siguiente estructura:
TowelStuff/
bin/
CHANGES.txt
docs/
LICENSE.txt
MANIFEST.in
README.txt
setup.py
towelstuff/
__init__.py
location.py
utils.py
test/
__init__.py
test_location.py
test_utils.py
=== Carpetas y ficheros sin mayor importancia ===
En la carpeta **bin** se pueden añadir scripts que puedan ser útiles para usar el código de ''towelstuff''. Si no existiesen puede ser eliminada. \\
La carpeta **docs** debería contener documentos de diseño, implementación, FAQ o cualquier otro documento escrito. \\
El fichero **LICENSE.txt** será, generalmente, un copy\paste de la licencia escogida. \\
El fichero **README.txt** contendrá la explicación del código, el formato debería de ser [[http://guide.python-distribute.org/glossary.html#term-restructuredtext|reST]]. \\
La carpeta **test** debe contener los tests escritos usando el paquete ''unittest''.
=== Fichero CHANGES.txt ===
En una primera versión dicho fichero puede contener algo del estilo:
v, -- Initial release.
=== Fichero MANIFEST.in ===
Debería contener algo del estilo:
include *.txt
recursive-include docs *.txt
Es un fichero que, mediante comandos, uno por línea, indica acciones a realizar con ficheros externos. Por ejemplo:
include *.txt
recursive-include hindoor/modules/interface *.*
prune hindoor/modules/interface/test
Dice que del directorio raíz se incluirán todos los .txt. Del directorio ''hindoor/modules/interface'' todos los ficheros recursivamente excepto los del directorio ''test''.
=== Fichero setup.py ===
Un fichero con el siguiente formato:
#from distutils.core import setup
from setuptools import setup
setup(
name='TowelStuff',
version='0.1.0',
author='J. Random Hacker',
author_email='jrh@example.com',
packages=['towelstuff', 'towelstuff.test'],
scripts=['bin/stowe-towels.py','bin/wash-towels.py'],
url='http://pypi.python.org/pypi/TowelStuff/',
license='LICENSE.txt',
description='Useful towel-related stuff.',
long_description=open('README.txt').read(),
install_requires=[
"Django >= 1.1.1",
"caldav == 0.1.4",
],
)
La versión puede ser perfectamente ''0.1.0''.
=== Comandos ===
Podemos ver los comandos disponibles con:
$ python setup.py --help-commands
Crear el paquete a distribuir:
$ python setup.py sdist
Crear un paquete binario:
$ python setup.py bdist
Instalar desde el setup.py:
$ python setup.py install
=== Entry points ===
* [[http://stackoverflow.com/a/782984/69550]]
Acciones que se realicen según la distribución creada.
=== Actualizar el paquete ===
Para actualizar el paquete deberemos:
- Aumentar el número de versión en el fichero ''setup.py''.
- Actualizar ''CHANGES.txt''.
- Ejecutar ''python setup.py sdist'' otra vez.
=== Notas ===
Para recoger los ''install_requires'' de un ''requirements.txt'' podemos hacer una copia del fichero en la misma carpeta, agregar el import de este en el ''MANIFEST.in'' y:
frequirem = open ('./requirements.txt')
requirem = frequirem.readlines()
frequirem.close()
...
install_requires=requirem,
En setup para...
setup (
...
include_package_data = True, # añadir al directorio de instalacion los datos no .py
scripts=['bin/serverstarter','bin/start_interface'], # añadir scripts python como ejecutables
data_files=[('', ['hindoor/modules/interface/index.html'])], # añadir ficheros no indicados
...
)
Ejemplos:
* [[https://github.com/django/django/blob/master/setup.py|Django]]
* [[http://code.google.com/p/pyglet/source/browse/setup.py|pyglet]]
===== Tips & Tricks =====
==== Python y la codificación ====
Puedes cambiar la codificación del script agregando al principio:
# -*- coding: utf-8 *-*
Podemos saber la codificación por defecto haciendo:
sys.getdefaultencoding()
Podemos indicar la codificación en un programa haciendo (PERO NO ES ACONSEJABLE):
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
Podemos cambiar la configuración por defecto de python en el script ''site.py'', en la funcion ''setencoding'' (en mi caso en la ruta ''/usr/lib/python2.7''):
def setencoding():
encoding = "utf-8"
...
Podemos saber la codificación del sistema haciendo:
sys.getfilesystemencoding()
==== Modelos de concurrencia en Python ====
* **Processes**, separan los datos por distintos procesos los cuales, cada uno, tienen sus variables y datos independientemente. Cada uno tiene su propio intérprete de Python y son la única forma de sacar provecho a una CPU multicore.
* **Threads**, implementados como Posix Threads (los gestiona el sistema). Su defecto es que si hay que gestionar muchos el sistema necesita mucho tiempo para intercambiar la ejecución de estos y una vez se pasa de 100 threads se crea un cuello de botella.
* **Microthreads (tasklets)**, se encuentran en el intérprete ''Stackless'' (no es compatible con el oficial, lo cual es su principal problema). Es el GIL quien los gestiona
* **Greenlets**, son una copia de los microthreads pero compatible con el intérprete de Python oficial. El cambio de thread lo realiza el desarrollador (cooperative concurrency model) y no se ejecutan a la vez. El cambio entre greenlets se realiza de forma rápida y permite evitar los locks.
==== Propiedades y descriptores ====
* [[http://nbviewer.ipython.org/urls/gist.github.com/ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb]]
Podemos definir //getters// y //setters// en las propiedades de una clase python con los decoradores ''@property'' y ''@.setter''. Aún así existe un problema, y es que si tenemos varios el código puede hacerse demasiado largo:
class Movie(object):
def __init__(self, title, rating, runtime, budget, gross):
self._rating = None
self._runtime = None
self._budget = None
self._gross = None
self.title = title
self.rating = rating
self.runtime = runtime
self.gross = gross
self.budget = budget
#nice
@property
def budget(self):
return self._budget
@budget.setter
def budget(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._budget = value
#ok
@property
def rating(self):
return self._rating
@rating.setter
def rating(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._rating = value
#uhh...
@property
def runtime(self):
return self._runtime
@runtime.setter
def runtime(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._runtime = value
#is this forever?
@property
def gross(self):
return self._gross
@gross.setter
def gross(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._gross = value
def profit(self):
return self.gross - self.budget
=== Descriptores ===
Los descriptores nos permiten definir una lógica común para el ''get'', el ''set'' y el ''delete'' de las variables:
from weakref import WeakKeyDictionary
class NonNegative(object):
"""A descriptor that forbids negative values"""
def __init__(self, default):
self.default = default
self.data = WeakKeyDictionary()
def __get__(self, instance, owner):
# we get here when someone calls x.d, and d is a NonNegative instance
# instance = x
# owner = type(x)
return self.data.get(instance, self.default)
def __set__(self, instance, value):
# we get here when someone calls x.d = val, and d is a NonNegative instance
# instance = x
# value = val
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self.data[instance] = value
class Movie(object):
#always put descriptors at the class-level
rating = NonNegative(0)
runtime = NonNegative(0)
budget = NonNegative(0)
gross = NonNegative(0)
def __init__(self, title, rating, runtime, budget, gross):
self.title = title
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross
def profit(self):
return self.gross - self.budget
m = Movie('Casablanca', 97, 102, 964000, 1300000)
print m.budget # calls Movie.budget.__get__(m, Movie)
m.rating = 100 # calls Movie.budget.__set__(m, 100)
try:
m.rating = -1 # calls Movie.budget.__set__(m, -100)
except ValueError:
print "Woops, negative value"
El ''WeakKeyDictionary'' lo que hace es que cuando el objeto ''key'' se borra, se borra también del diccionario.
Esto funciona debido a que cuando se hace una asignación tipo ''m.rating = 100'' python reconoce el ''set'' y llama a ''Movie.rating.__set__(m, 100)''.
Para crear métodos haremos:
class CallbackProperty(object):
"""A property that will alert observers when upon updates"""
def __init__(self, default=None):
self.data = WeakKeyDictionary()
self.default = default
self.callbacks = WeakKeyDictionary()
def __get__(self, instance, owner):
if instance is None:
return self
return self.data.get(instance, self.default)
def __set__(self, instance, value):
for callback in self.callbacks.get(instance, []):
# alert callback function of new value
callback(value)
self.data[instance] = value
def add_callback(self, instance, callback):
"""Add a new function to call everytime the descriptor within instance updates"""
if instance not in self.callbacks:
self.callbacks[instance] = []
self.callbacks[instance].append(callback)
class BankAccount(object):
balance = CallbackProperty(0)
def low_balance_warning(value):
if value < 100:
print "You are now poor"
ba = BankAccount()
BankAccount.balance.add_callback(ba, low_balance_warning)
ba.balance = 5000
print "Balance is %s" % ba.balance
ba.balance = 99
print "Balance is %s" % ba.balance
==== Easier to Ask for Forgiveness than Permission ====
Existen dos estilos de control de excepciones:
* **LBYL**, denominado //Look Before You Leap//. Se comprueba si algo puede ir mal antes de hacerlo.
* **EAFP**, denominado //Easier to Ask for Forgiveness than Permission//. Se realiza la acción y si sale mal se intenta recuperar el estado.
El código con estilo LBYL sería algo parecido al siguiente:
def print_object(some_object):
# Check if the object is printable...
if isinstance(some_object, str):
print(some_object)
elif isinstance(some_object, dict):
print(some_object)
elif isinstance(some_object, list):
print(some_object)
# 97 elifs later...
else:
print("unprintable object")
... Y con estilo EAFP:
def print_object(some_object):
# Check if the object is printable...
try:
printable = str(some_object)
print(printable)
except TypeError:
print("unprintable object")
Pero este código puede ser mejorable. Podríamos controlar en qué parte del código puede haber un error y poner otro código que no controlamos en un ''else'':
def print_object(some_object):
# Check if the object is printable...
try:
printable = str(some_object)
except TypeError:
print("unprintable object")
else:
print(printable)
Otro uso del ''else'' podría ser, por ejemplo, para limpiar el entorno:
def display_username(user_id):
try:
db_connection = get_db_connection()
except DatabaseEatenByGrueError:
print('Sorry! Database was eaten by a grue.')
else:
print(db_connection.get_username(user_id))
db_connection.cleanup()
''raise'' nos sirve para lanzar una excepción si le añadimos la creación un objeto "Exception". Aún así también nos puede servir para, por ejemplo, mantener unas estadísticas si no queremos controlar la excepción. Si la incluimos en un ''exept'' propagará la excepción al siguiente nivel de código.
def calculate_value(self, foo, bar, baz):
try:
result = self._do_calculation(foo, bar, baz)
except:
self.user_screwups += 1
raise
return result
==== Instalaciones ====
**MySQL**
$ apt-get install libmysqlclient-dev
$ pip install MySQL-python
**Gevent**
$ apt-get install libevent-dev
$ apt-get install python-dev
$ pip install gevent
**ZeroMQ**
$ sudo apt-get install python-dev libzmq-dev
$ pip install pyzmq
** PyQt4 **
$ sudo apt-get install python-qt4