<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 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)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看