全局变量
Python 推荐支持 DB API 2.0 的数据库模块都应该提供如下 3 个全局变量:
apilevel:该全局变量显示数据库模块的 API 版本号。对于支持 DB API 2.0 版本的数据库模块来说,该变量值通常就是 2.0。如果这个变量不存在,则可能该数据库模块暂时不支持 DB API 2.0。读者应该考虑选择使用支持该数据库的其他数据库模块。
threadsafety:该全局变量指定数据库模块的线程安全等级,该等级值为 0~3 ,其中 3 代表该模块完全是线程安全的;1 表示该模块具有部分线程安全性,线程可以共享该模块,但不能共享连接;0 则表示线程完全不能共享该模块。
paramstyle:该全局变量指定当 SQL 语句需要参数时,可以使用哪种风格的参数。该变量可能返回如下变量值:
format:表示在 SQL 语句中使用 Python 标准的格式化字符串代表参数。例如,在程序中需要参数的地方使用 %s,接下来程序即可为这些参数指定参数值。
pyformat:表示在 SQL 语句中使用扩展的格式代码代表参数。比如使用 %(name),这样即可使用包含 key 为 name 的字典为该参数指定参数值。
qmark:表示在 SQL 语句中使用问号(?)代表参数。在 SQL 语句中有几个参数,全部用问号代替。
numeric:表示在 SQL 语句中使用数字占位符(:N)代表参数。例如:1 代表一个参数,:2 也表示一个参数,这些数字相当于参数名,因此它们不一定需要连续。
named:表示在 SQL 语句中使用命名占位符(:name)代表参数。例如 :name 代表一个参数,:age 也表示一个参数。
通过查阅这些全局变量,即可大致了解该数据库 API 模块的对外的编程风格,至于该模块内部的实现细节,完全由该模块实现者负责提供,通常不需要开发者关心。
数据库 API 的核心类
遵守 DB API 2.0 协议的数据库模块通常会提供一个 connect() 函数,该函数用于连接数据库,并返回数据库连接对象。
数据库连接对象通常会具有如下方法和属性:
cursor(factory=Cursor):打开游标。
commit():提交事务。
rollback():回滚事务。
close():关闭数据库连接。
isolation_level:返回或设置数据库连接中事务的隔离级别。
in_transaction:判断当前是否处于事务中。
上面第一个方法可以返回一个游标对象,游标对象是 Python DB API 的核心对象,该对象主要用于执行各种 SQL 语句,包括 DDL、DML、select 查询语句等。使用游标执行不同的 SQL 语句返回不同的数据。
游标对象通常会具有如下方法和属性:
execute(sql[, parameters]):执行 SQL 语句。parameters 参数用于为 SQL 语句中的参数指定值。
executemany(sql, seq_of_parameters):重复执行 SQL 语句。可以通过 seq_of_parameters 序列为 SQL 语句中的参数指定值,该序列有多少个元素,SQL 语句被执行多少次。
executescript(sql_script):这不是 DB API 2.0 的标准方法。该方法可以直接执行包含多条 SQL 语句的 SQL 脚本。
fetchone():获取查询结果集的下一行。如果没有下一行,则返回 None。
fetchmany(size=cursor.arraysize):返回查询结果集的下 N 行组成的列表。如果没有更多的数据行,则返回空列表。
fetchall():返回查询结果集的全部行组成的列表。
close():关闭游标。
rowcount:该只读属性返回受 SQL 语句影响的行数。对于 executemany() 方法,该方法所修改的记录条数也可通过该属性获取。
lastrowid:该只读属性可获取最后修改行的 rowid。
arraysize:用于设置或获取 fetchmany() 默认获取的记录条数,该属性默认为 1。有些数据库模块没有该属性。
description:该只读属性可获取最后一次查询返回的所有列的信息。
connection:该只读属性返回创建游标的数据库连接对象。有些数据库模块没有该属性。
总结来看,Python 的 DB API 2.0 由一个 connect() 开始,一共涉及数据库连接和游标两个核心 API。它们的分工如下:
数据库连接:用于获取游标、控制事务。
游标:执行各种 SQL 语句。
掌握了上面这些 API 之后,接下来可以大致归纳出 Python DB API 2.0 的编程步骤。
*** 作数据库的基本流程
使用 Python DB API 2.0 *** 作数据库的基本流程如下:
调用 connect() 方法打开数据库连接,该方法返回数据库连接对象。
通过数据库连接对象打开游标。
使用游标执行 SQL 语句(包括 DDL、DML、select 查询语句等)。如果执行的是查询语句,则处理查询数据。
关闭游标。
关闭数据库连接。
下图显示了使用 Python DB API 2.0 *** 作数据库的基本流程。
对大多数软件开发者而言,术语数据库通常是指RDBMS(关系数据库管理系统), 这些系统使用表格(类似于电子表格的网格),其中行表示记录,列表示记录的字段。表格及其中存放的数据是使用SQL (结构化査询语言)编写的语句来创建并 *** 纵的。Python提供了用于 *** 纵SQL数据库的API(应用程序接口),通常与作为标准的SQLite 3数据库一起发布。
另一种数据库是DBM (数据库管理器),其中存放任意数量的键-值项。Python 的标准库提供了几种DBM的接口,包括某些特定于UNIX平台的。DBM的工作方式 与Python中的字典类似,区别在于DBM通常存放于磁盘上而不是内存中,并且其键与值总是bytes对象,并可能受到长度限制。本章第一节中讲解的shelve模块提供了方便的DBM接口,允许我们使用字符串作为键,使用任意(picklable)对象作为值。
如果可用的 DBM 与 SQLite 数据库不够充分,Python Package Index, pypi.python.org/pypi中提供了大量数据库相关的包,包括bsddb DBM ("Berkeley DB"),对象-关系映射器,比如SQLAlchemy (www.sqlalchemy.org),以及流行的客户端/服务器数据的接口,比如 DB2、Informix、Ingres、MySQL、ODBC 以及 PostgreSQL。
本章中,我们将实现某程序的两个版本,该程序用于维护一个DVD列表,并追踪每个DVD的标题、发行年份、时间长度以及发行者。该程序的第一版使用DBM (通过shelve模块)存放其数据,第二版则使用SQLite数据库。两个程序都可以加载与保存简单的XML格式,这使得从某个程序导出DVD数据并将其导入到其他程序成为可能。与DBM版相比,基于SQL的程序提供了更多一些的功能,并且其数据设计也稍干净一些。
12.1 DBM数据库
shelve模块为DBM提供了一个wrapper,借助于此,我们在与DBM交互时,可以将其看做一个字典,这里是假定我们只使用字符串键与picklable值,实际处理时, shelve模块会将键与值转换为bytes对象(或者反过来)。
由于shelve模块使用的是底层的DBM,因此,如果其他计算机上没有同样的DBM,那么在某台计算机上保存的DBM文件在其他机器上无法读取是可能的。为解决这一问题,常见的解决方案是对那些必须在机器之间可传输的文件提供XML导入与导出功能,这也是我们在本节的DVD程序dvds-dbm.py中所做的。
对键,我们使用DVD的标题;对值,则使用元组,其中存放发行者、发行年份以及时间。借助于shelve模块,我们不需要进行任何数据转换,并可以把DBM对象当做一个字典进行处理。
程序在结构上类似于我们前面看到的那种菜单驱动型的程序,因此,这里主要展示的是与DBM程序设计相关的那部分。下面给出的是程序main()函数中的一部分, 忽略了其中菜单处理的部分代码。
db = None
try:
db = shelve.open(filename, protocol=pickle.HIGHEST_PROTOCOL)
finally:
if db is not None:
db.dose()
这里我们已打开(如果不存在就创建)指定的DBM文件,以便于对其进行读写 *** 作。每一项的值使用指定的pickle协议保存为一个pickle,现有的项可以被读取, 即便是使用更底层的协议保存的,因为Python可以计算出用于读取pickle的正确协议。最后,DBM被关闭——其作用是清除DBM的内部缓存,并确保磁盘文件可以反映出已作的任何改变,此外,文件也需要关闭。
该程序提供了用于添加、编辑、列出、移除、导入、导出DVD数据的相应选项。除添加外,我们将忽略大部分用户接口代码,同样是因为已经在其他上下文中进行了展示。
def add_dvd(db):
title = Console.get_string("Title", "title")
if not title:
return
director = Console.get_string("Director", "director")
if not director:
return
year = Console.get_integer("Year", "year",minimum=1896,
maximum=datetime,date.today().year)
duration = Console.get_integer("Duration (minutes)", "minutes“, minimum=0, maximum=60*48)
db[title] = (director, year, duration)
db.sync()
像程序菜单调用的所有函数一样,这一函数也以DBM对象(db)作为其唯一参数。该函数的大部分工作都是获取DVD的详细资料,在倒数第二行,我们将键-值项存储在DBM文件中,DVD的标题作为键,发行者、年份以及时间(由shelve模块pickled在一起)作为值。
为与Python通常的一致性同步,DBM提供了与字典一样的API,因此,除了 shelve.open() 函数(前面已展示)与shelve.Shelf.sync()方法(该方法用于清除shelve的内部缓存,并对磁盘上文件的数据与所做的改变进行同步——这里就是添加一个新项),我们不需要学习任何新语法。
def edit_dvd(db):
old_title = find_dvd(db, "edit")
if old_title is None:
return
title = Console.get.string("Title", "title", old_title)
if not title:
return
director, year, duration = db[old_title]
...
db[title]= (director, year, duration)
if title != old_title:
del db[old_title]
db.sync()
为对某个DVD进行编辑,用户必须首先选择要 *** 作的DVD,也就是获取DVD 的标题,因为标题用作键,值则用于存放其他相关数据。由于必要的功能在其他场合 (比如移除DVD)也需要使用,因此我们将其实现在一个单独的find_dvd()函数中,稍后将査看该函数。如果找到了该DVD,我们就获取用户所做的改变,并使用现有值作为默认值,以便提高交互的速度。(对于这一函数,我们忽略了大部分用户接口代码, 因为其与添加DVD时几乎是相同的。)最后,我们保存数据,就像添加时所做的一样。如果标题未作改变,就重写相关联的值;如果标题已改变,就创建一个新的键-值对, 并且需要删除原始项。
def find_dvd(db, message):
message = "(Start of) title to " + message
while True:
matches =[]
start = Console.get_string(message, "title")
if not start:
return None
for title in db:
if title.lower().startswith(start.lower()):
matches.append(title)
if len(matches) == 0:
print("There are no dvds starting with", start)
continue
elif len(matches) == 1:
return matches[0]
elif len(matches) >DISPLAY_LIMIT:
print("Too many dvds start with {0}try entering more of the title".format(start)
continue
else:
matches = sorted(matches, key=str.lower)
for i, match in enumerate(matches):
print("{0}: {1}".format(i+1, match))
which = Console.get_integer("Number (or 0 to cancel)",
"number", minimum=1, maximum=len(matches))
return matches[which - 1] if which != 0 else None
为尽可能快而容易地发现某个DVD,我们需要用户只输入其标题的一个或头几个字符。在具备了标题的起始字符后,我们在DBM中迭代并创建一个匹配列表。如果只有一个匹配项,就返回该项;如果有几个匹配项(但少于DISPLAY_LIMIT, 一个在程序中其他地方设置的整数),就以大小写不敏感的顺序展示所有这些匹配项,并为每一项设置一个编号,以便用户可以只输入编号就可以选择某个标题。(Console.get_integer()函数可以接受0,即便最小值大于0,以便0可以用作一个删除值。通过使用参数allow_zero=False, 可以禁止这种行为。我们不能使用Enter键,也就是说,没有什么意味着取消,因为什么也不输入意味着接受默认值。)
def list_dvds(db):
start =”"
if len(db)>DISPLAY.LIMIT:
start = Console.get_string(“List those starting with [Enter=all]”, "start”)
print()
for title in sorted(db, key=str.lower):
if not start or title.Iower().startswith(start.lower()):
director, year, duration = db[title]
print("{title} ({year}) {duration} minute{0}, by "
"{director}".format(Util.s(duration),**locals()))
列出所有DVD (或者那些标题以某个子字符串引导)就是对DBM的所有项进行迭代。
Util.s()函数就是简单的s = lambda x: "" if x == 1 else "s",因此,如果时间长度不是1分钟,就返回"s"。
def remove_dvd(db):
title = find_dvd(db, "remove")
if title is None:
return
ans = Console.get_bool("Remove {0}?".format(title), "no")
if ans:
del db[title]
db.sync()
要移除一个DVD,首先需要找到用户要移除的DVD,并请求确认,获取后从DBM中删除该项即可。
到这里,我们展示了如何使用shelve模块打开(或创建)一个DBM文件,以及如何向其中添加项、编辑项、对其项进行迭代以及移除某个项。
遗憾的是,在我们的数据设计中存在一个瑕疵。发行者名称是重复的,这很容易导致不一致性,比如,发行者Danny DeVito可能被输入为"Danny De Vito",用于 一个电影;也可以输入为“Danny deVito",用于另一个。为解决这一问题,可以使用两个DBM文件,主DVD文件使用标题键与(年份,时间长度,发行者ID)值发行者文件使用发行者ID (整数)键与发行者名称值。下一节展示的SQL数据库 版程序将避免这一瑕疵,这是通过使用两个表格实现的,一个用于DVD,另一个用于发行者。
12.2 SQL数据库
大多数流行的SQL数据库的接口在第三方模块中是可用的,Python带有sqlite3 模块(以及SQLite 3数据库),因此,在Python中,可以直接开始数据库程序设计。SQLite是一个轻量级的SQL数据库,缺少很多诸如PostgreSQL这种数据库的功能, 但非常便于构造原型系统,并且在很多情况下也是够用的。
为使后台数据库之间的切换尽可能容易,PEP 249 (Python Database API Specification v2.0)提供了称为DB-API 2.0的API规范。数据库接口应该遵循这一规范,比如sqlite3模块就遵循这一规范,但不是所有第三方模块都遵循。API规范中指定了两种主要的对象,即连接对象与游标对象。表12-1与表12-2中分别列出了这两种对象必须支持的API。在sqlite3模块中,除DB-API 2.0规范必需的之外,其连接对象与游标对象都提供了很多附加的属性与方法。
DVD程序的SQL版本为dvds.sql.py,该程序将发行者与DVD数据分开存储,以 避免重复,并提供一个新菜单,以供用户列出发行者。该程序使用的两个表格在图12-1
def connect(filename):
create= not os.path.exists(filename)
db = sqlite3.connect(filename)
if create:
cursor = db.cursor()
cursor.execute("CREATE TABLE directors ("
"id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, "
"name TEXT UNIQUE NOT NULL)")
cursor.execute("CREATE TABLE dvds ("
"id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, "
"title TEXT NOT NULL, "
"year INTEGER NOT NULL,"
"duration INTEGER NOT NULL, "
"director_id INTEGER NOT NULL, ”
"FOREIGN KEY (director_id) REFERENCES directors)")
db.commit()
return db
sqlite3.connect()函数会返回一个数据库对象,并打开其指定的数据库文件。如果该文件不存在,就创建一个空的数据库文件。鉴于此,在调用sqlite3.connect()之前,我们要注意数据库是否是准备从头开始创建,如果是,就必须创建该程序要使用的表格。所有査询都是通过一个数据库游标完成的,可以从数据库对象的cursor()方法获取。
注意,两个表格都是使用一个ID字段创建的,ID字段有一个AUTOINCREMENT 约束——这意味着SQLite会自动为ID字段赋予唯一性的数值,因此,在插入新记录时,我们可以将这些字段留给SQLite处理。
SQLite支持有限的数据类型——实际上就是布尔型、数值型与字符串——但使用数据'‘适配器”可以对其进行扩展,或者是扩展到预定义的数据类型(比如那些用于日期与datetimes的类型),或者是用于表示任意数据类型的自定义类型。DVD程序并不需要这一功能,如果需要,sqlite3模块的文档提供了很多详细解释。我们使用的外部键语法可能与用于其他数据库的语法不同,并且在任何情况下,只是记录我们的意图,因为SQLite不像很多其他数据库那样需要强制关系完整性,sqlite3另一点与众不同的地方在于其默认行为是支持隐式的事务处理,因此,没有提供显式的“开始事务” 方法。
def add_dvd(db):
title = Console.get_string("Title", "title")
if not title:
return
director = Console.get_string("Director", "director")
if not director:
return
year = Console.get_integer("Year", "year”, minimum=1896,
maximum=datetime.date.today().year)
duration = Console.get_integer("Duration (minutes)", "minutes",
minimum=0,maximum=60*48)
director_id = get_and_set_director(db, director)
cursor = db.cursor()
cursor.execute("INSERT INTO dvds ”
"(title, year, duration, director_id)"
"VALUES (?, ?, ?, ?)",
(title, year, duration, director_id))
db.commit()
这一函数的开始代码与dvds-dbm.py程序中的对应函数一样,但在完成数据的收集后,与原来的函数有很大的差别。用户输入的发行者可能在也可能不在directors表格中,因此,我们有一个get_and_set_director()函数,在数据库中尚无某个发行者时, 该函数就将其插入到其中,无论哪种情况都返回就绪的发行者ID,以便在需要的时候插入到dvds表。在所有数据都可用后,我们执行一条SQL INSERT语句。我们不需要指定记录ID,因为SQLite会自动为我们提供。
在査询中,我们使用问号(?)作为占位符,每个?都由包含SQL语句的字符串后面的序列中的值替代。命名的占位符也可以使用,后面在编辑记录时我们将看到。尽管避免使用占位符(而只是简单地使用嵌入到其中的数据来格式化SQL字符串)也是可能的,我们建议总是使用占位符,并将数据项正确编码与转义的工作留给数据库模块来完成。使用占位符的另一个好处是可以提高安全性,因为这可以防止任意的SQL 被恶意地插入到一个査询中。
def get_and_set_director(db, director):
director_id = get_director_id(db, director)
if directorjd is not None:
return director_id
cursor = db.cursor()
cursor.execute("lNSERT INTO directors (name) VALUES (?)”,(director,))
db.commit()
return get_director_id(db, director)
这一函数返回给定发行者的ID,并在必要的时候插入新的发行者记录。如果某个记录被插入,我们首先尝试使用get_director_id()函数取回其ID。
def get_director_id(db, director):
cursor = db.cursor()
cursor.execute("SELECT id FROM directors WHERE name=?",(director,))
fields = cursor.fetchone()
return fields[0] if fields is not None else None
get_director_id()函数返回给定发行者的ID,如果数据库中没有指定的发行者,就返回None。我们使用fetchone()方法,因为或者有一个匹配的记录,或者没有。(我们知道,不会有重复的发行者,因为directors表格的名称字段有一个UNIQUE约束,在任何情况下,在添加一个新的发行者之前,我们总是先检査其是否存在。)这种取回方法总是返回一个字段序列(如果没有更多的记录,就返回None)。即便如此,这里我们只是请求返回一个单独的字段。
def edit_dvd(db):
title, identity = find_dvd(db, "edit")
if title is None:
return
title = Console.get_string("Title","title", title)
if not title:
return
cursor = db.cursor()
cursor.execute("SELECT dvds.year, dvds.duration, directors.name"
“FROM dvds, directors "
"WHERE dvds.director_id = directors.id AND "
"dvds.id=:id", dict(id=identity))
year, duration, director = cursor.fetchone()
director = Console.get_string("Director", "director", director)
if not director:
return
year = Console,get_integer("Year","year", year, 1896,datetime.date.today().year)
duration = Console.get_integer("Duration (minutes)", "minutes",
duration, minimum=0, maximum=60*48)
director_id = get_and_set_director(db, director)
cursor.execute("UPDATE dvds SET title=:title, year=:year,"
"duration=:duration, director_id=:directorjd "
"WHERE id=:identity", locals())
db.commit()
要编辑DVD记录,我们必须首先找到用户需要 *** 纵的记录。如果找到了某个记录,我们就给用户修改其标题的机会,之后取回该记录的其他字段,以便将现有值作为默认值,将用户的输入工作最小化,用户只需要按Enter键就可以接受默认值。这里,我们使用了命名的占位符(形式为:name),并且必须使用映射来提供相应的值。对SELECT语句,我们使用一个新创建的字典;对UPDATE语句,我们使用的是由 locals()返回的字典。
我们可以同时为这两个语句都使用新字典,这种情况下,对UPDATE语句,我们可以传递 dict(title=title, year=year, duration=duration, director_id=director_id, id=identity)),而非 locals()。
在具备所有字段并且用户已经输入了需要做的改变之后,我们取回相应的发行者ID (如果必要就插入新的发行者记录),之后使用新数据对数据库进行更新。我们采用了一种简化的方法,对记录的所有字段进行更新,而不仅仅是那些做了修改的字段。
在使用DBM文件时,DVD标题被用作键,因此,如果标题进行了修改,我们就需要创建一个新的键-值项,并删除原始项。不过,这里每个DVD记录都有一个唯一性的ID,该ID是记录初次插入时创建的,因此,我们只需要改变任何其他字段的值, 而不需要其他 *** 作。
def find_dvd(db, message):
message = "(Start of) title to " + message
cursor = db.cursor()
while True: .
start = Console.get_stnng(message, "title")
if not start:
return (None, None)
cursor.execute("SELECT title, id FROM dvds "
"WHERE title LIKE ? ORDER BY title”,
(start +"%",))
records = cursor.fetchall()
if len(records) == 0:
print("There are no dvds starting with", start)
continue
elif len(records) == 1:
return records[0]
elif len(records) >DISPLAY_LIMIT:
print("Too many dvds ({0}) start with {1}try entering "
"more of the title".format(len(records),start))
continue
else:
for i, record in enumerate(records):
print("{0}:{1}".format(i + 1, record[0]))
which = Console.get_integer("Number (or 0 to cancel)",
"number", minimum=1, maximum=len(records))
return records[which -1] if which != 0 else (None, None)
这一函数的功能与dvdsdbm.py程序中的find_dvd()函数相同,并返回一个二元组 (DVD标题,DVD ID)或(None, None),具体依赖于是否找到了某个记录。这里并不需要在所有数据上进行迭代,而是使用SQL通配符(%),因此只取回相关的记录。
由于我们希望匹配的记录数较小,因此我们一次性将其都取回到序列的序列中。如果有不止一个匹配的记录,但数量上又少到可以显示,我们就打印记录,并将每条记录附带一个数字编号,以便用户可以选择需要的记录,其方式与在dvds-dbm.py程序中所做的类似:
def list_dvds(db):
cursor = db.cursor()
sql = ("SELECT dvds.title, dvds.year, dvds.duration, "
"directors.name FROM dvds, directors "
"WHERE dvds.director_id = directors.id")
start = None
if dvd_count(db) >DISPLAY_LIMIT:
start = Console.get_string("List those starting with [Enter=all]", "start")
sql += " AND dvds.title LIKE ?"
sql += ” ORDER BY dvds.title"
print()
if start is None:
cursor.execute(sql)
else:
cursor.execute(sql, (start +"%",))
for record in cursor:
print("{0[0]} ({0[1]}) {0[2]} minutes, by {0[3]}".format(record))
要列出每个DVD的详细资料,我们执行一个SELECT査询。该査询连接两个表,如果记录(由dvd_count()函数返回)数量超过了显示限制值,就将第2个元素添加到WHERE 分支,之后执行该査询,并在结果上进行迭代。每个记录都是一个序列,其字段是与 SELECT査询相匹配的。
def dvd_count(db):
cursor = db.cursor()
cursor.execute("SELECT COUNT(*) FROM dvds")
return cursor.fetchone()[0]
我们将这几行代码放置在一个单独的函数中,因为我们在几个不同的函数中都需要使用这几行代码。
我们忽略了 list_directors()函数的代码,因为该函数在结构上与list_dvds()函数非常类似,只不过更简单一些,因为本函数只列出一个字段(name)。
def remove_dvd(db):
title, identity = find_dvd(db, "remove")
if title is None:
return
ans = Console.get_bool("Remove {0}?".format(title), "no")
if ans:
cursor = db.cursor()
cursor.execute("DELETE FROM dvds WHERE id=?", (identity,))
db.commit()
在用户需要删除一个记录时,将调用本函数,并且本函数与dvds-dbm.py程序中 相应的函数是非常类似的。
到此,我们完全查阅了 dvds-sql.py程序,并且了解了如何创建数据库表格、选取 记录、在选定的记录上进行迭代以及插入、更新与删除记录。使用execute()方法,我们可以执行底层数据库所支持的任意SQL语句。
SQLite提供了比我们这里使用的多得多的功能,包括自动提交模式(以及任意其他类型的事务控制),以及创建可以在SQL查询内执行的函数的能力。提供一个工厂函数并用于控制对每个取回的记录返回什么(比如,一个字典或自定义类型,而不是字段序列)也是可能的。此外,通过传递“:memory:”作为文件名,创建内存中的SQLite 数据库也是可能的。
以上内容部分摘自视频课程05后端编程Python22 数据库编程,更多实 *** 示例请参照视频讲解。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。
使用 Python 和 Flask 设计 RESTful API
近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配。
在本文中我将向你展示如何简单地使用 Python 和 Flask 框架来创建一个 RESTful 的 web service。
什么是 REST?
六条设计规范定义了一个 REST 系统的特点:
客户端-服务器: 客户端和服务器之间隔离,服务器提供服务,客户端进行消费。
无状态: 从客户端到服务器的每个请求都必须包含理解请求所必需的信息。换句话说, 服务器不会存储客户端上一次请求的信息用来给下一次使用。
可缓存: 服务器必须明示客户端请求能否缓存。
分层系统: 客户端和服务器之间的通信应该以一种标准的方式,就是中间层代替服务器做出响应的时候,客户端不需要做任何变动。
统一的接口: 服务器和客户端的通信方法必须是统一的。
按需编码: 服务器可以提供可执行代码或脚本,为客户端在它们的环境中执行。这个约束是唯一一个是可选的。
什么是一个 RESTful 的 web service?
REST 架构的最初目的是适应万维网的 HTTP 协议。
RESTful web services 概念的核心就是“资源”。 资源可以用 URI 来表示。客户端使用 HTTP 协议定义的方法来发送请求到这些 URIs,当然可能会导致这些被访问的”资源“状态的改变。
HTTP 标准的方法有如下:
========== ===================== ==================================HTTP 方法 行为 示例
========== ===================== ==================================
GET 获取资源的信息 http://example.com/api/orders
GET 获取某个特定资源的信息 http://example.com/api/orders/123
POST 创建新资源 http://example.com/api/orders
PUT 更新资源 http://example.com/api/orders/123
DELETE 删除资源 http://example.com/api/orders/123
========== ====================== ==================================
REST 设计不需要特定的数据格式。在请求中数据可以以 JSON 形式, 或者有时候作为 url 中查询参数项。
设计一个简单的 web service
坚持 REST 的准则设计一个 web service 或者 API 的任务就变成一个标识资源被展示出来以及它们是怎样受不同的请求方法影响的练习。
比如说,我们要编写一个待办事项应用程序而且我们想要为它设计一个 web service。要做的第一件事情就是决定用什么样的根 URL 来访问该服务。例如,我们可以通过这个来访问:
http://[hostname]/todo/api/v1.0/
在这里我已经决定在 URL 中包含应用的名称以及 API 的版本号。在 URL 中包含应用名称有助于提供一个命名空间以便区分同一系统上的其它服务。在 URL 中包含版本号能够帮助以后的更新,如果新版本中存在新的和潜在不兼容的功能,可以不影响依赖于较旧的功能的应用程序。
下一步骤就是选择将由该服务暴露(展示)的资源。这是一个十分简单地应用,我们只有任务,因此在我们待办事项中唯一的资源就是任务。
我们的任务资源将要使用 HTTP 方法如下:
========== =============================================== =============================HTTP 方法 URL 动作
========== =============================================== ==============================
GET http://[hostname]/todo/api/v1.0/tasks 检索任务列表
GET http://[hostname]/todo/api/v1.0/tasks/[task_id] 检索某个任务
POST http://[hostname]/todo/api/v1.0/tasks 创建新任务
PUT http://[hostname]/todo/api/v1.0/tasks/[task_id] 更新任务
DELETE http://[hostname]/todo/api/v1.0/tasks/[task_id] 删除任务
========== ================================================ =============================
我们定义的任务有如下一些属性:
id: 任务的唯一标识符。数字类型。
title: 简短的任务描述。字符串类型。
description: 具体的任务描述。文本类型。
done: 任务完成的状态。布尔值。
目前为止关于我们的 web service 的设计基本完成。剩下的事情就是实现它!
Flask 框架的简介
如果你读过 Flask Mega-Tutorial 系列,就会知道 Flask 是一个简单却十分强大的 Python web 框架。
在我们深入研究 web services 的细节之前,让我们回顾一下一个普通的 Flask Web 应用程序的结构。
我会首先假设你知道 Python 在你的平台上工作的基本知识。 我将讲解的例子是工作在一个类 Unix *** 作系统。简而言之,这意味着它们能工作在 Linux,Mac OS X 和 Windows(如果你使用Cygwin)。 如果你使用 Windows 上原生的 Python 版本的话,命令会有所不同。
让我们开始在一个虚拟环境上安装 Flask。如果你的系统上没有 virtualenv,你可以从https://pypi.python.org/pypi/virtualenv 上下载:
$ mkdir todo-api$ cd todo-api
$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools............................done.
Installing pip...................done.
$ flask/bin/pip install flask
既然已经安装了 Flask,现在开始创建一个简单地网页应用,我们把它放在一个叫 app.py 的文件中:
#!flask/bin/pythonfrom flask import Flaskapp = Flask(__name__)@app.route('/')def index():return "Hello, World!"if __name__ == '__main__':
app.run(debug=True)
为了运行这个程序我们必须执行 app.py:
$ chmod a+x app.py$ ./app.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
现在你可以启动你的网页浏览器,输入 http://localhost:5000 看看这个小应用程序的效果。
简单吧?现在我们将这个应用程序转换成我们的 RESTful service!
使用 Python 和 Flask 实现 RESTful services
使用 Flask 构建 web services 是十分简单地,比我在 Mega-Tutorial 中构建的完整的服务端的应用程序要简单地多。
在 Flask 中有许多扩展来帮助我们构建 RESTful services,但是在我看来这个任务十分简单,没有必要使用 Flask 扩展。
我们 web service 的客户端需要添加、删除以及修改任务的服务,因此显然我们需要一种方式来存储任务。最直接的方式就是建立一个小型的数据库,但是数据库并不是本文的主体。学习在 Flask 中使用合适的数据库,我强烈建议阅读 Mega-Tutorial。
这里我们直接把任务列表存储在内存中,因此这些任务列表只会在 web 服务器运行中工作,在结束的时候就失效。 这种方式只是适用我们自己开发的 web 服务器,不适用于生产环境的 web 服务器, 这种情况一个合适的数据库的搭建是必须的。
我们现在来实现 web service 的第一个入口:
#!flask/bin/pythonfrom flask import Flask, jsonifyapp = Flask(__name__)tasks = [{
'id': 1,
'title': u'Buy groceries',
'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
'done': False
},
{
'id': 2,
'title': u'Learn Python',
'description': u'Need to find a good Python tutorial on the web',
'done': False
}]@app.route('/todo/api/v1.0/tasks', methods=['GET'])def get_tasks():
return jsonify({'tasks': tasks})if __name__ == '__main__':
app.run(debug=True)
正如你所见,没有多大的变化。我们创建一个任务的内存数据库,这里无非就是一个字典和数组。数组中的每一个元素都具有上述定义的任务的属性。
取代了首页,我们现在拥有一个 get_tasks 的函数,访问的 URI 为 /todo/api/v1.0/tasks,并且只允许 GET 的 HTTP 方法。
这个函数的响应不是文本,我们使用 JSON 数据格式来响应,Flask 的 jsonify 函数从我们的数据结构中生成。
使用网页浏览器来测试我们的 web service 不是一个最好的注意,因为网页浏览器上不能轻易地模拟所有的 HTTP 请求的方法。相反,我们会使用 curl。如果你还没有安装 curl 的话,请立即安装它。
通过执行 app.py,启动 web service。接着打开一个新的控制台窗口,运行以下命令:
$ curl -i http://localhost:5000/todo/api/v1.0/tasksHTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 294
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 04:53:53 GMT
{
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": 1,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
}
]
}
我们已经成功地调用我们的 RESTful service 的一个函数!
现在我们开始编写 GET 方法请求我们的任务资源的第二个版本。这是一个用来返回单独一个任务的函数:
from flask import abort@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])def get_task(task_id):task = filter(lambda t: t['id'] == task_id, tasks)
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
第二个函数有些意思。这里我们得到了 URL 中任务的 id,接着 Flask 把它转换成 函数中的 task_id 的参数。
我们用这个参数来搜索我们的任务数组。如果我们的数据库中不存在搜索的 id,我们将会返回一个类似 404 的错误,根据 HTTP 规范的意思是 “资源未找到”。
如果我们找到相应的任务,那么我们只需将它用 jsonify 打包成 JSON 格式并将其发送作为响应,就像我们以前那样处理整个任务集合。
调用 curl 请求的结果如下:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 151
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:50 GMT
{
"task": {
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
}
}
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 238
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:52 GMT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p><p>If you entered the URL manually please check your spelling and try again.</p>
当我们请求 id #2 的资源时候,我们获取到了,但是当我们请求 #3 的时候返回了 404 错误。有关错误奇怪的是返回的是 HTML 信息而不是 JSON,这是因为 Flask 按照默认方式生成 404 响应。由于这是一个 Web service 客户端希望我们总是以 JSON 格式回应,所以我们需要改善我们的 404 错误处理程序:
from flask import make_response@app.errorhandler(404)def not_found(error):return make_response(jsonify({'error': 'Not found'}), 404)
我们会得到一个友好的错误提示:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 26
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:36:54 GMT
{
"error": "Not found"
}
接下来就是 POST 方法,我们用来在我们的任务数据库中插入一个新的任务:
from flask import request@app.route('/todo/api/v1.0/tasks', methods=['POST'])def create_task():if not request.json or not 'title' in request.json:
abort(400)
task = {
'id': tasks[-1]['id'] + 1,
'title': request.json['title'],
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
添加一个新的任务也是相当容易地。只有当请求以 JSON 格式形式,request.json 才会有请求的数据。如果没有数据,或者存在数据但是缺少 title 项,我们将会返回 400,这是表示请求无效。
接着我们会创建一个新的任务字典,使用最后一个任务的 id + 1 作为该任务的 id。我们允许 description 字段缺失,并且假设 done 字段设置成 False。
我们把新的任务添加到我们的任务数组中,并且把新添加的任务和状态 201 响应给客户端。
使用如下的 curl 命令来测试这个新的函数:
$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasksHTTP/1.0 201 Created
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:56:21 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Read a book"
}
}
注意:如果你在 Windows 上并且运行 Cygwin 版本的 curl,上面的命令不会有任何问题。然而,如果你使用原生的 curl,命令会有些不同:
curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks当然在完成这个请求后,我们可以得到任务的更新列表:
$ curl -i http://localhost:5000/todo/api/v1.0/tasksHTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 423
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:57:44 GMT
{
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": 1,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
},
{
"description": "",
"done": false,
"id": 3,
"title": "Read a book"
}
]
}
剩下的两个函数如下所示:
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])def update_task(task_id):task = filter(lambda t: t['id'] == task_id, tasks)
if len(task) == 0:
abort(404)
if not request.json:
abort(400)
if 'title' in request.json and type(request.json['title']) != unicode:
abort(400)
if 'description' in request.json and type(request.json['description']) is not unicode:
abort(400)
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)