2010年8月22日星期日

ORM & Serialization in Python

这篇依旧是导读性质的一篇,在看Python自带文档缺少头绪的时候可以参考,不然就是在被误导了哦。
而且其实呢,闲扯才是这篇的真正目的呢,虽然说貌似没什么想说的,唉还是在胡言乱语来着的。

之所以要****,是为了存储或传输。

Qt中使用的方式是通过流来实现的,提供x,x方法,并通过重载<<以及>>运算符来实现接口。

虽然标题什么的是已经确定了话题的方向,不过下面的内容只是在随意地跑跑而已。


* import

直接导入py文件或pyc文件,适合存放开发中的自定义数据。


* pickle

用于Python对象和数据流见的转化,用于数据持久化
最简单的示例是:
pickle.dump(data, open('save.p', 'wb'))
reader = pickle.load(open('save.p', 'rb'))
data是需要保存的类型,通常是个字典
可以往一个file对象dump/load多次
pickle的dumps和loads则是用字符串代替文件
对于自己定义类型的实例,可以提供__getstate__/__dict__以及__setstate__
其保存的数据格式会有不同版本,不过文件是保持向后兼容的
似乎pickle是Py的类似模块中最通用的选择,即处于比较暴露的接口
关联的模块marshal和shelve有不同的用法。

试验代码:
#+BEGIN_SRC python
import pickle
class MyObj1:
     def __init__(self,value):
        self.value=value
class MyObj2:
     def __init__(self,value):
        self.value=value
     def __getstate__(self):
        return self.__dict__.copy()
     def __setstate__(self,dict):
        self.__dict__.update(dict)
m1 = MyObj(6)
m2 = MyObj2(7)
data1 = {'str':"string",0:"zero",1:m1,2:m2}
save = pickle.dumps(data1)
data2 = pickle.loads(save)
print data1[2].value == data2[2].value
#+END_SRC


* json

Python中实现json编解码的模块提供了类似pickle的接口,提供load和dump方法。
具体的编解码操作由JSONEncoder/JSONDecoder提供,可以通过派生以cls参数使用或者添加hook的方式来添加对非内置对象实例的操作方法。

试验代码:
#+BEGIN_SRC python
import json
data1 = {0:1,1:2,2:3}
print json.dumps(data1,sort_keys=True)
def myhook(dct):
     return {"vector":"|".join(map(str,dct["vector"]))}
data2 = json.loads("""
{"vector":[1,2]}
""",object_hook=myhook)
print data2

class MyEncoder(json.JSONEncoder):
     def default(self, obj):
        if isinstance(obj, complex):
         return [obj.real, obj.imag]
        return json.JSONEncoder.default(self, obj)
print MyEncoder().encode(1+4j)
#+END_SRC


* XML

Python中和XML相关的实现且在文档中提供实例的有几个:
HTMLParser通过实现handle方法
xmlrpclib/SimpleXMLRPCServer这个用起来和XML感觉不大
xml.dom.minidom实现了简单的DOM接口
或者用SAX的接口的实现更好的解析性能
关于DOM与SAX的规范的内容并不在Py的范畴内
xml.parsers.expat亦有足够悠久的历史
xml.etree.ElementTree则是Py2.5中新增的
各自用法还算有明显的区别的,算是明显吧


* sqlite3

用sqlite3.connect创建Connection对象,可以文件可以":memory:",提供cursor方法获得Cursor对象,然后就找到我们的execute方法了。
可用“?”占位传入值,执行“select”后可对Cursor对象迭代,或者用Cursor对象的若干fetch方法来获取Row的数据。
事务方面的操作在Connection对象上。
可自定义converter来实现sqlite数据和Python对象的转化。

试验代码:
#+BEGIN_SRC python
import sqlite3
conn = sqlite3.connect(":memory:")
c = conn.cursor()
c.execute("create table name(key,value)")
c.execute('insert into name values (?,?)',("hello","world"))
conn.commit()
c.execute('select * from name order by key')
for row in c:
     print row
c.close()
#+END_SRC


* ConfigParser

用来读写ini格式的文件。
虽然是源于Windows的格式,Linux下还是有程序在用的。


* csv

用于读写CSV格式的文件,通过对文件建立reader和writer对象。


* codecs

用于读写UTF-8编码的文件,提供了open的同名函数


* StringIO

实现内存中的文件对象,接口按照对文件的方式使用,提供getvalue方法获取值。


* ZODB

Zope Object Database,就是Zope对象数据库的意思,不过可以作为独立的模块使用。
和Django开发中支持的MongoDB类似的,都属于NoSql的对象数据库,不过ZODB对Py有Native的感觉。
安装“easy_install ZODB3”,导入“import ZODB”,然后便可使用。
使用的接口类似于shelve模块(都默认仅在__setitem__之类的操作时探测到更新),
也就是说是依然dict对象提供的接口的方式使用一个root对象。
因为这里不去涉及ZOPE,所以也仅写一段just work的代码来试一试。

试验代码:
#+BEGIN_SRC python
from ZODB.FileStorage import FileStorage
from ZODB.DB import DB
from persistent import Persistent
class One(Persistent):
     def __init__(self):
        self.todo=[]
     def add(self,task):
        self.todo.append(task)
        self._p_changed = 1
storage = FileStorage('Data.fs')
db = DB(storage)
connection = db.open()
root = connection.root()
root['main']=["hello","world"]
import transaction
transaction.commit()
print root.items()
connection.close()
db.close()
#+END_SRC


* Django-norel

是一个让Django在NoSql数据库上模拟出Django原本的数据Model的查询的Django分支,在Google App Engine上写应用时在用。



* SQLAlchemy

对象关系映射,也就是通常缩写为ORM(Object-relational mapping)的东西。
使用了ORM可以通过OO的方式方便使用数据库,并可方便的切换所使用的数据库后端。
SQLAlchemy虽然才在2006年发布了他的第一个版本,但是很快成为Python社区使用最广泛的ORM模块。
和Django的ORM模块作用是一样的,只不过Django的Model类一般只在Django应用里使用,而且不及SQLAlchemy复杂(至少不是只倾向暴露给用户一个最高层的接口)。
TurboGears的话,是目前尝试使用SQLAlchemy很好的实例,以及应用的价值所在。
如果利用好缓存的话,ORM的性能表现并没有对应用太大的影响。

安装“easy_install SQLAlchemy”,导入“import sqlalchemy”,
然后“sqlalchemy.__version__ ”便可以查看版本。

先来整理一下概念上的东西,并假定已有了简单的SQL和ORM的基础:
对象Engine,表示一个数据库,用create_engine创建。
对象MetaData表示对数据的操作,提供方法create_all/drop_all在Engine上创建或删除表
对象Table用以表示数据表,构造器可用于构造表,可在表实例上构建查询并以execute方法执行查询
对象Column表示数据表的列,列的类型定义在sqlalchemy.types。
declarative_base的实例Base是以申明的方式来创建对象,相比用mapper关联表和对象要直观,两者等价所以通常就以派生Base的方式使用了。有属性__table__表示关联的Table对象和属性metadata表示拥有的MetaData对象。
类型Session表示对数据库操作的会话,通过sessionmaker(bind=engine)创建,然后用来创建session实例。实例session有方法add/add_all和delete用于添加删除实例,方法query用以创建Query对象,方法commit和方法rollback用于事务的提交与回滚,方法from_statement直接执行SQL语句。
对象Query用于构建查询,在session实例上以所查询对象为参数创建,提供方法filter,order_by以及序列操作进行查询,提供方法all,one,firs以及count获得查询结果,方法join可以实现表的join查询,简单的查询操作便都在此进行。
aliased用于解决多表查询时的命名冲突问题。
外键列ForeignKey用来指向另一个表的主键,用以建立表间关系。数据实例的API通过relationship函数给模型添加属性。

当前手头没有打算写实际的例子,所以依然是试验目的的代码,目的仅是演示一下代码的流程:
#+BEGIN_SRC python
print "create_engine"
from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=True)

print "declare_schema"
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
Base = declarative_base()
class Entry(Base):
     __tablename__ = 'entry'
     id = Column(Integer, primary_key=True)
     name = Column(String(50))
     category_id = Column(Integer, ForeignKey('category.id'))
     category = relationship("Category", backref="entries")
     def __init__(self,name,category):
        self.name = name
        self.category = category
     def __repr__(self):
        return "<Entry %s>"%self.id
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, backref
class Category(Base):
     __tablename__ = 'category'
     id = Column(Integer, primary_key=True)
     name = Column(String)
     #entry_id = Column(Integer, ForeignKey('entry.id'))
     def __init__(self,name):
         self.name = name
     def __repr__(self):
         return "<Category %s>"%self.id
Base.metadata.create_all(engine)

print "use_db"
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
c = Category("hello_world")
session.add(c)
session.add(Category("hello_earth"))
session.commit()
for i in range(1,10):
     item = Entry(str(i),c)
     session.add(item)
session.commit()
for i in session.query(Entry).filter(Entry.id==5).all():
     print i
     print i.category
print session.query(Category).count()

print "END"
#+END_SRC


* Elixir

Elixir构建在SQLAlchemy之上,提供直观易用的声明式接口。
在使用方式上Elixir类似于SQLAlchemy的declarative扩展,而且在API上有所简化以方便使用,也隐藏了一些映射的实现细节。

安装“easy_install Elixir”,导入“from elixir import *”,之后便可使用。
值得注意的是对于Elixir未包装的功能,SQLAlchemy的模块依然是需要且肯定会使用到的,特别是查询方面的Query对象。

然后直接写一段试验用的代码了:
#+BEGIN_SRC python
from elixir import *

metadata.bind = "sqlite:///:memory:"
metadata.bind.echo = True

class Post(Entity):
     using_options(tablename='post')
     title = Field(Unicode(30))
     content = Field(UnicodeText)
     comments = OneToMany('Comment',order_by='-id')
     def __repr__(self):
        return "<Post %s>"%self.title
class Comment(Entity):
     content = Field(UnicodeText)
     post = ManyToOne('Post')
     def __repr__(self):
        return "<Comment %s>"%(self.content[:5])

#from model import *
setup_all()
create_all()

p = Post(title=u"demo",content=u"This is a test post.")
for i in range(1,10):
     p.comments.append(Comment(content=u"#%s comment"%i))
session.commit()

for i in Post.query.all():
     print i,i.comments
p.delete()
print Post.query.count()

cleanup_all(True)
#+END_SRC


* Camelot

一个基于PyQt和Elixir依照Django的Model和Admin接口用于开发类似access桌面MIS的RAD框架。
内含一个类似[[http://code.google.com/p/formlayout/]]的form窗口生成工具。

这个就纯观赏用。


* Python

语言的解析树本身就是一个静态的数据,不过不会去直接使用它:
#+BEGIN_SRC python
import parser
st = parser.expr('a + 5')
print parser.st2tuple(st)
a = 5
code = st.compile()#same as 'parser.compilest(st)'
import pprint
pprint.pprint(eval(code))
#+END_SRC
较通常的做法呢是实现对象的__getattribute__/__getattr__方法,以及hasattr/getattr这样的方法,或者类似__class__/__dict__获得中对象所关心的属性。
不过通常也只是这样来获取数据,而没有把数据写成py文件的习惯(不然就等同于使用了罪恶的eval函数了)。


* 闲扯

因为目的是对想法的提及,所以依然还是想在行文逻辑上有连续的感觉。
(上面这句就有问题来着...)
在Django中,模板中数据的来源有两个方面,一是由视图传入的字典提供哦,一是在模板渲染时调用所编写的标签或过滤器返回的结果。对于单页面的网页在浏览器中缓存的是最终显示的结构,在访问新的数据时,仅在样式和脚本上有所缓存。
如果需要把未渲染的模板提前保存在本地的话,就需要在渲染时执行对服务器的全程调用,这就是当前Ajax技术所实现的事情。服务器返回的不再是完整的页面,而是html/text片段,或xml/json数据,或js可执行代码。
将本地的页面更多的作为显示的容器,还是将远程的调用仅涉及数据相关的API,是把功能实现在客户端还是服务器的两种倾向。
对于PyQt这样Api较封闭且脚本类的东西来说,利用漂亮的QWidget及其派生类树形结构。可以在本地程序中下载全程的代码,然后无缝地作为本地程序来执行。
Firefox的xul技术就是这样一种程序环境,程序以扩展的性质安装到本地,通过Market来分发,通过签名来保障安全。而且界面和Qt一样是本地的,而不仅作为嵌入而渲染。如今Eclipse,Notepad++都有各自的扩展Market,而IM之类的软件也可做这样的平台。
现在觉得Chrome的Market更好用一点,应为扩展更接近单纯的网页。虽然功能有些限制,不过填删与开发似乎觉得更方便一些。
不过FX的扩展有沙箱like的权限机制,相比在PyQt中去执行远端的代码就是一件危险的事情了(QML的事属于下面一行)。
说到浏览器,HTML5可用来构建富客户端了(代码执行环境放在本机,而数据的储存放在服务器上,代码可以根据需要动态的下载),和即使Debian的软件包可以做到高速的分发以及虚拟机一样快速且底副作用的部署的话,依然是可以感受的其中的区别的。
不过,区别只是出于观察的视角,在给予用户的体验上,之间又有相关联的地方。


闲扯居然也能坑这么久...也没质量什么的...


Sep 27 2010

没有评论: