ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# Python Flask 高级编程视频笔记 [TOC] ### flask 路由的基本原理 `add_url_route`可以传入`rule`、`view_function`、`endpoint`,如果不传`endpoint`,会默认将`view_function`的名字作为`endpoint`。 `add_url_route`会将最终的`rule`存放在`url_map`里面,视图函数存放在`view_function`的字典里面。 > `view_functions`为字典,键为`endpoint`,值为视图函数 * 首先`url_map`这个 `map`对象里面必须由我们的 `url` -> `search endpoint`的指向; * 同时`view_functions`里面必须记录`search endpoint`所指向的视图函数。 这样当一个请求进入 `flask`之后才能根据 `url` 顺利的找到对应的视图函数,这就是 `flask`路由的基本原理。 ### 循环引用图解 ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fxhbcn1d20j30x40u04qp.jpg) ### 1\. **拆分视图函数**到单独的模块中去 将视图函数从主执行文件分离出来时,不能直接导入flask的核心对象,导致不能使用flask核心对象来注册视图函数的路由 ### 2\. 只有由**视图函数**或者`http`请求触发的`request`才能得到`get`返回的结果数据 * `flask`默认`request`类型是`localproxy`(本地代理) ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fxhpf1jm9ej31c60p64es.jpg) ### 3\. **验证层** **验证层**:使用`wtforms`进行参数校验 ### 4\. **MVC**模型 * **MVC**模型里绝对不是只有数据,如果只有数据的话,那只能算作数据表 * **MVC**里的*M*是一个业务模型,必须定义很多操作一个个数据字段的业务方法 > **经典面试问题**: 业务逻辑应该写在**MVC**里的哪一层? 业务逻辑最好应该是在**M**(*model*)层编写 ### 5\. **ORM**的含义 **ORM**:关系型数据库和实体间做映射,操作对象的属性和方法,跳过SQL语句 对象关系映射(英语:`Object Relational Mapping`,简称`ORM`,或`O/RM`,或`O/R mapping`),用于实现面向对象编程语言里不同类型系统的数据之间的转换。其实是创建了一个可在编程语言里使用的`虚拟对象数据库`。`Object`是可以继承的,是可以使用接口的,而`Relation`没有这个概念。 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k5d8gsqj30g50d1dgk.jpg) ### 6\. 最基本的数据结构 * **栈**:**后进先出** * **队列**:**先进先出** ### 7\. **`with`语句** `with`语句:上下文管理器可以使用`with`语句 > 实现了上下文协议的对象就可以使用`with`语句 实现了上下文协议的对象通常称为**上下文管理器** 一个对象只要实现了`__enter__`和`__exit__`两个方法,就是实现了**上下文协议** 上下文表达式(with后面的表达式)必须返回一个上下文管理器 示例1: ~~~ 1class A:2    def __enter__(self):3        a = 14​5    def __exit__(self, exc_type, exc_val, exc_tb):6        b = 27​8with A() as obj_A:  9    pass ~~~ * `obj_A` 是 `None`;`A()`直接实例化 `A`,返回的是上下文管理器对象 * as 语句后面的变量不是上下文管理器 * `__enter__` 方法所返回的值会赋给 as 语句后面的变量 ~~~ 1class A:2    def __enter__(self):3        a = 14        return a5​6    def __exit__(self, exc_type, exc_val, exc_tb):7        b = 28​9with A() as obj_A:  # obj_A :110    pass ~~~ 示例2:`文件读写` ~~~ 1try:2    f = open(r'D:\t.txt')3    print(f.read())4finally:5    f.close() ~~~ 使用**with语句**改写 ~~~ 1with open(r'D:\t.txt') as f:2    print(f.read()) ~~~ 示例3:`with语句处理异常` ~~~ 1class MyResource:2    def __enter__(self):3        print('connect to resource')4        return self5​6    def __exit__(self, exc_type, exc_val, exc_tb):7        if exc_tb:8            print('process exception')9        else:10            print('no exception')11        print('close resource connection')12        # return True     # 返回 True 表示已经在 __exit__ 内部处理过异常,不需要在外部处理13        return False    # 返回 False 表示没有在 __exit__ 内部处理异常,需要在外部处理14        # 如果 __exit__ 没有返回,则默认返回 False15​16    def query(self):17        print('query data')18​19​20try:21    with MyResource() as resource:22        1/023        resource.query()24except Exception as ex:25    print('with语句出现异常')26    pass ~~~ ### 8.操作数据库的流程 * 连接数据库 * `flask-sqlalchemy`连接数据库 ~~~ 1SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://username:password@localhost:3306/fisher?charset=utf8' ~~~ 参数解释: `cymysql`:`mysql`数据库的链接驱动 `username:password@localhost:3306`:用户名:密码@服务器地址:端口 `fisher`:数据库名称 `charset=utf8`:指定数据库的编码方式为`utf8` > 采坑: > > **`mysql`数据库必须指定编码方式,否则`commit`的时候会出错。** 时间:2018年10月18日20:39:43 就因此踩了坑,花费了三天的时间。刚开始没有指定数据库的编码方式,结果在用户之间发起鱼漂的时候,储存鱼漂到数据库的时候报如下错误: > > ~~~ > 1sqlalchemy.exc.InternalError: (cymysql.err.InternalError) (1366,...) > ~~~ > > 使用 `vscode`进行远程调试,主要调试了提交数据库的几个操作: > > * 用户注册的时候,需要储存用户数据到数据库,这类 `commit`没问题,储存的是 `user`表 > > * 赠送数据的时候,需要将礼物(书籍)数据添加到数据库,这类 `commit`没问题,储存的是 `gift`表 > > * 添加心愿书籍的时候,需要储存心愿,这类 `commit`没问题,储存的是 `wish`表 > > * 储存鱼漂的时候,`commit`就会报错,这类 `commit`储存的是 `drift`表 > > > 查 `google`确实查到了是 `mysql`编码的问题, > > * 尝试1:修改 `mysql`编码模式为 `utf8`,结果:无效 > > * 尝试2:修改已创建的 `fisher`数据库的编码模式为 `utf8`,结果:无效 > > * 尝试3:修改`mysql`连接方式`SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://username:password@localhost:3306/fisher?charset=utf8'`,结果:无效 > > * 尝试4:修改 > > * 尝试4:删除 `fisher`数据库,重新让 `sqlalchemy`建立数据表,结果:**有效** > > > 原因嘛,猜测为`drift`表的编码模式出现了问题。 > > 至于为什么其他表的编码模式没问题,只有 `drift`这个搞不清楚,以后在捉摸吧。 * `SQL`操作 * 释放资源 使用 * `try` * `except` * `finally` 无论出现什么异常,最终都会执行`final`语句,不会浪费资源,很优雅 另一种方式就是使用**with语句** ### 9\. 进程和线程 #### **进程** 进程:是竞争计算机资源的基本单位 * 每一个应用程序至少需要一个进程 * 进程是分配资源的 * 多进程管理的资源是不能相互访问的 * **多进程**资源共享需要使用**进程通信技术** #### **线程** 线程:是进程的一部分,一个进程可以有一个或多个线程 * 线程:利用 `cpu` 执行代码 * 线程属于进程 * 线程不拥有资源,但是可以访问进程的资源 * 多线程可以更加充分的利用 `cpu` 的性能优势 * 多个线程共享一个进程的资源,就会造成进程不安全 #### **GIL** **全局解释器锁**(英语:`Global Interpreter Lock`,缩写**GIL**) * `python`解释器 * `cpython`:有GIL * `jpython`:无GIL * **锁** * **细粒度锁**:是程序员主动添加的 * **粗粒度锁**:**GIL**,多核 `cpu` 只有 `1` 个线程执行,一定程度上保证了线程安全 * 还有特例情况(无法保证线程安全) > 例: `a += 1` > > * 在 `python` 解释器中会被翻译成 `bytecode`(字节码),`a += 1` 可能会被翻译成多段 `bytecode`,如果解释器正在执行多段 `bytecode` 其中一段的时候被挂起去执行第二个线程,等第二个线程执行完之后再回来,接着执行 `a += 1` 剩下的几段 `bytecode` 的时候就不能保证线程安全了。 > > * 如果 `a += 1` 这段代码的多段 `bytecode` 会一直执行完(不会中断),则可以保证线程安全,但是**GIL**做不到,它只能认一段段的 `bytecode`,它是以 `bytecode` 为基本单位来执行的。 > * `python`多线程到底是不是鸡肋? > node.js 是单进程、单线程的语言 * `cpu`密集型程序:一段代码的大部分执行时间是消耗在 `cpu` 计算上的程序 * 例如:圆周率的计算、视频的解码 * `IO`密集型程序:一段代码的大部分执行时间是消耗在**查询数据库**、**请求网络资源**、**读写文件**等 `IO` 操作上的程序 * 现实中目前写的绝大多数程序都是`IO`密集型的程序 * `python`多线程在 `IO` 密集型程序里具有一定意义,但是不适合`cpu`密集型程序 ### 10\. 多线程 在多线程的情况下,多个请求传入进来,如果用同一个变量名`request`来命名多个线程里的请求会造成混乱,该如何处理? * 可以用字典来处理(字典是`python`中非常依赖的一种数据结构) * `request = {key1:value1, key2:value2, key3:value3, ...}` * 多线程的每个线程都有它的唯一标识`thread_key` * 解决方案:`request = {thread_key1:Request1, thread_key2:Request2, thread_key3:Request3, ...}`,一个变量指向的是字典的数据结构,字典的内部包含不同的线程创建的不同的`Request`实例化对象 * 线程隔离 * 用不同的线程`id`号作为键,其实就是**线程隔离** * 不同的线程在字典中有不同的状态,各个线程的状态都被保存在字典中,互不干扰 * **不同线程**操作**线程隔离**的对象时,互不影响 > 线程`t1`操作`L.a`(对象L的属性a)与线程`t2`操作`L.a`(对象L的属性a) 两者是互不干扰的,各自进行各自的操作 #### 普通对象 * **不同线程操作普通对象的情况** ~~~ 1import threading2import time3​4​5class A:6    b = 17​8​9my_obj = A()10​11​12def worker():13    # 新线程14    my_obj.b = 215​16​17new_t = threading.Thread(target=worker, name='my_test_thread')18new_t.start()19time.sleep(1)20​21​22# 主线程23print(my_obj.b)24# 新线程的修改影响到了主线程的打印结果,因为对象A只是普通对象,不是线程隔离的对象 ~~~ #### 线程隔离对象 * **不同线程操作线程隔离对象的情况** ~~~ 1import threading2import time3​4from werkzeug.local import Local5​6​7# class A(Local):8#     b = 19​10​11my_obj = Local()12my_obj.b = 113​14​15def worker():16    # 新线程17    my_obj.b = 218    print('in new thread b is:' + str(my_obj.b))19​20​21new_t = threading.Thread(target=worker, name='my_test_thread')22new_t.start()23time.sleep(1)24​25​26# 主线程27print('in main thread b is:' + str(my_obj.b))28# 新线程对my_obj.b的修改不影响主线程的打印结果,因为my_obj是线程隔离的对象 ~~~ * `from werkzeug.local import Local`,`Local`是`werkzeug`包里面的,不是`flask`的 * `LocalStack`是可以用来做线程隔离的栈,封装了`Local`对象,把`Local`对象作为自己的一个属性,从而实现线程隔离的栈结构 * `Local`是一个可以用来做线程隔离的对象,使用字典的方式实现线程隔离 * `stack`是栈结构 #### **封装** * **软件世界里的一切都是由封装来构建的,没有什么是封装解决不了的问题!** * **如果一次封装解决不了问题,那么就再来一次!** * **编程也是一种艺术,代码风格要含蓄!** ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5k6tfff4j31kw0t3q9z.jpg) #### `LocalStack` ##### 作为栈 > 基本上来讲,要实现**栈**结构,必须要实现`push`、`pop`、`top`这三个操作 `push` 推入栈 `top` 取栈顶元素,不弹出该元素 `pop` 取栈顶元素,并弹出该元素 **栈**结构只能取栈顶元素(后进先出) (如果栈可以随意的按照下标去结构中的元素,那么**栈**和**列表**之类的数据结构有什么区别呢?) **`规律:很多数据结构,实际上就是限制了某些能力`** ~~~ 1from werkzeug.local import LocalStack2​3​4s = LocalStack()5s.push(1)6​7print(s.top)8print(s.top)9print(s.pop())10print(s.top)11​12​13s.push(1)14s.push(2)15​16print(s.top)17print(s.top)18print(s.pop())19print(s.top)20----------------------------------------------21执行结果:22123124125None262272282291 ~~~ ##### 作为线程隔离对象 > 两个线程拥有两个栈,是相互隔离的,互不干扰 ~~~ 1import threading2import time3​4from werkzeug.local import LocalStack5​6​7my_stack = LocalStack() # 实例化具有线程隔离属性的LocalStack对象8my_stack.push(1)9print('in main thread after push, value is:' + str(my_stack.top))10​11​12def worker():13    # 新线程14    print('in new thread before push, value is:' + str(my_stack.top))15    # 因为线程隔离,所以在主线程中推入1跟其他线程无关,故新线程中的栈顶是没有值的(None)16    my_stack.push(2)17    print('in new thread after push, value is:' + str(my_stack.top))18​19​20new_t = threading.Thread(target=worker, name='my_new_thread')21new_t.start()22time.sleep(1)23​24# 主线程25print('finally, in main thread value is:' + str(my_stack.top))26# 因为线程隔离,在新线程中推入2不影响主线程栈顶值得打印27------------------------------------------------------------------------------------28执行结果:29in main thread after push, value is:130in new thread before push, value is:None31in new thread after push, value is:232finally, in main thread value is:1 ~~~ ##### **经典面试问题** > 1. `flask`使用`LocalStack`是为了隔离什么对象? 答:这个问题很简单 ,什么对象被推入栈中就是为了隔离什么对象,`AppContext`(应用上下文)和`RequestContext`(请求上下文)被推入栈中,所以是为了隔离`AppContext`(应用上下文)和`RequestContext`(请求上下文) > > 2. 为什么要隔离这些对象? 表面原因:在多线程的环境下,每个线程都会创建一些对象,如果我们不把这些对象做成线程隔离的对象,那么很容易发生混淆,一旦发生混淆之后,就会造成我们程序运行的错误 根本原因:我们需要用一个变量名,同时指向多个线程创建的多个实例化对象,这是不可能的。但是我们可以做到当前线程在引用到`request`这个变量名的时候,可以正确的寻找到当前线程(它自己)创建的实例化对象。 > > 3. 什么是线程隔离的对象和被线程隔离的对象? `LocalStack`和`Local`是**线程隔离的对象** `AppContext`(应用上下文)和`RequestContext`(请求上下文)是**被线程隔离的对象** > > 4. `AppContext`(应用上下文)和`Flask`核心对象的区别? 这两个是两个对象,`Flask`核心对象`app`将作为一个属性存在于`AppContext`(应用上下文)下!!! > > 5. `Flask`的核心对象可以有多个吗? `Flask`核心对象在全局里只有一个。因为`app`是在入口文件里创建的,入口文件是在主线程里去执行的,所以以后无论启动多少个线程,都不会执行`create_app()`了。 > * **使用线程隔离的意义**:`使当前线程能够正确引用到他自己所创建的对象,而不是引用到其他线程所创建的对象` ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5k00g4m8j318m0pwq82.jpg) ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k7eqmfnj31kw0j576h.jpg) ### 11\. `flask`开启多线程的方法 在入口文件将`threaded=True`开启 ![](https://ws1.sinaimg.cn/large/0069RVTdgy1fuvbfagzcgj31ie0ayjtb.jpg) ### 12\. `ViewModel`层的作用 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5k03a3auj31kw0vmwld.jpg) * 页面所需数据结构与**原始数据结构**是一一对应的时候: * 原始数据可以直接传给页面 * 页面所需数据结构与**原始数据结构**不一致: * 需要`ViewModel`对原始数据结构进行`裁剪`、`修饰`、`合并` 因为原始数据并不一定能满足客户端显示的要求,`ViewModel`给了调整数据的机会,不同的页面对数据差异化的要求,可以在`ViewModel`里进行集中处理。 > 将`author`列表转换成字符串再传给客户端: ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k7vo7d4j312y0oo41g.jpg) > > ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k0xi7v0j31160m8q5q.jpg) **返回`author`列表的灵活性要比返回字符串高**,返回列表,客户端可以根据需求使用不同的符号将作者分割链接,但是字符串就限制了客户端的选择。 对于返回客户端`author`数据的格式,`web`编程经验的个人建议: > > * 如果我们正在做的是单页面前后端分离的应用程序,建议`author`保持列表的形式直接返回到客户端去,让客户端使用`JavaScript`来操作或者解析列表。 > > * 如果是在做网站的话,建议`author`在`ViewModel`里处理。 > > > 原因:`JavaScript`处理这些事情非常方便,但如果是模板渲染的方式来渲染`html`的话,我们先把数据处理好,直接往模板里填充会是更好的选择! **数据在哪里处理,可以根据实际情况而定!** ### 13\. 面向对象 #### `面向对象`的类 * 描述自己的特征(数据) * 使用类变量、实例变量来描述自己的特征 * 行为 * 用方法来定义类的行为 > 对面向对象理解不够深刻的同学经常写出只有行为没有特征的类,他们所写的类里大量的都是方法而没有类变量、实例变量,这种类的本质还是`面向过程`,因为面向过程的思维方式是人类最为熟悉的一种思维方式,所以比较容易写出面向过程的类。 `面向过程`的基本单位是函数,`面向对象`的基本单位是类 虽然使用了`class`关键字并且将一些方法或者函数封装到`class`内部,但是并没有改变这种面向过程的实质。 `面向对象`是一种思维方式,并不在于你的代码是怎么写的,如果说你的思维方式出现错误,那你肯定是写不出面向对象的代码的。 * 如何去审视自己的类?去判断我们写出来的类到底是不是一个`伪面向对象`? * 如果一个类有大量的**可以被标注为**`classmethod`或者`staticmethod` 的静态方法,那么你的类封装的是不好的,并没有充分利用面向对象的特性。 ### 14\. 代码解释权的反转 * 代码的解释权不再由函数的编写方所定义的,而是把解释的权利交给了函数的调用方 ![](https://ws1.sinaimg.cn/large/006tKfTcgy1g0jxvqxxrtj31cc0h4goe.jpg) ~~~ 1return jsonify(books)2# 报错,因为books是一个对象,对象时无法进行序列化的3​4return jsonify(books.__dict__)5# 将books取字典之后同样会报错,因为books里面包含对象,所包含的对象无法进行序列化6​7return json.dumps(books, default=lambda o: o.__dict__)8# 最后使用json.dumps(),完美解决问题9​ ~~~ > 在`json.dumps()`的内部处理这些别人传过来的参数的时候,我们是不知道怎么去解释它的,所以我们把解释权交给函数的调用方,由函数的调用方把不能序列化的类型转化为可以序列化的类型,转移解释权的思维使用`函数式编程`是很容易编写的。在设计`json.dumps()`的时候要求函数的调用方传递进来一个函数,传递进来的函数它的具体的实现细节是由函数的调用方编写的,我们不需要关心函数内部具体的实现细节,一旦遇到了不能序列化的对象就调用`func(obj)`函数,让`func(obj)`负责把不能序列化的类型转化为可序列化的类型,我们只需要关注`return`的结果就行了 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fux6z1wuyhj31gq0tw0z8.jpg) ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k0xwm6kj31160m8q5q.jpg) ### 15\. 单页面和网站的区别 > 经典面试问题: 单页面和普通网站的区别? > > * `单页面`: > > 1. 并不一定只有一个页面; > > 2. 最大的特点在于数据的渲染是在客户端进行的; > > 3. 单页面应用程的业务逻辑,也就是说数据的运算主要还是集中在客户端,用`JS`去操作的。 > > * `多页面普通网站`:大多数情况下数据的渲染或者模板的填充是在服务端进行的。 > #### **普通网站** ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fux8javeqxj31a40qk0wd.jpg) ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fux8kwd41cj31kw0pa79o.jpg) #### **单页面** * 单页面中`html`也是静态资源 ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fw5k0zwtewj31ho0o6n0s.jpg) ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fux8n53dlpj31gs0pwtcy.jpg) ### 16\. `flask`静态文件访问原理 * 在实例化`flask`对象的时候,可以指定`static_folder`(静态文件目录)、`static_url_path`(静态文件访问路由) > ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fuxi9v0m34j31kw16iarn.jpg) > > ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fuxic6kbypj31kw08oadm.jpg) > > ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fuxidvzh3nj31ds0cg41m.jpg) > > ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5kaj36v8j31ks0da77r.jpg) `_static_folder`和`_static_url_path`默认值为`None` * `blueprint`(蓝图)的静态资源操作与`flask`核心对象操作一样 ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5ka7tl1oj31d007k75z.jpg) ### 17\. 模板文件的位置与修改方案 `templates`文件位置可以由`template_folder`指定模板文件路径 * 需求:将字典填充到`html`里,再将填充之后的`html`返回到客户端去 `flask`为了让我们能够在模板里面很好的解析和展示数据,它引入了一个模板引擎`Jinja2` > 像`Jinja`这种可以帮助我们在`html`中渲染和填充数据的语言通常被称为`模板语言` ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5kb6jkk7j30rg0oiwhj.jpg) 在`Jinja`里有两个流程控制语句是经常用到的:(`Jinja`流程控制语句都必须写在`{% %}`里面) * `if`语句(条件语句) ~~~ 1   {% if data.age < 18 %}2       <ul>{{ data.name }}</ul>3   {% elif data.age == 18%}4       <ul>do some thing</ul>5   {% else %}6       <ul>{{ data.age }}</ul>7​8   {% endif %} ~~~ * `for in`语句(循环控制语句) ~~~ 1   {% for foo in [1,2,3,4,5] %}2       {{ foo }}3       <div>999</div>4​5   {% endfor %}6​7   {% for key, value in data.items() %}8       {# 注意for后面跟的是键值对,用逗号分开。#}9       {# data后面要加上iterms(),要确保是个可迭代对象,否则会报错 #}10       {{ key }}11       {{ value }}12​13   {% endfor %} ~~~ #### 模板继承的用法 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5kbmrxbrj31c80moq67.jpg) 1. 写好一级`html`页面:`layout.html` ,包含`block`模块。![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5k11fw1aj31cc0sewk8.jpg) 2. 再需要继承的`html`页面顶端导入基础`html`:`{% extends ‘layout.html' %}`。 3. 使用`{{ super() }}`关键字可以继承一级`html`页面中的`block`模块的内容,不使用的话只能替换,无法继承。![](https://ws1.sinaimg.cn/large/006tNbRwgy1fuym0r5qkqj31kw15adrx.jpg) #### 模板语言的过滤器 过滤器的基本用法是在关键是后面加上竖线 `|` * `default`过滤器:是用来判断属性是否存在的,当访问一个不存在的属性时,`default`后面的赋值才会被显示,当访问一个存在的属性时,`default`后面的赋值不会被显示。 ~~~ 1{# data.name data.age 存在,data.school 不存在#}2​3{{ data.school | default=('未名') }} ~~~ * `|` 有点像`linux`里的管道符号,其表达的意思是`值得传递` ~~~ 1{# data.name = None, data.age 存在,data.school 不存在 #}2​3{{ data.name == None | default=('未名') }}4{# 页面最终返回的结果是 True,竖线并不是表示前面的语句成立,就执行后面的语句5竖线表示的是值的传递,前面等式成立为 True,将 值True 传给后面的语句,default判断出 True是存在的,所以页面返回的是 True #} ~~~ 更复杂的示例: ~~~ {# data.name = None, data.age 存在,data.school 不存在 #} {{ data.school | default(data.school) | default=('未名') }} {# 页面最终返回的结果是未名 第一个 default 首先判断第一个 data.school 是否存在,不存在 然后 defuult 再对其括号内的 data.school 求值,不存在 然后再将值传给第二个 default,因为接收到的是不存在的结果 所以第二个 default 会把 '未名' 显示出来 #} ~~~ * `length`过滤器(长度过滤器):是用来判断长度的 ~~~ {# data.name、data.age 存在 #} {{ data | length() }} {# 页面最终的返回结果为 2 #} ~~~ ### 18\. 反向构建`URL` 反向构建`url`使用的是`url_for`,使用方法: ~~~ {{ url_for('static', filename='test.css') }} {# static 是静态文件,test.css 是需要加载的 css文件 #} ~~~ **凡是涉及到`url`生成的时候,建议都使用`url_for`,例如`CSS`文件的加载、`JS`文件的加载、图片的加载,包括视图函数里重定向的时候一样可以使用`url_for`来生成** * 加载`css`文件的方法 * 使用硬路径 * 缺点:当服务器域名地址、域名端口等需要改动过的时候非常麻烦 ~~~ <link rel="stylesheet" href="http://localhost/5000/static/test.css"> ~~~ * 使用相对路径 * 缺点:当需要修改静态资源(css、js等)路径的时候非常麻烦 ~~~ <link rel="stylesheet" href="../static/test.css"> ~~~ * 使用反向构建`url`的方法 * 非常方便、完美解决问题 ~~~ <link rel="stylesheet" href="{{ url_for('static', filename='test.css') }}"> ~~~ ### 19\. `Messaging Flash`消息闪现 #### 消息闪现的用法 * 导入`flash`函数:`from flask import flash` * 在视图函数中调用`flash(message, category='message')`函数: ~~~ flash('你好,这里是消息闪现!', category='errors') flash('Hello, this is messaging flash!', category='warning') flash('你好,这里是消息闪现!') ~~~ * 在`html`页面使用模板语言使用`get_flashed_messages()`方法获得需要闪现的消息 * 问题1:如何获取`get_flashed_messages()`函数的调用结果呢? * 按照`python`惯有的操作思维,先定义一个变量,再引用这个变量就可以获得这个函数的调用结果了。 * 问题2:在模板语言里如何定义一个变量? ~~~ {# 使用`set`关键字定义一个变量 #} {% set messages = get_flashed_messages %} {{ messages }} {# 正常调用 messages #} ------------------------------------------- ['你好,这里是消息闪现!', 'Hello, this is messaging flash!', '你好,这里是消息闪现!'] {# 页面最终返回结果 #} ~~~ * 需要在配置文件里配置`SECRET_KEY`才能正确显示消息闪现 * `SECRET_KEY`本身就是一串字符串,但是要尽可能的保证它是独一无二的,换句话说`SECRET_KEY`就是一个秘钥 * 当`flask`需要去操作加密数据的时候,它需要读取`SECRET_KEY`,并且把秘钥运用到它的一系列算法中,最终生成加密数据 * `flask`消息闪现需要用到`session`,`flask`里面的`session`是客户端的不是服务端的,所以说加密对`flask`来说是极其重要的,所以说我们需要给应用程序配置`SECRET_KEY` * 服务端的数据是相对比较安全的,但是如果数据是储存在客户端的,那么最好把数据加密,因为客户端是不能信任的,很容易被篡改 #### `block`变量作用域 在一个`block`里定义的变量的作用于只存在于该`block`中,不能在其他`block`中使用 #### `with`语句变量作用域 模板语言里的`with`语句内定义的变量,只能在`with`语句内部使用,不能在外部使用 ~~~ {% with messages = get_flashed_messages() %} {{ messages }} {% endwith %} {# messages 变量只能在 with 语句内部使用 #} ~~~ > **Filtering Flash Messages Optionally you can pass a list of categories which filters the results of get\_flashed\_messages(). This is useful if you wish to render each category in a separate block.** ~~~ {% with errors = get_flashed_messages(category_filter=["error"]) %} {% if errors %} <div class="alert-message block-message error"> <a class="close" href="#">×</a> <ul> {%- for msg in errors %} <li>{{ msg }}</li> {% endfor -%} </ul> </div> {% endif %} {% endwith %} ~~~ ### 20\. 搜索页面详解 建议: * 加载`CSS`文件的时候,一般写在`html`文件的顶部 * 加载`JS`文件的时候,一般写在`html`文件的底部 ~~~ class BookViewModel: def __init__(self, book): self.title = book['title'] self.publisher = book['publisher'] self.author = '丶'.join(book['author']) self.pages = book['pages'] or '' self.price = book['price'] self.summary = book['summary'] or '' self.image = book['image'] @property def intro(self): intros = filter(lambda x: True if x else False, [self.author, self.publisher, self.price]) return ' / '.join(intros) # filter 为过滤器 # lambda 表达式 ~~~ > `filter`函数(过滤器): 过滤规则是由`lambda`表达式来定义的, 如果`lambda`表达式某一项数据是`False`,那么该项数据就会被过滤掉; 如果返回的是`Ture`,该项数据会被保留。 在`html`页面使用模板语言调用`intro`的数据: ~~~ {% for book in books.books %} <div class="row col-padding"> <a href="{{ url_for('web.book_detail', isbn=book.isbn) }}" class=""> <div class="col-md-2"> <img class="book-img-small shadow" src="{{ book.image }}"> </div> <div class="col-md-7 flex-vertical description-font"> <span class="title">{{ book.title }}</span> {# <span>{{ [book.author | d(''), book.publisher | d('', true) , '¥' + book.price | d('')] | join(' / ') }}</span>#} <span>{{ book.intro }}</span> <span class="summary">{{ book.summary | default('', true) }}</span> </div> </a> </div> {% endfor %} ~~~ 页面最终的展示效果: ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k14k5twj317m0a6acq.jpg) > 因为搜索结果页面展示的时候,需要展示`作者`、`出版社`、`价格`并将这三项数据使用`/`连接,但是获取的数据并不是都有这三项数据,或者原始数据包含该项数据,但是该项数据为`空`,那么就会造成`曹雪芹//23.00元`这种情况,所以为了解决这个问题,引入了`intro`函数,我们在`intro`函数里判断三项原始数据是否存在且是否为空,若存在且不为空则返回数据,如果存在且为空则返回`False`,如果不存在则返回`False`,过滤完成后得到一个`intros`列表,最后使用`return`语句将`intros`列表使用`/`连接起来再返回。 #### `@property`装饰器 使用`@property`是让`intro`函数可以作为属性的方式访问,就是将类的方法转换为属性 * 模板语言里 intro 作为`函数`的形式访问:`{{ book.intro() }}` * 模板语言里 intro 作为`属性`的形式访问:`{{ book.intro }}` > 前文示例中的`intro`是数据,应该用属性访问的方式来获取数据,而不应该用行为的方式来表达数据 对象的两个特性: * 数据是用来描述它的特征的 * 方法(或者函数)是用来描述它的行为的 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5kcvfnkwj31kw098acb.jpg) ### 21\. 业务模型 * `book`模型 * `user`模型 * `gift`模型:展示用户与书籍之间关系的 > 如何在`gift`模型中表示`user`呢? 在`gift`模型里面引用`user`模型,`sqlalchemy`提供了`relationship`函数用来表明引用的关系。 ~~~ from sqlalchemy import Column, Integer, Boolean, ForeignKey, String from sqlalchemy.orm import relationship from app.models.base import Base class Gift(Base): id = Column(Integer, primary_key=True) user = relationship('User') uid = Column(Integer, ForeignKey('user.id')) isbn = Column(String(15), nullable=False) # 因为书籍的数据是从yushu.im的 API 获取的,不是从数据库获取的,所以不能从数据库关联 # 赠送书籍的 isbn 编号是可以重复的,原因很简单 # 例如:A 送给 B 挪威的森林;C 也可以送给 D 挪威的森林。 # 从数据库关联 book 模型的写法跟关联 user 模型的写法一样 # book = relationship('Book') # bid = Column(Integer, ForeignKey('book.id')) launched = Column(Boolean, default=False) # 表明书籍有没有赠送出去,默认 False 未赠送出去 ~~~ #### 假删除 业务模型最终都会在数据库生成一条一条的记录 * 物理删除:直接从数据库里删除记录 * 缺点:删除之后找不回来 > 互联网有时候需要分析用户的行为: 一个用户曾经要赠送一份礼物,后来他把赠送礼物取消了,不想再赠送书籍了。 如果把这条记录直接从数据库里删除之后,是没有办法对用户的历史行为进行分析的 所以大多是情况都不会采用物理删除,而是用`假删除`或者`软删除` `假删除`或者`软删除`:是新增加一个属性`status`,用`status`表示这条数据是否被删除 > `status = Column(SmallInteger, default=1)` `1`表示保留记录,`0`表示删除记录 通过更改`status`的状态在决定是否展示这条记录,实际上这条数据一直存在于数据库当中 基本上所有模型都需要`status`属性,所以将`status`写在基类里,通过继承使所有模型获得`status`属性 ~~~ from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Column, Integer, SmallInteger db = SQLAlchemy() class Base(db.Model): __abstract__ = True # 作用:不让 sqlalchemy 创建 Base 数据表 create_time = Column('create_time', Integer) status = Column(SmallInteger, default=1) ~~~ > 创建`Base`之后,`sqlalchemy`会自动创建`Base`数据表,但是我们并没有在`Base`类里定义`primary_key`,所以运行时会报错。创建`Base`类只是想让模型继承它,并不想创建`Base`数据表(没有需求,没有任何意义)。在`Base`类里加入`__abstract__ = True`可以让`sqlalchemy`不去创建数据表 ### 22\. 用户注册 ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k1k0seej31kw0e3abv.jpg) * `request.form`可以获取用户提交的表单信息 * `wtforms.validators.DataRequired`验证,也就是代表了该字段为必填项,表单提交时必须非空。 ~~~ from wtforms import Form, StringField, PasswordField from wtforms.validators import DataRequired, Length, Email class RegisterForm(Form): email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮件不符合规范')]) password = PasswordField(validators=[DataRequired(message='密码不可以为空,请输入你的密码'), Length(6, 32)]) nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')]) ~~~ #### 用户密码加密 使用`werkzeug.security`包里的`generate_password_hash`函数对用户密码加密 ~~~ from werkzeug.security import generate_password_hash user.password = generate_password_hash(form.password.data) ~~~ #### 修改数据库表单的名称 默认情况下,在模型里定义的字段的名字就是生成的数据库表单的名字 * 修改生成的数据库表名称的方法: * 使用`__tablename__`修改 * 修改生成的数据库表字段名称的方法: * `传入字符串` ~~~ from sqlalchemy import Column from app.models.base import Base class User(Base): __tablename__ = 'user' # 添加 __tablename__ 指定生成的数据库表名为 user _password = Column('password') # 在 Column 里传递字符串指定表字段的名字 id = Column(Integer, primary_key=True) nickname = Column(String(24), nullable=False) phone_number = Column(String(18), unique=True) confirmed = Column(Boolean, default=False) ~~~ #### `python`动态赋值 ~~~ @web.route('/register', methods=['GET', 'POST']) def register(): form = RegisterForm(request.form) # 实例化注册表单,获取用户提交的表单 if request.method == 'POST' and form.validate(): user = User() # 实例化用户 user.set_attrs(form) # 将真实用户与服务器用户绑定,相应属性赋值 return render_template('auth/register.html', form={'data': {}}) ~~~ `set_attrs`使用了`python`作为动态语言的优势,在基类`Base`里复写了`set_attrs`方法,所有模型都继承了`set_attrs`方法 ~~~ class Base(db.Model): __abstract__ = True create_time = Column('create_time', Integer) status = Column(SmallInteger, default=1) def set_attrs(self, attrs_dict): for key, value in attrs_dict.items(): if hasattr(self, key) and key != 'id': # 主键 id 不能修改 setattr(self, key, value) # set_attrs 接收一个字典类型的参数,如果字典里的某一个 key 与模型里的某一个属性相同 # 就把字典里 key 所对应的值赋给模型的相关属性 ~~~ #### 自定义验证器 如何校验业务逻辑相关的规则?(使用自定义验证器) > 比如:`email`符合电子邮箱规范,但是假如数据库里已经存在了一个同名的`email`,这种情况该怎么处理?大多数同学在写代码的时候也会进行业务性质的校验,但是很多人都会把业务性质的校验写到视图函数里面去。建议:**业务性质的校验也应该放到`form`里进行统一校验** ~~~ from wtforms import Form, StringField, PasswordField from wtforms.validators import DataRequired, Length, Email, ValidationError from app.models.user import User class RegisterForm(Form): email = StringField(validators=[ DataRequired(), Length(8, 64), Email(message='电子邮件不符合规范')]) password = PasswordField(validators=[ DataRequired(message='密码不可以为空,请输入你的密码'), Length(6, 32)]) nickname = StringField(validators=[ DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')]) # 自定义验证器,验证 email def validate_email(self, field): if User.query.filter_by(email=field.data).first(): raise ValidationError('电子邮件已被注册') # 自定义验证器,验证 nickname def validate_nickname(self, field): if User.query.filter_by(nickname=field.data).first(): raise ValidationError('该昵称已被注册') ~~~ #### `cookie` `cookie`本来的机制:哪个网站写入的`cookie`,哪个网站才能获取这个`cookie` > 也有很多技术实现跨站`cookie`共享 `cookie`的用途: * 用户票据的保存 * 广告的精准投放 #### 用户验证 建议:将用户密码验证的过程放在用户模型里,而不要放在视图函数里 * 邮箱验证 * 直接在数据库查询邮箱 * 密码验证 * 1. 现将用户提交的明文密码加密 * 2. 再与数据库储存的加密密码进行比对 ~~~ from werkzeug.security import check_password_hash def check_password(self, raw): return check_password_hash(self._password, raw) # raw 为用户提交的明文密码 # self._password 为数据库储存的加密的密码 # 使用 check_password_hash 函数可以直接进行加密验证 # 验证过程: # 1.先将明文密码 raw 加密; # 2.再与数据库里的密码进行比对; # 3.如果相同则返回 True,如果不相同则返回 False ~~~ #### 用户登录成功 用户登陆成功之后: 1. 需要为用户生成一个票据 2. 并且将票据写入`cookie`中 3. 还要负责读取和管理票据 > 所以说整个登录机制是非常繁琐的,自己去实现一整套的`cookie`管理机制是非常不明智的, 幸运的是`flask`提供了一个插件,可以完全使用过这个插件来管理用户的登录信息 ##### 使用`flask-login`插件管理用户的登录信息 1. 安装插件 ~~~ pip install flask-login ~~~ 2. 插件初始 因为`flask-login`插件是专为`flask`定制的插件,所以需要在`app`目录下的`init.py`文件中将插件初始化 ~~~ from flask_login import LoginManager login_manager = LoginManager() # 实例化 LoginManager def create_app(): app = Flask(__name__) app.config.from_object('app.secure') app.config.from_object('app.setting') register_blueprint(app) db.init_app(app) login_manager.init_app(app) # 初始化 login_manager # 写法一:传入关键字参数 # db.create_all(app=app) # 写法二:with语句 + 上下文管理器 with app.app_context(): db.create_all() return app ~~~ 3. 保存用户的票据信息 ~~~ from flask_login import login_user @web.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm(request.form) if request.method == 'POST' and form.validate(): user = User.query.filter_by(email=form.email.data).first() if user and user.check_password(form.password.data): login_user(user, remember=True) # remember=True 表示免密登录,flask默认免密登录的有效时长为365天 # 如果需要更改免密登录的有效时长,则可以在 flask 的配置文件中设置 REMENBER_COOKIE_DURATION 的值 else: flash('账号不存在或密码错误') return render_template('auth/login.html', form=form) ~~~ > 这里并不直接操作`cookie`,而是通过`login_user(user)`间接的把用户票据写入到`cookie`中。 票据到底是什么?我们往`cookie`里写入的又是什么? > > * 我们往`login_user(user)`里传入了一个我们自己定义的`user`模型,那么是不是说`login_user(user)`把我们用户模型里所有的数据全部写入到`cookie`中了呢? 并不是,因为这个模型是我们自己定义的,我们自己定义的数据可能非常的多,全部写入不现实;而且其中的一些信息是根本不需要写入`cookie`中的。 > > * 那么最关键的、最应该写入`cookie`中的是什么信息?是用户的`id`号!因为`id`号才能代表用户的身份 > > * 那么`login_user(user)`怎么知道我们自己定义的`user`模型下面这么多个属性里,哪一个才是代表用户身份信息的`id`号呢? 所以`flask_login`这个插件要求在`user`模型下定义一系列的可以获取用户相应属性的方法,这样`flask_login`可以直接调用这些方法去获取模型的属性,而不用去识别用户属性 > > * 可以继承`flask_login`插件里`UserMixin`基类,从而获得这些方法(获取模型属性的方法),避免重复写。此种方法**对模型里属性的名称有硬性要求**,比如`id`不能为`id`,因为调用的时候会继承`UserMixin`里的方法,`UserMixin`方法里是写死了的,如果属性名称不一样需要在`user`模型里覆写获取属性的相关方法。 ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k15t7xoj31ei0buq6g.jpg) > #### 访问权限控制 网站的视图函数大致分为两类: * 需要用户登录才能访问 * 不需要登录即可访问 > 如果需要限制一个视图函数需要登录才能访问,怎么办? 如果仅仅是将用户信息写入到`cookie`中,我们完全不需要引入第三方插件,但是如果考虑到要对某些视图函数做权限的控制的话,第三方插件就非常有用了。 对于一个用户管理插件而言,最复杂的实现的地方就在于对权限的控制 使用第三方插件的装饰器来进行登录权限的控制: 1. 在视图函数前加上装饰器 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5k18cn17j311g0d275z.jpg) #### 重定向攻击 1. 当用户访问一个需要登录的视图函数的时候,会自动跳转到登录页面(生成附加`next`信息的登录页面`url`) ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k19sufxj31660dwwi6.jpg) 2. 登录页面的`url`后面会添加`next`信息,例如: `http://127.0.0.1:5000/login?next=%2Fmy%2Fgifts` `next`表示的就是登陆完成后所需要跳转的页面(重定向),一般是定向为原来需要访问的页面 3. 如果用人恶意篡改`next`信息,例如: `http://127.0.0.1:5000/login?next=http://www.baidu.com` 使用该链接登录之后就会跳转到百度页面,这种被称为`重定向攻击` 4. 那么如何防止这种重定向攻击呢? 需要在视图函数中判断`next`的内容是否为`/`开头,因为如果是`/`开头的话表明还是在我们域名内部跳转,要跳转到其他域名的话必须以`http://`开头 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5k1736qaj31ee0m2dmk.jpg) > `next.startswith()`函数可以判断`next`信息是否以`/`开头 ### 23\. `wish`模型 #### 1\. 分析得到`wish`模型几乎和`gift`一模一样,所以直接复制过来稍微改一下就行了 #### 2\. 点击`赠送此书`和`加入到心愿清单`要求用户登录,在这两个视图函数前加上`@login_required` ~~~ - 赠送此书:`save_to_gift` - 加入到心愿清单:`save_to_wish` ~~~ ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k1g7utrj318q0rmn0m.jpg) #### 3\. `gift`还有`uid`属性需要赋值,`uid`从哪里获取呢? 当一个用户登录之后,我们可以通过第三方用户登录管理插件`flask_login`的`current_user`获取当前用户的`id`,`current_user`为什么可以获取当前用户呢?因为之前我们在`user`模型里定义了`get_user`方法,该方法可以让我们通过`uid`获取用户模型,所以这里的`current_user`本质上就是一个实例化的`user`模型,所以可以用`current_user.id`获取当前用户的`uid`。 ~~~ from app import login_manager @login_manager.user_loader def get_user(uid): return User.query.get(int(uid)) ~~~ ~~~ @web.route('/gifts/book/<isbn>') @login_required def save_to_gifts(isbn): gift = Gift() gift.isbn = isbn gift.uid = current_user.id db.session.add(gift) db.session.commit(gift) # 将数据写入到数据库 ~~~ #### 4\. 鱼豆 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k1l3pijj31800iwac8.jpg) 为了后期方便修改上传一本书所获得的鱼豆数量,所以将上传一本书所获得的鱼豆数量设定为变量写到配置文件里,再在需要的时候读取配置文件。 ~~~ BEANS_UPDATE_ONE_BOOK = 0.5 current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK'] ~~~ #### 5\. 前面第3点中的`save_to_gift`视图函数有很多问题 * `isbn`编号没有验证 * 不清楚传入的`isbn`编号符不符合`isbn`规范 * 不清楚传入的`isbn`编号是否已存在与数据库当中 * 如果这本书不在数据库,则需要上传之后才能赠送 * 不清楚传入的`isbn`编号是否已经存在于赠送清单中 * 如果这本书在赠送清单,则不需要加入赠送清单了 * 不清楚传入的`isbn`编号是否已经存在于心愿清单中 * 如果这本在心愿清单,表示用户没有这本书,那么用户就无法赠送这本书 * 在`user`模型的内部添加判断书籍能否添加到赠送清单的条件: ~~~ class User(UserMixin, Base): ... def can_save_to_list(self, isbn): if is_isbn_or_key(isbn) != 'isbn': return False yushu_book = YuShuBook() yushu_book.search_by_isbn(isbn) if not yushu_book.first(): return False # 不允许一个用户同时赠送多本相同的图书 # 一个用户不能同时成为一本图书的赠送者和索要者 # 这本图书既不在赠送清单中也不再心愿清单中才能添加 gifting = Gift.query.filter_by(isbn=isbn, uid=self.id, lunched=False).first() wishing = Wish.query.filter_by(isbn=isbn, uid=self.id, lunched=False).first() if gifting and wishing: return True else: return False ~~~ * 在`save_to_gift`视图函数中调用判断条件的函数来判断是否将书籍添加到赠送清单: ~~~ @web.route('/gifts/book/<isbn>') @login_required def save_to_gifts(isbn): if current_user.can_save_to_list(isbn): gift = Gift() gift.isbn = isbn gift.uid = current_user.id current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK'] db.session.add(gift) db.session.commit(gift) # 将数据写入到数据库 else: flash('这本书已添加至你的赠送清单或已存在与你的心愿清单,请不要重复添加') ~~~ > **`can_save_to_list`写在`user`里正不正确?** `can_save_to_list`是用来做校验的,之前做校验的时候都是建议大家把校验放在`Form`里,为什么这里没有写在`Form`里呢?其实这个原因就在于编程是没有定论的,不是说只要是校验的都要全部放在`Form`里,而是要根据你的实际情况来选择,放在`Form`里有放在`Form`里的好处,放在`user`模型里有放在`user`模型里的好处。你把`can_save_to_list`看做参数的校验,放在`Form`里是没有错的,但是这个`can_save_to_list`可不可以看做是用户的行为呢?也可以看做是用户的行为,既然是用户的行为,那么放在`user`模型里也是没有错的。而且放在`user`模型里是有好处的,好处就是它的复用性会更强一些,以后如果需要相同搞的判断你的时候,放在`Form`校验里用起来是很不方便的,但是如果放在`user`模型里用起来是相当方便的。 所以说呢要根据实际情况而定,编程是没有定论的,只要你能找到充分的依据,那么你就可以这么做。 #### 6\. 事物 事物是数据库里的概念,但是放到模型的方式里,它也是存在的。 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k1lzurwj31g00f4te4.jpg) 这里是操作了两张数据表: * `gift`表 * `user`表 如果在操作完`gift`表之后突然程序中断了,`user`表中的`beans`并没有加上,这样就会造成数据库数据的异常。所以说我们必须要保证要么两个数据表同时操作,要么都不操作,这样才能保证我们数据的完整性。 那么这样在数据库保证数据一致性的方法叫做`事物` > 那么如何使用`sqlalchemy`来进行事物的操作? 其实`sqlalchemy`就是天然支持这种事物的,其实我们这种写法已经用到事物,为什么呢?因为在`db.session.commit(gift)`前面的所有操作,都是真正的提交到数据库里去,一直到调用`db.session.commit(gift)`才提交的。 道理是这个道理,但是上述代码还是有问题的,那就是没有执行数据库的`rollback`回滚操作 ~~~ try: gift = Gift() gift.isbn = isbn gift.uid = current_user.id current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK'] db.session.add(gift) db.session.commit(gift) # 将数据写入到数据库 except Exception as e: db.session.rollback() raise e ~~~ > 为什么一定要执行`db.session.rollback()`? 如果说执行`db.session.commit(gift)`的时候,出现了错误,而我们有没有进行回滚操作,不仅仅这次的插入操作失败了,还有后续的所有插入操作都会失败。 建议:以后只要进行`db.session.commit()`操作都要用`try except`将其包裹起来 ~~~ try: ... db.session.add(gift) db.session.commit(gift) except Exception as e: db.session.rollback() raise e ~~~ #### 7\. `python @conetentmanager` 思考问题:对于`db.session.commit()`我们可能在整个项目的很多地方都需要使用到,每次写`db.session.commit()`的时候都要重复写这样一串代码,那么有没有什么方法可以避免写这样重复的代码呢? * 认识`python @conetentmanager` `@conetentmanager`给了我们一个机会,让我们可以把原来不是上下文管理器的类转化为上下文管理器。 假如`MyResource`是`flask`提供给我们的或者是其他第三方类库提供给我们的话,我们去修改别人的源码,在源码里添加`__enter__`方法和`__exit__`方法这样合适吗?显然不合适。但是我们可以在`MyResource`的外部把`MyResource`包装成上下文管理器, ~~~ class MyResource: # def __enter__(self): # print('connect to resource') # return self # # def __exit__(self, exc_type, exc_val, exc_tb): # print('close resource connection') def query(self): print('query data') # with MyResource as r: # r.query() from contextlib import contextmanager @contextmanager def Make_Resource(): print('connect to resource') yield MyResource() print('close resource connection') with Make_Resource() as r: r.query() ---------------------------------------------------------------- 运行结果: connect to resource query data close resource connection ~~~ > 带有`yield`关键字的函数叫做`生成器` `yield`关键字可以让函数在处理到`yield`返回`MyResource`之后处于`中断`的状态,然后让程序在外面执行完`r.query()`之后再次回到`yield`这里执行后面的代码。 我们整体来看下,这里使用`@conetentmanager`之后与正常使用`with`语句的代码到底是减少了还是增多了? 很多教程里都说`@conetentmanager`这个内置的装饰器可以简化上下文管理器的定义,但是我不这么认为,我认为这种做法是完全不正确的。本身的`@conetentmanager`装饰器在理解上就是比较抽象的,其实还不如`__enter__`和`__exit__`方法来的直接。 * 灵活运用`@conetentmanager` 需求场景:我现在需要打印一本书的名字《且将生活一饮而尽》,书名前后需要使用书名号《》将书名括起来。这个书名是我从数据库里查出来的,我们在数据库里保存书名的时候肯定不会在书名前后加上书名号,我现在取出来了,我想让它在显示的时候加上书名号,怎么办呢?有没有什么办法可以自动把书名号加上呢? 答:可以使用`@conetentmanager`内置装饰器轻松的在书名的前后加上书名号 ~~~ from contextlib import contextmanager print('《且将生活一饮而尽》') @contextmanager def book_mark(): print('《', end='') yield print('》', end='') with book_mark(): print('且将生活一饮而尽', end='') ----------------------------------------------- 运行结果: 《且将生活一饮而尽》 《且将生活一饮而尽》 ~~~ * 使用`@conetentmanager`重构代码 我们最核心的代码是`db.session.commit()`,但是我现在想在`db`下面新增一个方法,然后我们用`with`语句去调用`db`下面我们自己定义的方法,就可以实现自动在前后加上`try`和`except`。 > `db`是`sqlalchemy`第三方插件的,我们如何在第三方类库里面新增加一个方法呢? 很简单,继承`sqlalchemy` > **小技巧**: 有时候我们在给类定义子类的时候,子类的名字非常难取,那么子类的名字难取,那不如我们先更改父类的名字,然后将之前父类的名字给子类(使用`from import`更改父类的名字) ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fw5k1nelguj31b40mqq97.jpg) ~~~ @web.route('/gifts/book/<isbn>') @login_required def save_to_gifts(isbn): if current_user.can_save_to_list(isbn): # try: with db.auto_commit(): gift = Gift() gift.isbn = isbn gift.uid = current_user.id current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK'] db.session.add(gift) # db.session.commit(gift) # 将数据写入到数据库 # except Exception as e: # db.session.rollback() # raise e else: flash('这本书已添加至你的赠送清单或已存在与你的心愿清单,请不要重复添加') ~~~ #### 8\. 为`create_time`赋值 我们发现`gift`数据库里有一个`create_time`字段,但是并没有值,为什么没有值呢? 我们回想一个,数据库里有字段,那肯定是在我们定义模型的时候定义了该字段。`create_time`是在我们模型的基类`Base`里定义的。 `create_time`表示用户当前行为的发生时间,该怎么给`create_time`赋值呢? `create_time`表示当前模型生成和保存的时间。用户发生行为,用户是模型的实例化,所以肯定是在用户模型的实例属性里给`create_time`赋值,调用实例属性的时候就是用户发生行为的时候,所以我们可以在`Base`里定义`__init__`初始化函数,来赋值。 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k1ckcmwj319s0fijva.jpg) > 导入`datetime`: `from datetime import datetime` `datetime.now()`表示获取当前时间 `timestamp()`表示转化为时间戳的格式 > **注意**: 类的**类变量**与类的**实例变量**的的区别: 类的类变量是发生在类的定义的过程中,并不是发生在对象实例化的过程中; 类的实例变量是发生在对象实例化的过程中。 区别示例: 如果我们在上述定义`create_time`的时候将其定义在`create_time = Column('create_time', Integer, default=int(datetime.now().timestamp()))`,那么将会导致数据库里所有的`create_time`都是同一个时间(创建基类`Base`的时间),很显然这种做法是错误的。 #### 9.`ajax`技术 对于这种:原来在什么页面,由于我们要提交某些信息,最后又要回到这个页面的这种操作,很多情情况下我们可以使用`ajax`技术。 ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k1bfs1mj31cs0kc778.jpg) > `ajax`是前段的技术,做网站归最网站,但是也要善于使用`ajax`技术来改善我们服务器的性能。 在上述过程不使用`ajax`技术的时候,最消耗服务器性能的是`book_detail`,`book_detail`又要把详情页面模板渲染再返回,这个模板渲染是最消耗服务器性能的。 **解决办法**:把整个页面当做静态文件缓存起来,也是很多网站经常用到的一种技术。缓存起来之后,直接把页面从缓存读取出来再返回回去就行了。 #### 10\. 将数据库里的时间戳转换为正常的年月日 ~~~ 数据库时间戳python时间对象正常时间 ~~~ `time=single.create_time.strftime('%Y-%m-%d')` `create_time`是一个整数,整数下面是没有`strftime()`方法的,只有`python`的时间类型才有`strftime()`方法,所以需要先把`create_time`转化为`python`的时间类型对象。 因为`create_time`是所有模型都有的属性,所以建议在模型的基类里进行转换。使用`fromtimestamp()`函数转换。 ~~~ @property def create_datetime(self): if self.create_time: return datetime.fromtimestamp(self.create_time) else: return None ~~~ ~~~ def __map_to_trade(self, single): if single.create_datetime: time = single.create_datetime.strftime('%Y-%m-%d') else: time = '未知' return dict( user_name=single.user.nickname, time=time, id=single(id) ) ~~~ #### 11\. 再次提到`MVC` * `MVC`: `M`:模型层,对应`Models`,例如:`book`模型、`user`模型、`gift`模型、`wish`模型 `V`:视图层,对应`Template`模板 `C`:控制层,对应视图函数 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw5kewz560j31d80lm41i.jpg) * `Django`里的`MVT`: `M`:模型层 `V`:控制层 `T`:视图层 > **经典面试问题**: 业务逻辑应该写在`MVC`的哪一层里? 答:业务逻辑应该写在`M`模型层里 很多同学会回答业务逻辑应该写在`C`控制层里面,这个答案是错误的。 那是因为他们没有搞清楚模型层和`数据层`的区别,在早期的时候,数据层的概念确实是存在的,它的全称应该叫做`数据持久化层`。 `数据持久化层`它的主要作用是什么? 以前我们没有`ORM`,当我们的模型层要使用数据的时候,它是有可能需要使用到不同的数据库的,比如有些数据是储存在`Oracle`、有些数据是储存在`MySQL`里的、还有些数据是储存在`MongoDB`里的,由于这些数据的数据源不通,有时候它们的一些具体的`SQL`的操作方法也不同,但是为了让我们模型使用更加舒服,要保持一个不同数据库的统一调用接口,我们需要用数据层来做封装。但是我们`ORM`就不需要关注底层接口的,`ORM`已经帮我们做好了封装。比如我们的`SQLAlchemy`,它自身就可以完成对接不同的数据库。 ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fw5kff6cuyj31fe0newhx.jpg) > **小知识**: `Model`模型层是可以分层的,在**复杂的业务逻辑**里我们可以在`Model`模型层里进一步的细分为`Model`、`Logic`、`Service` ![](https://ws3.sinaimg.cn/large/006tNbRwgy1fw5k1efbwzj31kw0mb0x2.jpg) #### 12\. 复写`filter_by` * **问题背景**: 我们之前所有的查询语句里都是有个严重的错误的,那么这个错误在什么地方呢? 我们项目里采用的数据删除方式不是物理删除,而是软删除,而软删除是通过一个状态标示位`status`来表示这条数据是否已被删除,如果我们在查询的时候不加上这个关键条件`status=1`的话,那么我们查询出来的结果会包括已经被我们删掉的数据 * **解决方法**: 我们确实可以在每个`filter_by`里面加上`status=1`的搜索条件,但是这样会很繁琐。换种思维方式,既然我要做数据库的查询,那么我的目的就是要查询没有被删除的数据。我们所有的查询条件都是传入到`filter_by`这个查询函数里的,那么我们其实是可以考虑改写`filter_by`这个函数内部的相关代码从而让我们的查询条件可以自动覆盖`status=1`。 问题是这个`filter_by`函数不是我们自己定义的,它是第三方库`SQLAlchemy`提供的,最容易想到的方案就是继承相关的类,然后用自己的实现方法去覆盖这个`filter_by`函数。 如果我们要去覆盖类库里面的相关对象的话,一定要搞清楚这个类库对象的继承关系。 ![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw5k1dfks4j31iq0wm7de.jpg) > **小知识**: > > * 上图中的`**kwargs`实际上就是一组查询条件:`isbn=isbn, launched=False, uid=current_user.id`类似这种,我们只需要在这组查询条件里添加上`status=1`是不是就可以了? > > * 那么`**kwargs`到底是什么呢? 这就比较考验同学的`python`基础,我们在`python`基础教程里面已经说过了`**kwargs`是一个字典,既然是字典那就好处理了。我们直接使用字典添加键值对的方式把`status=1`添加上就行了,`kwargs['status'] = 1`。 但是我们这里最好判断一下,万一别人在使用的时候传入了`status=1`,我们就没有必要赋值了。就是使用判断字典中是否存在该键的普通方法。 > ~~~ if 'status' not in kwargs.keys(): kwargs['status'] = 1 ~~~ > * 以上只是实现了自己的逻辑,我们还需要完成原有的`filter_by`的逻辑。 调用基类下面`filter_by`方法就可以了`super(Query, self).filter_by(**kwargs)` > > * **注意:原有`filter_by`传入的参数 \*\*kwargs 是有双星号的,这里的双星号也不能少,否则会出现错误。在传入一个字典的时候必须对这个字典进行解包,使用双星号解包。** 最后因为原有`filter_by`函数有`return`语句,所以我们也需要添加是`return`。 > > * 自定义的`Query`还没有替换`BaseQuery`,那么怎么使用`Query`替换原有的`BaseQuery`呢? 查看源码得知:`flask_sqlalchemy`里的`SQLAlchemy`的构造函数里是允许我们传入一个我们自己的`BaseQuery`的。 所以我们只需要在实例化的时候传入我们自定义的`Query`就可以了。 > ~~~ db = SQLAlchemy(query_class=Query) ~~~ #### 13\. 复杂`SQL`语句的编写方案 问题背景: 前面我们已经完成了搜索结果页面和图书详情页面,下面我们来完成最近上传页面。最近上传也是我们网站的首页,最近上传页面的规则如下图: ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fw5kge3vh1j31kw09fwfy.jpg)