# 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 = 145 def __exit__(self, exc_type, exc_val, exc_tb):6 b = 278with A() as obj_A: 9 pass
~~~
* `obj_A` 是 `None`;`A()`直接实例化 `A`,返回的是上下文管理器对象
* as 语句后面的变量不是上下文管理器
* `__enter__` 方法所返回的值会赋给 as 语句后面的变量
~~~
1class A:2 def __enter__(self):3 a = 14 return a56 def __exit__(self, exc_type, exc_val, exc_tb):7 b = 289with 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 self56 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__ 没有返回,则默认返回 False1516 def query(self):17 print('query data')181920try: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 time345class A:6 b = 1789my_obj = A()101112def worker():13 # 新线程14 my_obj.b = 2151617new_t = threading.Thread(target=worker, name='my_test_thread')18new_t.start()19time.sleep(1)202122# 主线程23print(my_obj.b)24# 新线程的修改影响到了主线程的打印结果,因为对象A只是普通对象,不是线程隔离的对象
~~~
#### 线程隔离对象
* **不同线程操作线程隔离对象的情况**
~~~
1import threading2import time34from werkzeug.local import Local567# class A(Local):8# b = 191011my_obj = Local()12my_obj.b = 1131415def worker():16 # 新线程17 my_obj.b = 218 print('in new thread b is:' + str(my_obj.b))192021new_t = threading.Thread(target=worker, name='my_test_thread')22new_t.start()23time.sleep(1)242526# 主线程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 LocalStack234s = LocalStack()5s.push(1)67print(s.top)8print(s.top)9print(s.pop())10print(s.top)111213s.push(1)14s.push(2)1516print(s.top)17print(s.top)18print(s.pop())19print(s.top)20----------------------------------------------21执行结果:22123124125None262272282291
~~~
##### 作为线程隔离对象
> 两个线程拥有两个栈,是相互隔离的,互不干扰
~~~
1import threading2import time34from werkzeug.local import LocalStack567my_stack = LocalStack() # 实例化具有线程隔离属性的LocalStack对象8my_stack.push(1)9print('in main thread after push, value is:' + str(my_stack.top))101112def 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))181920new_t = threading.Thread(target=worker, name='my_new_thread')21new_t.start()22time.sleep(1)2324# 主线程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是一个对象,对象时无法进行序列化的34return jsonify(books.__dict__)5# 将books取字典之后同样会报错,因为books里面包含对象,所包含的对象无法进行序列化67return 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>78 {% endif %}
~~~
* `for in`语句(循环控制语句)
~~~
1 {% for foo in [1,2,3,4,5] %}2 {{ foo }}3 <div>999</div>45 {% endfor %}67 {% for key, value in data.items() %}8 {# 注意for后面跟的是键值对,用逗号分开。#}9 {# data后面要加上iterms(),要确保是个可迭代对象,否则会报错 #}10 {{ key }}11 {{ value }}1213 {% 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 不存在#}23{{ data.school | default=('未名') }}
~~~
* `|` 有点像`linux`里的管道符号,其表达的意思是`值得传递`
~~~
1{# data.name = None, data.age 存在,data.school 不存在 #}23{{ 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)