====== 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