|
周末花了一个下午和一个晚上把Scott Rosenberg的Dreaming in Code从头到尾看了一遍。最直接的感受是这本书把我之前很多记忆碎片串在了一起,成为一个生动而"完整"的故事,虽然书的内容本身组织的多少还是有点散。 我本人对Chandler这个项目并不陌生:之前出于对Python/wxWidget和开源本身的兴趣,陆续用过几个0.x的版本,一开始并不是十分友好,性能上也有问题,甚至会莫名的吃掉我机器上的数百兆(或者上G?)空间。后来的版本在性能和可用性上确实提高了不少,但一直感觉这个项目缺少必要的、以及许多开源项目应有的momentum。Phillip J. Eby对Chandler开发人员不懂Python的批评,当时我的印象也很深。而项目中出现的人物,包括Mitchell Kapor、Ted Leung,也都在Chandler这个范畴之外follow过。其他细节包括:Chandler和Cosmo这两个名称的由来、Chandler项目组中女性成员相对高的比例、一些熟悉的人物及其观点(Alan Kay, Bill Joy, Frederick Brooks, Donald Knuth、Linus Torvalds, Ward Cunningham, Larry Wall, Guido van Rossum, Joel Spolsky, etc.)、一些公司的分分和和以及人员流动等等。感觉挺亲切的。 可能更重要、也更深刻的原因是:尽管书中一再提到"There's no such thing as a typical software project, every project is different",我仍然深深的感觉到,Chandler遇到的这些问题,其实也是我亲历的很多项目的种种问题的一个缩影。对这些问题,作者并没有给出解决方案,其实谁也没有标准答案。软件开发是一项非常具有挑战性的工作,也正是像我们这样有热情、有涵养的专业人士生存的空间和价值所在。
以下是一段视频,Ward Cunningham针对Debt Metaphor这个隐喻的由来和人们对它的一些误解进行了澄清: 我最感兴趣的是Burden这一段:Cunningham解释说,经常看到有些开发团队,他们快速的开发出软件产品推向市场,不断的往里面添加新的特性,在这个过程中,不断的学习,但从不把学到的东西、总结的经验教训应用回去,这就像是从银行借贷,但从不想着有一天需要偿还(是不是有点像是在说引发这次次贷危机的美国人的消费习惯和观念?),到最后,你所有的收入都将用于偿还利息,你的购买力也将降为零。映射回软件开发的语境,如果我们在一个较长的时间跨度内,开发一个软件,不断的增加feature,但从不回过头去整理和重构,把对这个软件和这些特性的新的理解和认知写回去,那么最终这个软件、这些feature将不再会有任何实际的价值,对它们做任何事,都将花费越来越多的时间和精力,开发进度也就因此下降为零。
经历了有史以来最忙碌的一周,当然要好好放松一下,除了听上乘的古典音乐,沏上一壶上等的乌龙细细品味,也是一种享受。乌龙茶和紫砂壶可是绝配,如果是安溪的铁观音,加上做工精良的宜兴紫砂壶,那滋味,唇齿留香,别提多惬意了。 好的紫砂壶是需要"养"的,今天专程去茶城败了一只回来,开始"喂"铁观音,哈哈。
先简单介绍一下问题的语境。 手头有个开发了3年的Spring+iBATIS应用(经典三层架构),最近尝试用Hibernate实现原有SQLMap版的部分CRUD操作。除开混合事务和其他一些底层配置细节(如TransactionAwareDataSource、禁用lazy-load等)之外,最大的一个"pattern-mismatch"是:Model层和持久层采用了dirty flag机制,即每次INSERT和UPDATE操作,都会根据每个字段的dirty与否决定是否参加INSERT/UPDATE,而这些dirty flag可以被外部重置,所以业务层的代码,经常可以看到类似这样的pattern: 1- new一个model类并setId() (或者在已有的model上重置dirty flag) 2- set需要update的字段(通常都只是部分字段) 3- 丢给DAO层去update 最终的效果是某张表某个ID的某条记录的某些字段被更新了,而其他字段不受影响,这就是我在标题中提到的所谓"暴力"update,虽不elegant,但却也简单实用,至少很"直接"。 为了让Hibernate版的DAO(默认除Trasient之外全体字段参加INSERT和UPDATE)继续支持这样的"use-pattern",除了按照Hibernate的习惯去配置映射和SessionFactory等之外,我们需要做一些额外的工作: 1- 在BO/Entity类上追加注解 @org.hibernate.annotations.Entity(dynamicInsert=true, dynamicUpdate=true) 2- 实现org.hibernate.Interceptor接口的findDirty()方法,Hibernate提供了一个EmptyInterceptor可以作为起点,方法签名如下: public int[] findDirty( Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types ); 返回的int[]包含所有应该被认为是dirty的字段索引,返回null表示默认处理,传入的entity是持久对象,字段列表会通过propertyNames参数传给你。 3- 注入到Spring的Application Context中,类似这样: <bean id="findDirtyInterceptor" class="gao.sean.hybrid.interceptor.FindDirtyInterceptor"/>
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> <property name="entityInterceptor"><ref bean="findDirtyInterceptor"/></property> </bean> 在这样的配置下,业务层的代码就可以继续"暴力"update了。
如果你使用早前版本的Spring,又恰好采用了Annotation注解方式(而非传统XML方式)配置Hibernate对象关系映射,那么在通过org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean配置sessionFactory时,你一定对annotatedClasses、annotatedPackages有一种说不出的胸闷的感觉,如此以高配置性见长的Spring,怎么在这一个小小的环节上就不能做得再灵活些呢,一定要一个个手写Class路径么? 估计有不少人无奈选择了从AnnotationSessionFactoryBean继承一个自定义的子类,自己实现扫描逻辑,找出@Entity注解过的类清单配置进去。 Spring 2.5.6里有个不怎么起眼的改进,那就是在AnnotationSessionFactoryBean上增加了一个新的方法: setPackagesToScan(String[] packagesToScan)有了这个方法,我们不再需要自己动手去实现实体类的扫描了,直接在Spring配置文件中AnnotationSessionFactoryBean这个section上增加类似如下的一个property即可(假定你需要加载的实体类所在的包名match这个字符串"com.**.bo"): <property name="packagesToScan" value="com.**.bo"/> 你也可以以清单的方式指定多于1条的匹配字串,如: <property name="packagesToScan"> <list> <value>com.abc.core.bo</value> <value>com.abc.auditing.bo</value> </list> </property>
Pylons是一个典型的MVC Web框架,在之前的几篇随笔中,我们一起了解了Pylons的安装、默认项目结构、Routes和controller("C")以及SQLAlchemy("M"),在这个系列的最后,我们一起来看看"V"。 在我们的controller代码中,每个action在return的时候,可以选择: 1- 直接return字符串 2- 通过render()函数将输出交给页面模板引擎处理 3- redirect_to()重定向到其他URL 直接return太简单,redirect_to也没有特别需要介绍的,重点看render()。如果你一直follow这个系列,那么在你的controllers/hello.py中,可以看到这样一行import: from newapp.lib.base import BaseController, render 从lib.base引入了一个render函数,跟到lib/base代码里查看,我们会知道: from pylons.templating import render_mako as render 其实我们用到的render()函数,是pylons.templating.render_mako的别名。 注: 这里假定你在paster create时选择了默认的mako,其他Pylons原生支持的页面模板引擎还有结构相对更层次化的Genshi和更接近Django实现的Jinja。
render_mako()函数签名如下: render_mako(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None) 最基本的用法是给出template文件名,然后通过extra_vars传入参数,Pylons默认查找页面模板文件是在项目的templates子目录,这个路径也可以在config/environment.py中修改。在Pylons中,被推荐的传参做法是使用tmpl_context,在生成controller的时候,已经自动import了tmpl_context并别名为c,这样,我们只需要在c上绑上我们需要传递给模板的数据,模板在解析时,也就能够通过c得到这些数据了。像这样: c.name = u'Pylons n00b' return render('hello.mako') 然后,在hello.mako中: <h3>Hello <b>${c.name}</b>!</h3> 在模板代码中,有些Pylons系统变量/函数是可以直接访问的,包括: tmpl_context (c) - 用于controller和template传递数据 config - 配置信息 app_globals (g) - 应用的全局变量 h - WebHelpers,包括h.form、h.link_to、h.url_for等等实用函数 request - 请求 response - 应答 session - 会话信息 translator、ungettext()、_()、N_() - 处理国际化 除了基本的${}变量替代语法之外,类似JSP,Mako还支持注释、if/else/for控制逻辑、代码片段、return、标签等,具体的可以扫一眼官方说明: http://www.makotemplates.org/docs/syntax.html很精简,也非常容易理解,这里就不详细说明了。 至此,我们已经了解了Pylons最核心的几个组件,足够我们搭建常规的Web应用了。其他值得大家继续挖掘的内容包括:国际化、表单验证(FormEncode)、用户验证和权限(AuthKit、repoze.who、repoze.what)、AJAX、Python 3.0、WSGI基础架构等。 本文是该系列最后一篇,希望通过简单的介绍和学习,大家能够喜欢并顺利的上手Pylons,也希望越来越多的人关注这个优秀的Python Web应用框架。
在前面的4篇随笔中,我们简要的介绍了SQLAlchemy,不过SQLAlchemy如何被集成到Pylons应用中呢? 首先我们看一下自动生成代码中的model子目录,其中有两个文件__init__.py和meta.py,其中meta.py定义了engine、Session和metadata三个公用变量,而__init__.py提供了一个核心的init_model(engine)方法,该方法分别将数据库engine和经过sessionmaker和scoped_session包装的Session对象植入到meta中,像这样: sm = orm.sessionmaker(autoflush=True, autocommit=True, bind=engine)
meta.engine = engine meta.Session = orm.scoped_session(sm) 这样一来,整个背后的"magic"就还剩下最后一块"拼图":谁来把engine初始化好并调用init_model方法呢?看看config/environment.py就清楚了: 1 """Pylons environment configuration""" 2 import os 3 4 from mako.lookup import TemplateLookup 5 from pylons.error import handle_mako_error 6 from pylons import config 7 from sqlalchemy import engine_from_config 8 9 import newapp.lib.app_globals as app_globals 10 import newapp.lib.helpers 11 from newapp.config.routing import make_map 12 from newapp.model import init_model 13 14 def load_environment(global_conf, app_conf): 15 """Configure the Pylons environment via the ``pylons.config`` 16 object 17 """ 18 # Pylons paths 19 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 paths = dict(root=root, 21 controllers=os.path.join(root, 'controllers'), 22 static_files=os.path.join(root, 'public'), 23 templates=[os.path.join(root, 'templates')]) 24 25 # Initialize config with the basic options 26 config.init_app(global_conf, app_conf, package='newapp', paths=paths) 27 28 config['routes.map'] = make_map() 29 config['pylons.app_globals'] = app_globals.Globals() 30 config['pylons.h'] = newapp.lib.helpers 31 32 # Create the Mako TemplateLookup, with the default auto-escaping 33 config['pylons.app_globals'].mako_lookup = TemplateLookup( 34 directories=paths['templates'], 35 error_handler=handle_mako_error, 36 module_directory=os.path.join(app_conf['cache_dir'], 'templates'), 37 input_encoding='utf-8', output_encoding='utf-8', 38 imports=['from webhelpers.html import escape'], 39 default_filters=['escape']) 40 41 # Setup SQLAlchemy database engine 42 engine = engine_from_config(config, 'sqlalchemy.') 43 init_model(engine) 44 45 # CONFIGURATION OPTIONS HERE (note: all config options will override 46 # any Pylons config options)
注意第7行的import和第42、43行代码,是不是豁然开朗?Pylons在初始化运行环境时,从config中读取sqlalchemy相关的配置信息,然后通过这些配置信息创建数据库engine,并调用init_model()方法初始化SQLAlchemy功能的核心对象:metadata和Session。有了meta.Session,我们就可以方便的在代码中执行对model层/数据库的访问了。
接着前面的例子说,我们定义了book_table和author_table,接下来: 1 class Book(object): 2 def __init__(self, title): 3 self.title = title 4 def __repr__(self): 5 return "<Book('%s')>" % self.title 6 7 class Author(object): 8 def __init__(self, name): 9 self.name = name 10 def __repr__(self): 11 return "<Author('%s')>" % self.name 这里我们定义两个类,继承自object,类似JavaBeans或者POJO,这里的__init__方法和__repr__方法不是必须的,只是为了创建对象和输出对象内容比较方便。然后就可以用SQLAlchemy的mapper和sessionmaker来建立映射关系并处理持久和查询等操作: 1 from sqlalchemy.orm import mapper,sessionmaker 2 3 mapper(Book, book_table) 4 mapper(Author, author_table) 5 6 Session = sessionmaker(bind=engine) 7 session = Session() 8 9 gia = Book(u'Groovy in Action') 10 ag = Author(u'Andrew Glover') 11 12 session.add(gia) 13 session.add(ag) 14 session.add_all([Book('Hibernate in Action'), Author('Gavin King')]) 15 s_gia = session.query(Book).filter_by(title=u'Groovy in Action').first() 16 s_gia.title =u'Groovy in Action Updated' 17 18 print "[DIRTY]", session.dirty 19 20 session.commit() # or session.rollback()
如果你用过Hibernate,那么这些代码对你来说,理解起来应该没有任何难度。 假如我告诉你,每次都要像这样先定义Table(schema),再定义class,然后用mapper建立对照,是不是有点那啥?SQLAlchemy的开发者们也意识到这一点,所以从0.5开始,SQLAlchemy可以通过sqlalchemy.ext.declarative支持我们实现更紧凑的model/schema定义: 1 from sqlalchemy.schema import Table, Column, ForeignKey, Sequence 2 from sqlalchemy.types import * 3 from sqlalchemy.orm import relation 4 from sqlalchemy.ext.declarative import declarative_base 5 6 Base = declarative_base() 7 metadata = Base.metadata 8 9 bookauthor_table = Table('bookauthor', metadata, 10 Column('book_id', Integer, ForeignKey('book.id'), nullable=False), 11 Column('author_id', Integer, ForeignKey('author.id'), nullable=False), 12 ) 13 14 class Book(Base): 15 __tablename__ = 'book' 16 id = Column(Integer, Sequence('seq_pk'), primary_key=True) 17 title = Column(Unicode(255), nullable=False) 18 authors = relation('Author', secondary=bookauthor_table) 19 20 21 class Author(Base): 22 __tablename__ = 'author' 23 id = Column(Integer, Sequence('seq_pk'), primary_key=True) 24 name = Column(Unicode(255), nullable=False) 25 books = relation('Book', secondary=bookauthor_table) 这里我们用到了many-to-many关系,其他的常见用法还包括many-to-one、one-to-many、JOIN、子查询、EXISTS、Lazy/Eager Load、Cascade (all/delete/delete-orphan)等等,大家可以根据需要查阅官方文档。
在介绍SQLAlchemy最核心最有价值的ORM部分之前,我们再简单过一遍SQLAlchemy提供的SQL Expression Language用法,就从最基本的CRUD来举例说明吧(接着上一篇的示例): 1 from sqlalchemy import select,update,delete 2 3 conn = engine.connect() 4 book_ins = book_table.insert(values=dict(title=u'Groovy in Action')) 5 author_ins = author_table.insert(values=dict(name=u'Andrew Glover')) 6 conn.execute(book_ins) 7 conn.execute(author_ins) 8 book = conn.execute(select([book_table], book_table.c.title.like(u'Groovy%'))).fetchone() 9 author = conn.execute(select([author_table])).fetchone() 10 bookauthor_ins = bookauthor_table.insert(values=dict(book_id=book[0],author_id=author[0])) 11 conn.execute(bookauthor_ins) 12 conn.execute(update(book_table,book_table.c.title==u'Groovy in Action'), title=u'Groovy in Action (中文版)') 13 conn.execute(delete(bookauthor_table)) 14 conn.close() 简单说明一下代码逻辑: 首先从engine建立连接,然后做两个insert动作,分别insert一条book记录(title为'Groovy in Action')和一条author记录(name为'Andrew Glover'),这之后分别再做两次select,得到刚insert的这两条记录,其中book记录的select用到了过滤条件,相当于"WHERE book.title like 'Groovy%'",然后构建一条新的insert语句,用于insert一条bookauthor关系记录,接下来,做一次update,将book.title为'Groovy in Action'的更新为'Groovy in Action (中文版)',最后,在关闭连接之前,做一次delete,删除bookauthor中的记录。 在指定WHERE条件时,.c是.columns的简写,所以book_table.c.title指代的就是book表的title列。更高级的用法是采用"&"、"|"、"!"三个符号,分别表示AND、OR和NOT,加上必要的"("和")"实现复杂的条件定义。由于传递给select()的第一个参数是个list,所以你应该已经猜到了,我们也可以多张表做关联查询。
在sqlalchemy.schema和sqlalchemy.types这两个module中,包含了定义数据库schema所需要的所有类,如Table、Column、String、Text、Date、Time、Boolean等。还是来看一个例子: 1 from sqlalchemy.engine import create_engine 2 from sqlalchemy.schema import MetaData, Table, Column, ForeignKey, Sequence 3 from sqlalchemy.types import * 4 5 engine = create_engine('postgres://test:test@localhost/test', echo=True) 6 7 metadata = MetaData() 8 metadata.bind = engine 9 10 book_table = Table('book', metadata, 11 Column('id', Integer, Sequence('seq_pk'), primary_key=True), 12 Column('title', Unicode(255), nullable=False), 13 ) 14 15 author_table = Table('author', metadata, 16 Column('id', Integer, Sequence('seq_pk'), primary_key=True), 17 Column('name', Unicode(255), nullable=False), 18 ) 19 20 bookauthor_table = Table('bookauthor', metadata, 21 Column('book_id', Integer, ForeignKey('book.id'), nullable=False), 22 Column('author_id', Integer, ForeignKey('author.id'), nullable=False), 23) 24 25metadata.create_all(checkfirst=True) 首先我们还是create_engine,然后新建一个MetaData对象,把engine绑上去,接下来,开始在metadata中定义表结构(metadata由Table构造函数传入),我们这里定义了3张表,分别是book、author和bookauthor关系表(“多对多”),其中新建一个Sequence对象,专门处理主键生成。最后我们通过执行metadata.create_all()创建数据库表,参数checkfirst=True表示如果数据库相关对象已经存在,则不重复执行创建。 对于已经存在于数据库中的表,我们可以通过传入autoload=True参数到Table构造函数的方式来加载现有的表结构到metadata中,而不必挨个儿再写一遍Column清单。 看到这儿,你也许觉得挺麻烦,不是么?Django和RoR都是可以直接定义数据model类,顺带就把schema也定义了,而不是像这样单独去写表结构的schema,显得很"底层"。确实,这样用SQLAlchemy并不是最优化的,SQLAlchemy本身并不会自动的帮你做很多事,但它基础打得很牢。如果你感兴趣,也可以先去看一下SQLAlchemy的扩展模块Elixir,通过Elixir,你可以像Ruby on Rails那样定义出实体和关系("Active Record")。 在稍后的第4部分中,我们会去了解如何以声明方式来更紧凑的定义我们的model/schema(0.5新特性)。鉴于笔者倾向于更强的控制力,所以在这个系列中就不去介绍SQLAlchemy的其他扩展模块了,如Elixir、SQLSoup等,大家可以根据需要去找下官方文档。
ORM是个大话题,大到可能好几本书都说不完。SQLAlchemy,别看它刚出到0.5.2,已然是Python世界ORM的事实标准,受到众多开发者和无数框架的青睐。 如果之前没有或很少接触SQLAlchemy,那么学习Pylons可能有相当一部分时间都会花在SQLAlchemy上。通常,人们选择Pylons或者TurboGears而不是Django,SQLAlchemy在这些决定的背后有着很重的分量。作为Pylons学习笔记的一部分,接下来我将分4篇随笔介绍SQLAlchemy,分别是Engine API、Schema Management (MetaData/Types)、SQL Expression Language和Object Relational Mapper。此文为第1篇,重点介绍Engine API。 类似Java的JDBC,Python也有一个类似的数据库访问接口规范,那就是DB-API(目前是2.0),不同的常见RDBMS都有符合DB-API标准的Python库,比如PostgreSQL有psycopg2,Oracle有cx_Oracle等。有了这个基础,SQLAlchemy也就得以方便的通过DB-API连接不同的数据库。 以PostgreSQL为例,通过SQLAlchemy的Engine API访问数据库的代码可以这样来写: 1 from sqlalchemy.engine import create_engine 2 3 engine = create_engine('postgres://user:pass@localhost/testdb') 4 connection = engine.connect() 5 connection.execute( 6 """ 7 CREATE TABLE book 8 ( 9 id serial NOT NULL, 10 title character varying(30) NOT NULL, 11 CONSTRAINT pk_book PRIMARY KEY (id) 12 ); 13 """ 14 ) 15 connection.execute( 16 """ 17 INSERT INTO book (title) VALUES (%s); 18 """, 19 "The Art of UNIX Programming" 20 ) 21 rs = connection.execute("SELECT title FROM book") 22 for row in rs: 23 print "Book Title: ", row['title'] 24 connection.close()
基本步骤就是create_engine、connect、execute和close,没有很特别的地方,不过SQLAlchemy的Engine API,并不是简单的DBAPI调用,而是包装了其他内容,如数据库连接池,我们可以在create_engine的时候指定连接池参数和其他额外配置,类似这样: engine = create_engine('postgres://user:pass@localhost/testdb', pool_size=10, convert_unicode=True) 类似Spring的JdbcTemplate,更多的时候,统一使用SQLAlchemy的Engine API而不是DB-API能给我们带来更大的灵活性,通常也更方便、更安全。
在开始之前,说点提外话,随着对Pylons了解的深入,你可能时不时需要看看相关组件/软件包是否有更新出来,方法也不复杂,通过"easy_install -U [组件名]"即可,在学习或者是开发过程中,最好是保持环境相对较新,直到出现相对大的release或者即将进入产品部署阶段。 继续介绍Pylons组件,先看个例子。首先用"paster controller hello"增加一个controller,路径中会增加出以下两个文件: controllers/hello.py tests/functional/test_hello.py 分别对应新增的controller类HelloController和功能测试类TestHelloController,它们分别继承自WSGIController->BaseController和TestCase->TestController。 我们主要看hello.py,默认内容如下: 1 import logging 2 3 from pylons import request, response, session, tmpl_context as c 4 from pylons.controllers.util import abort, redirect_to 5 6 from newapp.lib.base import BaseController, render 7 #from newapp import model 8 9 log = logging.getLogger(__name__) 10 11 class HelloController(BaseController): 12 13 def index(self): 14 # Return a rendered template 15 # return render('/template.mako') 16 # or, Return a response 17 return 'Hello World' 如果你的服务器没有Ctrl-C停掉,那么这个时候你已经可以通过 http://127.0.0.1:5000/hello/index看到该controller的处理结果了(Hello World)。 简单改造一下17行: from pylons import config return '<br/>'.join(config.keys()) 我们就可以在返回页面上显示出所有可以通过pylons.config访问到的参数列表。出了返回文本,也可以通过render()方法交给页面模板引擎生成页面,也可以通过redirect_to()跳转到其他URL。 Pylons是如何找到该请求应该由HelloController的index方法来处理的呢?这背后发生了什么?答案就是Routes。 Routes的作者是Ben Bangert,是Pylons框架三个主要作者/维护者之一,早期的版本主要是仿照Ruby on Rails的routes.rb开发的,有RoR经验的朋友可能一眼就能发现它们之间的相似之处。目前Routes的最新版是1.10.2。 Pylons应用中,routing的配置在config/routing.py,默认生成的内容如下: 1 """Routes configuration 2 3 The more specific and detailed routes should be defined first so they 4 may take precedent over the more generic routes. For more information 5 refer to the routes manual at http://routes.groovie.org/docs/ 6 """ 7 from pylons import config 8 from routes import Mapper 9 10 def make_map(): 11 """Create, configure and return the routes Mapper""" 12 map = Mapper(directory=config['pylons.paths']['controllers'], 13 always_scan=config['debug']) 14 map.minimization = False 15 16 # The ErrorController route (handles 404/500 error pages); it should 17 # likely stay at the top, ensuring it can always be resolved 18 map.connect('/error/{action}', controller='error') 19 map.connect('/error/{action}/{id}', controller='error') 20 21 # CUSTOM ROUTES HERE 22 23 map.connect('/{controller}/{action}') 24 map.connect('/{controller}/{action}/{id}') 25 26 return map 在这个配置中,对我们刚才的实例起到决定性作用的是第23行,我们的输入URL为"http://127.0.0.1:5000/hello/index",其中"/hello/index"通过"/{controller}/{action}"这个表达式match出controller为hello而action为index的解析结果,从而在controllers目录找到hello.py,和其中HelloController的index方法,进行调用。 map.connect()在上面代码中体现出两种用法: map.connect('pattern', key=value) - 指定默认的controller、action、id等 map.connect('pattern') - 直接指定pattern pattern字符串允许通配符,通常在最后一个元素上,比如'/{controller}/{action}/{*url}',将后面的整个URL片段交给前面指定的controller/action处理。除此以外,map.connect()还支持 1- "路径别名",如: map.connect('name', 'pattern', [_static=True]) 如果_static设为"True",表示为"静态命名路径"。 2- 额外的匹配条件,如: map.connect('/{controller}/{action}/{id}', requirements={'year': '\d+',}) map.connect('/{controller}/{action}/{id}', conditions=dict(method=['GET','POST'])) 所有的route优先级为从上到下。Routes除了提供解析进来的URL的逻辑,在我们的controller和template代码中,我们还可以方便的通过WebHelpers的url_for()方法计算相应的URL。 Routes 1.x中的有一些仿routes.rb功能将会在2.0中被去掉,包括Route Minimization、Route Memory、Implicit Defaults等。如果有兴趣的话,可以参考一下官方文档,这里就不一一介绍了。为什么要去掉?当然主要的动机还是减少歧义,避免一些不必要的混淆。至于深层次的原因么,可以参考Tim Peters《The Zen of Python》中的一句经典的Python哲学:Explicit is better than implicit。什么?没有听说过?打开python命令行,输入"import this"后回车,慢慢体会其中的道理吧。:)
大家新年好! 在 前一篇随笔中,大家了解了什么是Pylons,有哪些特点,今天笔者继续给介绍默认生成的项目结构。 通过Paste创建新的Pylons应用很简单,就是一句"paster create -t pylons [应用名]"命令,其中"-t pylons"或者全称"--template=pylons",用以告诉Paste我们新建的项目,将是一个Pylons应用,或者说,从一个预定义好的Pylons默认项目模板生成。如果你愿意,你也可以自己来制作新的模板以符合需要。"paster create --list-templates"可以查看当前可用的模板。 假定我们新建的Pylons项目名称为NewApp,那么执行"paster create -t pylons NewApp"后,我们将得到一个新的NewApp目录,其中包含一个docs目录(文档)、一个newapp目录(代码)、一个NewApp.egg-info目录(元数据)和一些顶级配置文件。 在开发过程中,我们经常会用到的是代码目录(这里是newapp)和位于项目根目录下的development.ini和test.ini文件了。注意这里newapp的大小写,Pylons在生成项目的时候,不论你指定的项目名称大小写是怎样,这里会自动.lower()转小写,只有项目顶级路径和egg信息会保留原始大小写,笔者认为这更加符合Web应用的nature。 代码目录下包括config、controllers、lib、model、public、templates和tests子目录,分别用于存放配置(如环境、中间件、路径查找逻辑)、控制器(处理请求)、全局辅助代码(全局变量、helpers等)、模型(后台数据访问)、静态页面/媒体文件、页面模板和测试代码。 最后说说ini文件:默认生成的development.ini和test.ini顾名思义分别对应开发和测试需要的基本配置,我们可以通过修改相应的参数配置来指定具体设定,如服务器IP和端口、数据库连接、日志等。看到这里你也许会问,那么产品环境呢?答案是默认没有生成,你可以从development.ini修改成需要的产品环境配置,也可以通过paster make-config newapp production.ini生成一个默认的产品环境典型配置。 以development.ini为例,Pylons的ini文件主要包括4个部分的内容: 1- 全局默认参数,如 [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports #email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost 2- 服务器配置,如 [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 3- 应用程序配置,如 [app:main] use = egg:NewApp full_stack = true
cache_dir = %(here)s/data beaker.session.key = newapp beaker.session.secret = somesecret
# If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions
# SQLAlchemy database URL sqlalchemy.url = sqlite:///%(here)s/development.db
# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. #set debug = false
简单说明一下,这里的full_stack设置为true表示打开交互式调试和错误报告等功能,"%(here)s"会被替换成项目所在路径,类似相对路径url中的"."转绝对路径,而beaker.*为配置会话相关的设定,如缓存、cookie基本内容等。对于产品环境来说,比较重要的一个配置是"set debug=false",否则一旦出现异常,交互式调试将使得攻击者能够执行系统命令。 4- 日志,如 [loggers] keys = root, routes, newapp, sqlalchemy
[handlers] keys = console
[formatters] keys = generic
[logger_root] level = INFO handlers = console
[logger_routes] level = INFO handlers = qualname = routes.middleware # "level = DEBUG" logs the route matched and routing variables.
[logger_newapp] level = DEBUG handlers = qualname = newapp
[logger_sqlalchemy] level = INFO handlers = qualname = sqlalchemy.engine # "level = INFO" logs SQL queries. # "level = DEBUG" logs SQL queries and results. # "level = WARN" logs neither. (Recommended for production systems.)
[handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic
[formatter_generic] format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S OK,这一篇就先讲到这儿,下一篇将介绍Routes和controller。
Pylons是一个Python语言的Web应用程序框架,如果你简单了解过Ruby on Rails和Django,你大概会问,Pylons有什么不一样呢?Pylons最大的特点是模块化,将处理Web应用环境下不同领域、不同问题的软件包集成在一起,形成一个整体,在提供一揽子解决方案的同时,不阻碍你选择别的替代组件。另外,Pylons是目前对WSGI标准支持最好的框架之一,未来的TurboGears 2.0也会基于Pylons构建。 Pylons从Ruby on Rails借鉴了不少东西,比如Routes,比如WebHelpers,从表面看更像是Python版的RoR,不过底下的架构应该说更加轻量和灵活,因为你可以灵活选择自己熟悉或者更贴和具体应用实际的组件,从ORM到页面模板,Pylons只是推荐一些大家普遍比较认可的选项,但并不强制你使用它们。 说完和Ruby on Rails的异同,当然也要回过头来说说同样是Python编写的Django。如果你只是想迅速的构建一个可以支撑大量访问的Web应用,Django是个不错的选择,但和RoR一样,你在很大程度上被限制在一定的pattern中:如果你按照Django的思路去实现你的应用,你会很happy;但一旦你觉得某个组件你不喜欢、不符合某个实际要求,想要来点定制,你就会觉得有些伸不开拳脚,或者工程浩大。目前感觉Django比较不爽的地方有:页面模板较弱,表现力有些不足,也有人说够用了;ORM目前是自己的一套,暂时没有成熟的SQLAlchemy支持,需要第三方包或者自己做;从架构上,Django对MVC的解读是MTV(Model-Template-View),大家都叫作controller的东东,在Django的世界里是view,以至于每次和别人解释,都要多费一番口舌。 Pylons目前版本是0.9.7(rc4),主要用到的第三方/独立组件有Paste、Routes、Beaker、Mako、FormEncode、WebHelpers和SQLAlchemy。安装方法如下: 首先你必须有Python(2.3+),然后你可以选择直接easy_install Pylons或者新建一个Virtual Environment,和系统中的Python环境隔离开,依赖的包可以独立升级。这里我们按照后一种方式,如果你是第一次使用Pylons,建议你也在独立Python virtualenv中安装。 1- easy_install virtualenv (这将安装Python虚拟环境工具) 2- python virtualenv.py ENV (创建新的虚拟环境。这里的ENV是你新建虚拟环境的路径,如"mydevenv") 3- source ENV/bin/activate (激活虚拟环境。如果是Windows的话,这里需要执行ENV\bin\activate.bat) 4- easy_install Pylons (这里使用的是虚拟环境的easy_install安装) 如果你觉得上面的步骤麻烦,Pylons开发团队提供了一个脚本来处理安装过程,下载后用Python执行即可: http://www.pylonshq.com/download/0.9.7/go-pylons.py如果需要SQLAlchemy,则再执行一下 easy_install SQLAlchemy 安装成功后,通过 paster create -t pylons [应用名] 即可新建Web应用主框架,然后cd到应用下,通过 paster serve --reload development.ini 启动Web服务,默认地址在 http://127.0.0.1:5000/更详细的信息,可参考Pylons项目主页: http://pylonshq.com/随着使用的深入,笔者还会陆续对Pylons和其他相关组件进行进一步的介绍。祝各位农历新年快乐!
在Unix环境下,命令行或者shell中sleep和kill是常见的动作,在Windows的.bat文件中处理类似的任务就不那么直接了,备忘如下:
[sleep] ping 127.0.0.1 -n 需要的秒数+1 -w 1000 > nul
[kill] taskkill /f /im "进程名(如notepad.exe)" taskkill /f /fi "WINDOWTITLE eq notepad*"
其中/f表示强制,/im表示image镜像名(可执行文件名),/fi表示filter,后面跟表达式,比如这里的"窗体标题等于notepad*",支持wildcast通配符。
|