# 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`路由的基本原理。
### 循环引用图解

### 1\. **拆分视图函数**到单独的模块中去
将视图函数从主执行文件分离出来时,不能直接导入flask的核心对象,导致不能使用flask核心对象来注册视图函数的路由
### 2\. 只有由**视图函数**或者`http`请求触发的`request`才能得到`get`返回的结果数据
*   `flask`默认`request`类型是`localproxy`(本地代理)
    

### 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`没有这个概念。  
### 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`是栈结构
        
#### **封装**
*   **软件世界里的一切都是由封装来构建的,没有什么是封装解决不了的问题!**
    
*   **如果一次封装解决不了问题,那么就再来一次!**
    
*   **编程也是一种艺术,代码风格要含蓄!**
    

#### `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()`了。
>     
*   **使用线程隔离的意义**:`使当前线程能够正确引用到他自己所创建的对象,而不是引用到其他线程所创建的对象`
    
  
### 11\. `flask`开启多线程的方法
在入口文件将`threaded=True`开启  
### 12\. `ViewModel`层的作用

*   页面所需数据结构与**原始数据结构**是一一对应的时候:
    
    *   原始数据可以直接传给页面
        
*   页面所需数据结构与**原始数据结构**不一致:
    
    *   需要`ViewModel`对原始数据结构进行`裁剪`、`修饰`、`合并`
        
因为原始数据并不一定能满足客户端显示的要求,`ViewModel`给了调整数据的机会,不同的页面对数据差异化的要求,可以在`ViewModel`里进行集中处理。
> 将`author`列表转换成字符串再传给客户端:  
> 
>   **返回`author`列表的灵活性要比返回字符串高**,返回列表,客户端可以根据需求使用不同的符号将作者分割链接,但是字符串就限制了客户端的选择。  对于返回客户端`author`数据的格式,`web`编程经验的个人建议:
> 
> *   如果我们正在做的是单页面前后端分离的应用程序,建议`author`保持列表的形式直接返回到客户端去,让客户端使用`JavaScript`来操作或者解析列表。
>     
> *   如果是在做网站的话,建议`author`在`ViewModel`里处理。
>     
> 
> 原因:`JavaScript`处理这些事情非常方便,但如果是模板渲染的方式来渲染`html`的话,我们先把数据处理好,直接往模板里填充会是更好的选择!  **数据在哪里处理,可以根据实际情况而定!**
### 13\. 面向对象
#### `面向对象`的类
*   描述自己的特征(数据)
    
    *   使用类变量、实例变量来描述自己的特征
        
*   行为
    
    *   用方法来定义类的行为
        
> 对面向对象理解不够深刻的同学经常写出只有行为没有特征的类,他们所写的类里大量的都是方法而没有类变量、实例变量,这种类的本质还是`面向过程`,因为面向过程的思维方式是人类最为熟悉的一种思维方式,所以比较容易写出面向过程的类。  `面向过程`的基本单位是函数,`面向对象`的基本单位是类  虽然使用了`class`关键字并且将一些方法或者函数封装到`class`内部,但是并没有改变这种面向过程的实质。  `面向对象`是一种思维方式,并不在于你的代码是怎么写的,如果说你的思维方式出现错误,那你肯定是写不出面向对象的代码的。
*   如何去审视自己的类?去判断我们写出来的类到底是不是一个`伪面向对象`?
    
    *   如果一个类有大量的**可以被标注为**`classmethod`或者`staticmethod` 的静态方法,那么你的类封装的是不好的,并没有充分利用面向对象的特性。
        
### 14\. 代码解释权的反转
*   代码的解释权不再由函数的编写方所定义的,而是把解释的权利交给了函数的调用方  
    
~~~
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`的结果就行了


### 15\. 单页面和网站的区别
> 经典面试问题:  单页面和普通网站的区别?
> 
> *   `单页面`:
>     
>     1.  并不一定只有一个页面;
>         
>     2.  最大的特点在于数据的渲染是在客户端进行的;
>         
>     3.  单页面应用程的业务逻辑,也就是说数据的运算主要还是集中在客户端,用`JS`去操作的。
>         
> *   `多页面普通网站`:大多数情况下数据的渲染或者模板的填充是在服务端进行的。
>     
#### **普通网站**
  
#### **单页面**
*   单页面中`html`也是静态资源
    
  
### 16\. `flask`静态文件访问原理
*   在实例化`flask`对象的时候,可以指定`static_folder`(静态文件目录)、`static_url_path`(静态文件访问路由)
    
> 
> 
> 
> 
> 
> 
>   `_static_folder`和`_static_url_path`默认值为`None`
*   `blueprint`(蓝图)的静态资源操作与`flask`核心对象操作一样  
    
### 17\. 模板文件的位置与修改方案
`templates`文件位置可以由`template_folder`指定模板文件路径
*   需求:将字典填充到`html`里,再将填充之后的`html`返回到客户端去
    
`flask`为了让我们能够在模板里面很好的解析和展示数据,它引入了一个模板引擎`Jinja2`
> 像`Jinja`这种可以帮助我们在`html`中渲染和填充数据的语言通常被称为`模板语言`

在`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 %}
~~~
#### 模板继承的用法

1.  写好一级`html`页面:`layout.html` ,包含`block`模块。
    
2.  再需要继承的`html`页面顶端导入基础`html`:`{% extends ‘layout.html' %}`。
    
3.  使用`{{ super() }}`关键字可以继承一级`html`页面中的`block`模块的内容,不使用的话只能替换,无法继承。
    
#### 模板语言的过滤器
过滤器的基本用法是在关键是后面加上竖线 `|`
*   `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 %}
~~~
页面最终的展示效果:  
> 因为搜索结果页面展示的时候,需要展示`作者`、`出版社`、`价格`并将这三项数据使用`/`连接,但是获取的数据并不是都有这三项数据,或者原始数据包含该项数据,但是该项数据为`空`,那么就会造成`曹雪芹//23.00元`这种情况,所以为了解决这个问题,引入了`intro`函数,我们在`intro`函数里判断三项原始数据是否存在且是否为空,若存在且不为空则返回数据,如果存在且为空则返回`False`,如果不存在则返回`False`,过滤完成后得到一个`intros`列表,最后使用`return`语句将`intros`列表使用`/`连接起来再返回。
#### `@property`装饰器
使用`@property`是让`intro`函数可以作为属性的方式访问,就是将类的方法转换为属性
*   模板语言里 intro 作为`函数`的形式访问:`{{ book.intro() }}`
    
*   模板语言里 intro 作为`属性`的形式访问:`{{ book.intro }}`
    
> 前文示例中的`intro`是数据,应该用属性访问的方式来获取数据,而不应该用行为的方式来表达数据
对象的两个特性:
*   数据是用来描述它的特征的
    
*   方法(或者函数)是用来描述它的行为的
    

### 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\. 用户注册

*   `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`模型里覆写获取属性的相关方法。  
>     
#### 访问权限控制
网站的视图函数大致分为两类:
*   需要用户登录才能访问
    
*   不需要登录即可访问
    
> 如果需要限制一个视图函数需要登录才能访问,怎么办?  如果仅仅是将用户信息写入到`cookie`中,我们完全不需要引入第三方插件,但是如果考虑到要对某些视图函数做权限的控制的话,第三方插件就非常有用了。  对于一个用户管理插件而言,最复杂的实现的地方就在于对权限的控制
使用第三方插件的装饰器来进行登录权限的控制:
1.  在视图函数前加上装饰器  
    
#### 重定向攻击
1.  当用户访问一个需要登录的视图函数的时候,会自动跳转到登录页面(生成附加`next`信息的登录页面`url`)  
    
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://`开头  
    
> `next.startswith()`函数可以判断`next`信息是否以`/`开头
### 23\. `wish`模型
#### 1\. 分析得到`wish`模型几乎和`gift`一模一样,所以直接复制过来稍微改一下就行了
#### 2\. 点击`赠送此书`和`加入到心愿清单`要求用户登录,在这两个视图函数前加上`@login_required`
~~~
- 赠送此书:`save_to_gift`
- 加入到心愿清单:`save_to_wish`
~~~

#### 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\. 鱼豆
  为了后期方便修改上传一本书所获得的鱼豆数量,所以将上传一本书所获得的鱼豆数量设定为变量写到配置文件里,再在需要的时候读取配置文件。
~~~
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\. 事物
事物是数据库里的概念,但是放到模型的方式里,它也是存在的。  
这里是操作了两张数据表:
*   `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`更改父类的名字)  
~~~
@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__`初始化函数,来赋值。  
> 导入`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`技术。  
> `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`:控制层,对应视图函数  
    
*   `Django`里的`MVT`:  `M`:模型层  `V`:控制层  `T`:视图层
    
> **经典面试问题**:  业务逻辑应该写在`MVC`的哪一层里?  答:业务逻辑应该写在`M`模型层里  很多同学会回答业务逻辑应该写在`C`控制层里面,这个答案是错误的。  那是因为他们没有搞清楚模型层和`数据层`的区别,在早期的时候,数据层的概念确实是存在的,它的全称应该叫做`数据持久化层`。  `数据持久化层`它的主要作用是什么?  以前我们没有`ORM`,当我们的模型层要使用数据的时候,它是有可能需要使用到不同的数据库的,比如有些数据是储存在`Oracle`、有些数据是储存在`MySQL`里的、还有些数据是储存在`MongoDB`里的,由于这些数据的数据源不通,有时候它们的一些具体的`SQL`的操作方法也不同,但是为了让我们模型使用更加舒服,要保持一个不同数据库的统一调用接口,我们需要用数据层来做封装。但是我们`ORM`就不需要关注底层接口的,`ORM`已经帮我们做好了封装。比如我们的`SQLAlchemy`,它自身就可以完成对接不同的数据库。  
> **小知识**:  `Model`模型层是可以分层的,在**复杂的业务逻辑**里我们可以在`Model`模型层里进一步的细分为`Model`、`Logic`、`Service`  
#### 12\. 复写`filter_by`
*   **问题背景**:  我们之前所有的查询语句里都是有个严重的错误的,那么这个错误在什么地方呢?  我们项目里采用的数据删除方式不是物理删除,而是软删除,而软删除是通过一个状态标示位`status`来表示这条数据是否已被删除,如果我们在查询的时候不加上这个关键条件`status=1`的话,那么我们查询出来的结果会包括已经被我们删掉的数据
    
*   **解决方法**:  我们确实可以在每个`filter_by`里面加上`status=1`的搜索条件,但是这样会很繁琐。换种思维方式,既然我要做数据库的查询,那么我的目的就是要查询没有被删除的数据。我们所有的查询条件都是传入到`filter_by`这个查询函数里的,那么我们其实是可以考虑改写`filter_by`这个函数内部的相关代码从而让我们的查询条件可以自动覆盖`status=1`。  问题是这个`filter_by`函数不是我们自己定义的,它是第三方库`SQLAlchemy`提供的,最容易想到的方案就是继承相关的类,然后用自己的实现方法去覆盖这个`filter_by`函数。  如果我们要去覆盖类库里面的相关对象的话,一定要搞清楚这个类库对象的继承关系。
    

> **小知识**:
> 
> *   上图中的`**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`语句的编写方案
问题背景:  前面我们已经完成了搜索结果页面和图书详情页面,下面我们来完成最近上传页面。最近上传也是我们网站的首页,最近上传页面的规则如下图:  
                    
        
    