<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>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                [TOC] 我們現在手上有一個設計,并準備把這個設計變成一個可用的程序!當然,通常這并不容易做到。我們將在整本書中看到一些例子和關于良好軟件設計的提示,但我們的重點是面向對象編程。那么,讓我們來看一下python語法,它允許我們創建面向對象軟件。 </b> 完成本章后,我們將理解: * 如何在Python中創建類和實例化對象 * 如何向Python對象添加屬性和行為 * 如何將類組織成包和模塊 * 如何建議人們不要破壞我們的數據 ## 創建python類 我們不必編寫太多的python代碼,就可以感受到python是一個非常“干凈”的語言。當我們想做某件事的時候,我們就做,而不必通過很多設置去做。正如你所看到的,在python中無處不在的“hello world”,通常只有一行。 </b> python 3中最簡單的類如下所示: ``` class MyFirstClass: pass ``` 這是我們的第一個面向對象程序!類定義從關鍵字`class`開始。后面跟著一個(我們選擇的)名稱來標識類,并且以冒號結尾。 > 類名必須遵循標準的python變量命名規則(它必須以字母或下劃線開頭,并且只能由字母、下劃線或數字)。此外,python樣式指南(在網上搜索“PEP 8”)建議將類命名為使用駝峰形式(以大寫字母開頭;任何后續字母單詞也應該以大寫字母開頭)。 在類定義行后是縮進的類內容。和其他python結構一樣,縮進用于分隔類,而不是像其他語言一樣使用大括號或括號。一般使用四個空格作為縮進,除非有一個令人信服的理由不這樣做(例如加入別人使用制表符進行縮進的代碼)。任何像樣的編程編輯器都可以進行二次配置,每次按Tab鍵時可以插入四個空格。 </b> 因為我們第一個類實際上不做任何事情,所以我們在第二行使用pass關鍵字,表示無需采取進一步行動。 </b> 我們可能認為用這個最基本的類我們做不了什么,但實際上我們可以實例化該類的對象。我們可以將這個類加載到python 3解釋器中,這樣我們就可以交互地使用它。為此,請在前面提到的類定義保存到名為`first_class.py`的文件中,然后運行命令`python -i first_class.py`。-i參數告訴python“運行代碼然后轉到交互式解釋器。以下解釋器會話演示了與此類的基本交互過程: ``` >>> a = MyFirstClass() >>> b = MyFirstClass() >>> print(a) <__main__.MyFirstClass object at 0xb7b7faec> >>> print(b) <__main__.MyFirstClass object at 0xb7b7fbac> >>> ``` 這段代碼實例化了新類的兩個對象,a和b。創建類的實例是一件簡單的事情,即鍵入類名后,再添加一對括號。它看起來很像一個普通的函數調用,但python知道我們正在“調用”一個類而不是一個函數,因此它理解它的工作是創建一個新的對象。打印后,這兩個對象告訴我們它們是哪個類以及它們在內存中的地址。在python代碼中,內存地址的使用并不多,但是這里,它們表明有兩個不同的對象。 > **下載示例代碼** 如果你已從 http://www.packtpub.com 中購買了這本書,你可以下載這本書的示例代碼文件。如果你在別處購買了這本書,你可以訪問 http://www. packtpub.com/support 并注冊,出版社會通過電子郵件發送文件給你。 ### 添加屬性 現在,我們有一個基本類,但它相當無用。它不包含任何數據,而且什么都不能做。我們應該怎樣給對象分配一個屬性呢? </b> 我們仍然可以不用在定義類環節做任何特別的事情。我們可以使用點表示法在已經實例化的對象上設置任意屬性: ``` class Point: pass p1 = Point() p2 = Point() p1.x = 5 p1.y = 4 p2.x = 3 p2.y = 6 print(p1.x, p1.y) print(p2.x, p2.y) ``` 如果我們運行此段代碼,結尾的兩個`print`語句將告訴我們兩個對象上的新屬性值: ``` 5 4 3 6 ``` 此代碼創建一個沒有數據或行為的空的`Point`類。然后它創建了該類的兩個實例,并分配給每個實例x和y坐標去識別一個二維空間中的點。我們要做的是,使用`<object>.<attribute>=<value>`語法,為對象的屬性進行賦值。這有時被稱為**點號表示**。值可以是任何東西:python主類型、內置數據類型,或者其他對象。它甚至可以是一個函數或另一個類! ### 讓它做點什么 到目前為止,我們已經擁有具有屬性的對象,這看起來很不錯,但面向對象編程的核心是有關對象之間的相互作用的。我們感興趣的是,調用方法(行為),使得對象屬性發生變化。是時候向我們的類中添加行為了。 </b> 讓我們在`Point`類上構建幾個方法。我們先構建一個重置`reset`方法,將點移動到原點(原點是x和Y都是零)。這是一個很好的入門方法,因為它不需要任何參數: ``` class Point: def reset(self): self.x = 0 self.y = 0 p = Point() p.reset() print(p.x, p.y) ``` 這個`print`語句向我們展示了兩個屬性都為零: `0 0` python中的方法的格式與函數的格式相同。它以關鍵字`def`開頭,后跟空格和方法名。接下來是一組包含參數列表的括號(我們隨后再討論`self`參數的意義),以冒號結束。下一行縮進,以包含方法內部的語句。這些語句可以是任意的python代碼,包括對對象本身的操作,以及對方法所看到的傳入的任何參數進行操作。 #### 捫心自問 方法和正常函數的一個區別是所有方法都有一個必需的參數。這個參數按慣例被稱為`self`,我從未見過程序員對這個變量使用任何其他名稱(約定是非常強大的事情)。但是,沒有什么能阻止你稱它為`this`,甚至是`Martha`。 </b> 方法的`self`變量僅僅意味著該方法所指向的對象。我們可以訪問對象的屬性或方法,就好像這個對象是其他任何對象。這正是我們在重置`reset`方法中所做的,我們設置了`self`對象的x和y屬性。 </b> 注意,當我們調用`p.reset()`方法時,不需要傳遞`self`參數。python會自動為我們處理這個問題。它知道我們在調用p對象上的一個方法,因此它自動將該對象傳遞給該方法。 </b> 然而,方法實際上只是一個恰好位于類上的函數。除了對對象調用方法外,我們還可以調用類上的函數,將我們的對象作為`self`參數顯式傳遞: ``` p = Point() Point.reset(p) print(p.x, p.y) ``` 輸出結果與前一個示例相同,因為內部發生了同樣的過程。 </b> 如果我們忘記把`self`包括在我們的類定義中,那會發生什么呢?python將返回一條錯誤消息: ``` >>> class Point: ... def reset(): ... pass ... >>> p = Point() >>> p.reset() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: reset() takes no arguments (1 given) ``` 錯誤信息并不像我們希望的那樣清晰(例如,“你這個愚蠢的傻瓜,你忘了`self`變量”,將更清楚一些)。記住,當你看到顯示缺少參數的錯誤消息,首先要檢查的是,看看方法定義中是不是忘記了`self`。 #### 更多參數 那么,我們如何將多個參數傳遞給一個方法呢?讓我們添加一個新方法,允許我們將一個點移動到任意位置,而不僅僅是移動到原點。我們可以還包括一個點接受另一個點對象作為輸入并返回它們之間的距離: ``` import math class Point: def move(self, x, y): self.x = x self.y = y def reset(self): self.move(0, 0) def calculate_distance(self, other_point): return math.sqrt((self.x - other_point.x)**2 + (self.y - other_point.y)**2) # 如何使用: point1 = Point() point2 = Point() point1.reset() point2.move(5,0) print(point2.calculate_distance(point1)) assert (point2.calculate_distance(point1) == point1.calculate_distance(point2)) point1.move(3,4) print(point1.calculate_distance(point2)) print(point1.calculate_distance(point1)) ``` 最后的print語句給出了以下輸出: ``` 5.0 4.472135955 0.0 ``` 這里發生了很多事。`Point`類現在有三個方法。`move`方法接受x和y這兩個參數,并用于設置`self`對象的值,就像上一個示例中的重置`reset`方法。舊的重置`reset`方法現在調用`move`方法,因為重置`reset`只是移動到特定的已知位置。 </b> `calculate_distance`方法使用不太復雜的勾股定理計算兩點之間的距離。我希望你理解數學(**表示平方,math.sqrt計算平方根),但這不是必須要掌握的,我們目前的焦點是學習如何寫方法。 </b> 前面示例末尾顯示了如何調用帶參數的方法:只需在括號內包含參數,并使用使用相同的點標記來訪問該方法。我只是隨便選了幾個位置來測試方法。測試代碼調用每個方法并在控制臺上打印結果。斷言函數是一個簡單的測試工具;如果斷言為假(或零、空或無),程序將終止。在這種情況下,我們使用它來確保用`calculate_distance`方法計算A點到B點或B點到A點之間的距離,都是相同的。 ### 初始化對象 如果我們沒有在`Point`對象上顯式設置x和y坐標,無論是使用`move`或直接訪問它們,我們將獲得一個沒有實際位置的`broken`點。當我們試圖訪問它時會發生什么? </b> 好吧,我們試試看。“試試看”對于學習Python來說是一個非常有用的工具。打開交互式解釋器并鍵入程序。以下互動會話顯示如果我們嘗試訪問缺失的屬性時會發生什么。如果你將上一個示例保存作為一個文件,或者正在使用隨書分發的示例,可以使用命令`python -i filename.py`將其加載到python解釋器中: ``` >>> point = Point() >>> point.x = 5 >>> print(point.x) 5 >>> print(point.y) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Point' object has no attribute 'y' ``` 好吧,至少它拋出了一個有用的異常信息。我們將在第4章“異常處理”里詳細介紹異常。你可能以前見過它們(尤其是無處不在的**SyntaxError**,意味著你鍵入的內容不正確!)。目前為止,我們只要簡單地記住這意味著程序出了問題。 </b> 輸出對于調試很有用。在交互式解釋器中,它告訴我們**第1行**出錯,該錯誤僅部分為真(在交互式會話中,一次僅執行一行)。如果我們運行一個文件腳本,它會告訴我們準確的行號,便于查找有問題的代碼。此外,它還告訴我們錯誤是一個屬性錯誤`AttributeError`,并提供了一個有用的消息告訴我們錯誤意味著什么。 </b> 我們可以捕獲這個錯誤,并從中恢復。但在這個例子中,似乎我們應該指定某種默認值。也許每個新對象都應該默認執行`reset()`方法,或者要求用戶在創建對象時告訴我們對象的位置應該在哪里。 </b> 大多數面向對象編程語言都有**構造函數**的概念,一種創建并初始化對象的特殊方法。python有點不同;它有一個構造函數*和*一個初始化函數。一般很少使用構造函數,除非你在做異國情調的事情。所以,我們討論初始化方法。 </b> python初始化方法與其他方法相同,只是它有一個特殊名稱,`__init__`。前綴和尾隨雙下劃線表示一種特殊的方法,Python解釋器將其視為特殊情況。 > 永遠不要用前綴和尾隨雙下劃線來命名自己的函數。對python來說可能沒有什么影響,但python的設計者可能在將來添加一個和你的函數同名的,有特殊目的函數。當他們這樣做的時候,你的代碼就崩潰了。 讓我們從`Point`類上的初始化函數開始,該函數要求用戶在實例化`Point`對象時提供x和y坐標: ``` class Point: def __init__(self, x, y): self.move(x, y) def move(self, x, y): self.x = x self.y = y def reset(self): self.move(0, 0) # 創建一個 Point 對象 point = Point(3, 5) print(point.x, point.y) ``` 現在,沒有y坐標,我們的點對象是走不了的!如果我們試圖創建一個不包含正確初始化參數的對象,我們將得到`not enough arguments`的錯誤信息,類似于早先我們忘記添加`self`參數的例子。 </b> 如果我們不想每次一定要輸入這兩個參數呢?好吧,我們可以使用與python函數相同的語法來提供默認參數。這個鍵參數語法在每個變量名后附加一個等號(原文拗口,不如直接說:變量名后附加一個等號和默認變量值)。如果調用對象時不提供參數,則使用默認參數值替代。變量仍然對函數可用,只是它們將具有參數列表中指定的值。下面是一個例子: ``` class Point: def __init__(self, x=0, y=0): self.move(x, y) ``` 大多數時候,我們把初始化語句放在一個`__init__`函數中。但前面提到過,除了初始化函數之外,python還有一個構造函數。你可能永遠不需要使用python構造函數,但了解它會有所幫助。所以我們大致說一下它。 </b> 這個構造函數被稱為`__new__`,與`__init__`差不多,構造函數只接受一個參數,即正在構造的類(因為是在對象之前調用`__new__`函數,所以沒有`self`參數)。它還必須返回新創建的對象。當談到復雜的元編程,或許有些益處,但在日常編程中不是很實用。實踐中,我們很少需要,如果一定要用的話,使用`__new__`和 `__init__`就夠用了。 ### 自我解釋 python是一種非常容易閱讀的編程語言;有些人可能會說它是自動文檔化的。然而,在進行面向對象編程時,編寫API文檔是非常重要的,目的是清楚地描述每個對象和方法。使文檔保持最新是困難的,最好的方法是把文檔寫進我們的代碼中。 </b> python通過使用**文檔字符串**來支持這一點。每個類、函數或方法頭部可以有一個標準的python字符串作為第一行,隨后接著是定義行(以冒號結尾的那一行)。文檔字符串行應該和隨后的代碼有相同的縮進。 </b> 文檔字符串只是用單引號(')或雙引號(")括起來的python字符串。 通常,文檔字符串相當長,跨越多行(樣式指南建議行長度不超過80個字符),這可以多行字符串格式來處理,用匹配的三個單引號(''')或三引號(""")字符括起來。 </b> 文檔字符串應該清楚、簡潔地概括類的目的,或者它所描述的方法。它應該解釋那些用法不是很清晰的參數,而且適時包括一些關于如何使用API的簡短的例子。任何一個毫無戒心的API用戶應該意識到的警告或問題應該被備注。 </b> 為了說明文檔字符串的用法,我們給`Point`類編寫一段完整的文檔,作為本節的結束: ``` import math class Point: '代表一個擁有二維幾何坐標的點' def __init__(self, x=0, y=0): '''初始化一個新點的位置。 x和y坐標應該被提供, 如果不提供,默認位置為原點''' self.move(x, y) def move(self, x, y): "在2D空間,將點移動到一個新位置" self.x = x self.y = y def reset(self): '將點位置重置到原點: 0, 0' self.move(0, 0) def calculate_distance(self, other_point): """計算從這一點到作為參數傳遞的第二點之間的距離。 這個函數使用畢達哥拉斯定理來計算兩點之間的距離。 返回的距離是一個浮點型數值。""" return math.sqrt( (self.x - other_point.x)**2 + (self.y - other_point.y)**2) ``` 嘗試鍵入或加載這個腳本文件到交互式解釋器(記住,命令是`python -i filename.py`)。然后,在python提示下輸入`help(Point)<enter>`。 您應該看到類的格式良好的文檔,如以下屏幕截圖: ![](https://box.kancloud.cn/d306b939c1e51c420bc7c9d76394becf_668x513.png) ## 模塊和包 現在,我們知道了如何創建類和實例化對象,但是我們如何組織它們呢?對于小程序,我們可以把所有的類放到一個文件中,并在文件末尾添加一段讓它們交互的腳本。然而,隨著我們項目的發展,尋找一個需要被很多類進行編輯的類,變得越來越困難。這時候可以借助模塊來幫助我們。模塊仍是一些簡單的python文件。我們小程序中的單個文件是一個模塊。兩個python文件是兩個模塊。如果在同一文件夾中有兩個文件,則可以從一個模塊加載一個類,用在另外一個模塊。 </b> 例如,如果我們正在構建一個電子商務系統,我們可能正在數據庫中存儲大量數據。我們可以將所有與訪問數據庫相關的類和函數放在一個單獨的文件(并給它起個有意義的名字:`database.py`)。然后,我們其他模塊(例如,客戶模型、產品信息和庫存)可以從該模塊導入類以訪問數據庫。 </b> `import`語句用于從模塊導入模塊、特定類或函數。我們已經看到在`Point`類上的例子。我們使用`import`語句來獲取python的內置模塊`math`并用其`sqrt`函數計算距離。下面是一個具體的例子。假設我們有一個名為`database.py`的數據庫模塊,包含一個名為`database`的類,我們還有另一個名為`products.py`的產品模塊,負責與產品相關的查詢。我們暫時不需要思考關于這些模塊的內容。我們只要知道`products.py`需要從`database.py`實例化數據庫類,以便它可以對數據庫中的`product`表執行查詢事務。`import`語句語法有幾種用于訪問類的變體: ``` import database db = database.Database() # 在db上執行查詢 ``` 此版本`import`語句將數據庫模塊導入到產品模塊的命名空間(當前可訪問模塊或函數的名稱列表),因此可以使用`database.<something>`的標記方法訪問數據庫模塊中的類和方法。或者,我們可以使用`from…import`語法,只導入需要的那個類: ``` from database import Database db = Database() # 在db上執行查詢 ``` 如果出于某種原因,產品模塊已經有一個名為`Database`的類,為了不混淆這兩個類,我們可以在產品模塊中重命名這個類: ``` from database import Database as DB db = DB() # 在db上執行查詢 ``` 我們還可以在一個語句中導入多個項。如果我們的數據庫模塊也 包含一個查詢類,我們可以使用以下方法導入這兩個類: ``` from database import Database, Query ``` 有些資料說,我們可以從數據庫模塊中導入所有類和函數: ``` from database import * ``` 但是千萬不要這樣做!每個有經驗的Python程序員都會告訴你不要使用這種語法。這會破壞一些你的編程環境,比如“它把名稱空間搞得一團糟“,這對初學者來說沒什么好處。如果你想了解為什么要避免這種語法,可以使用它并在兩年后嘗試理解代碼(然后你就會有多么痛的領悟了)。當然我們可以現在就解釋,這樣就不用浪費兩年時間了。 </b> 當我們在文件頂部使用`from database import Database`顯式導入`Database`類時,我們可以很容易地看到`Database`類的來源。我們可能稍后在文件中第400行使用`db=Database()`,然后快速查看`from database import Database`行以查看該`Database`類的來源。如果我們需要了解如何使用`Database`類,我們就可以訪問原始文件(或在交互式解釋器中導入`database`模塊,然后使用`help(database.database)`命令)。但如果使用`from database import *`語法,我們如何知道`Database`類是來自`database`模塊呢?這導致代碼維護變成了一場噩夢。 </b> 此外,大多數編輯器都能夠提供額外的功能,例如可靠的代碼補全,跳轉到類的定義或內聯文檔的能力,而`import *`語法通常會完全破壞編輯器的這些能力。 </b> 最后,使用`import *`語法可能將一些不需要的對象導入本地命名空間。當然,它將導入被導入模塊中所有類和函數,但它還將導入被導入模塊自身導入的任意類和函數! </b> 模塊中使用的每個名稱都應該來自一個特定的地方,不管它是不是在該模塊中定義的,或從另一個模塊顯式導入。不應該有仿佛來自稀薄空氣中的神奇變量。我們應該總是能夠立即確定在當前命名空間中的這些變量的來源。我保證如果你使用`import *`這種邪惡的語法,總有一天你會非常沮喪“這個類到底從哪里來的?” ### 組織模塊 隨著項目發展成為包含越來越多模塊的集合,我們可能發現我們想在模塊級別上在抽象出網狀的組織結構。然而,我們不能把模塊放在模塊里面;一個文件只能容納一個文件,模塊只不過是Python文件。 </b> 然而,文件可以放在文件夾中,模塊也可以。一個包就是放在一個文件夾中的一些模塊的集合。包的名稱就是文件夾的名稱。對于一個最簡單的包,我們只需要告訴Python有個文件夾是一個包,請在這個文件夾中放置一個(通常是空的)命名為`__init__.py`文件。如果我們忘記了加這個文件,我們將無法從這個文件夾導入模塊。 </b> 讓我們把我們的模塊放在工作文件夾的`ecommerce`包中,`ecommerce`包還包含一個啟動程序的`main.py`文件。讓我們在`ecommerce`包中再添加一個用于各種支付選項的包。文件夾層次結構將如下所示: ``` parent_directory/ main.py ecommerce/ __init__.py database.py products.py payments/ __init__.py square.py stripe.py ``` 當在包之間導入模塊或類時,我們必須小心語法。在Python 3中,有兩種方式導入模塊:絕對導入和相對導入。 #### 絕對導入 **絕對導入**,如果我們想導入模塊、函數或路徑,我們得給出完整路徑。如果我們需要訪問`products`模塊中的`Product`類,我們可以使用下面語法中的任何一個來進行絕對導入: ``` import ecommerce.products product = ecommerce.products.Product() ``` 或 ``` from ecommerce.products import Product product = Product() ``` 或 ``` from ecommerce import products product = products.Product() ``` `import`語句使用句點運算符來分隔包或模塊。 </b> 這些語句適用于任何模塊。我們可以在`main.py`、`database`模塊或任意一個支付模塊,實例化一個`Product`類。事實上,假設這些包對Python是可用的,我們就能夠導入它們。例如,很多包安裝在Python官網的包文件夾中,我們也可以自定義`PYTHONPATH`環境變量,動態告訴Python它應該從什么文件夾來搜索要導入的包和模塊。 </b> 那么,有了這些選擇,我們選擇哪種語法?這取決于你的個人品味和手頭上的應用程序。如果我想使用的`products`模塊中有幾十個類或函數,我通常使用`from ecommerce import products`語法,然后使用`products.Product`訪問`Product`類。如果我們只是需要`products`模塊的一兩個類,我可以直接使用`from ecommerce.proucts import Product`導入它們。除非存在某種命名沖突(例如,我需要訪問兩個完全不同的都叫`products`的模塊,我需要將它們分開),否則我個人不會經常使用第一種語法。做你想做的,讓你的代碼看起來更優雅。 #### 相對導入 當使用包中的相關模塊時,指定完整路徑似乎有點傻。我們已經知道我們的父模塊叫什么,所以可以使用相對導入。相對導入基本上是一種通過相對當前模塊的位置尋找類、函數或模塊的方法。例如,如果我們當前在`products`模塊中工作,我們希望從它旁邊的`database`模塊導入`Database`類,我們可以使用相對導入: ``` from .database import Database ``` `database`前面的句點表示“使用當前包中的`database`模塊”。在這種情況下,當前包是我們正在編輯的`products.py`文件所在的包(譯注:文件夾),即`ecommerce`包。 </b> 我們正在編輯`ecommerce.payments`包中的`paypal`模塊,如果我們想“使用父包中的`database`模塊”,這很容易通過兩個點號來完成,如下所示: ``` from ..database import Database ``` 我們可以用更多的點號來進一步提升層次。當然,我們也可以從一邊下去,從另一邊后退。我們沒有足夠深刻的層次結構例子來正確地說明這一點,但是下面的導入是有效的。如果我們有一個包含`email`模塊的`ecommerce.contact`包,我們可以把`email`模塊中的`send_mail`函數導入到我們的`paypal`模塊: ``` from ..contact.email import send_mail ``` 其中,兩個點號表示`payments`包的父包,然后使用標準的`package.module`語法升到的`contact`包。 </b> 最后,我們可以直接從包中導入代碼,而不僅僅是里面的模塊。在這個例子中,我們有一個包含兩個模塊(`database`模塊和`products`模塊)的`ecommerce`包,`database`模塊包含一個`db`變量,這個變量可以從很多地方進入。如果能使用`import ecommerce.db `導入豈不比` import ecommerce.database.db`更加方便? </b> 還記著`__init__.py`將目錄定義為包這件事嗎?這個文件可以包含我們喜歡的任何變量或類聲明,它們將作為包的一部分。在我們的示例中,如果`ecommerce/__init__.py`文件包含以下行: ``` from .database import db ``` 那么,我們可以使用下面一行命令從`main.py`或任何其他文件訪問`db`屬性: ``` from ecommerce import db ``` `__init__`可能會有所幫助。這里,它就好像它是`ecommerce.py`文件,這時候它是模塊而不是包。如果你所有的代碼都在單個模塊中,然后你決定將其分解,構建一個模塊包。新包的`__init__.py`文件仍然可以作為與它對話的其他模塊的主要聯系人,而代碼可以在內部被組織成幾個不同的模塊或子包。 </b> 我建議不要將所有代碼放在`__init__.py`文件里。盡管如此。程序員們不期望在這個文件中出現實際的邏輯,這有點兒像`from x import *`,如果他們正在尋找某個特定的代碼,他們可能會遇到麻煩,直到他們檢查`__init__.py`后才能找到它們。 ## 組織模塊內容 在任何一個模塊中,我們都可以定義變量、類或函數。用它們存儲全局狀態是一種方便的方法,且不會出現命名空間沖突。例如,我們一直在將`Database`類導入各種模塊,然后實例化它,然而,從`database`模塊創建一個全局可用的`database`對象可能更有意義。`database`模塊可能如下所示: ``` class Database: # 數據庫的實現 pass database = Database() ``` 然后,我們可以使用我們討論過的任何一種導入方法來訪問這個`database`對象,例如: ``` from ecommerce.database import database ``` 這個剛剛定義的類存在一個問題。當第一次導入模塊時,通常也是程序開始的時候,`database`對象會被立刻被創建。然而這并不總是理想的,因為連接到數據庫可能需要一段時間,減慢啟動速度,或者甚至可能還沒有可用的數據庫連接信息。我們可以延遲創建`database`對象,直到通過調用用于創建模塊級變量的`initialize_database`函數: ``` class Database: # 數據庫的實現 pass database = None def initialize_database(): global database database = Database() ``` `global`全局關鍵字告訴Python我們在`initialize_database`內部定義了一個模塊級的`database`變量。如果我們沒有指定`database`變量是全局變量的話,Python會創建一個新的局部變量,當方法退出時,這個局部變量被丟棄,模塊級變量值保持不變。 </b> 如這兩個示例所示,所有模塊級代碼都在導入時被執行。但是,如果模塊級代碼在方法或函數內部,雖然函數被創建了,但在調用函數之前,它的內部代碼不會被執行。這對于即將執行的腳本來說可能是一件棘手的事情(比如我們電子商務示例中的主腳本)。通常,我們會編寫一個程序(A)來做一些有用的事情,然后發現我們想在另外一個程序B從這個程序A中導入一個函數或類。然而,一旦我們導入它,模塊級的任何代碼都是立即執行。如果我們不小心,我們可能就運行了第一個程序A,而實際上我們只是想訪問程序A內部的幾個函數。 </b> 為了解決這個問題,我們應該總是把我們的啟動代碼放在一個函數中(按照慣例,調用`main`),且只有當我們知道我們正在運行腳本上的模塊時,才真正運行這個函數,現在,當我們的代碼是從不同的腳本導入時,我們該如何知道應該運行哪個腳本呢? ``` class UsefulClass: '''這個類可能對其他模塊有用''' pass def main(): ''' 創建一個有用的類,用它為我們的模塊做一些事情 ''' useful = UsefulClass() print(useful) if __name__ == "__main__": main() ``` 每個模塊都有一個`__name__`特殊變量(記住,Python使用雙下劃線表示特殊變量,例如類的`__init__`方法),指定了模塊導入時的名稱。當使用`python module.py`直接執行模塊時,這個模塊將不被導入,因此`__name__`被設置為字符串`__main__`。用這個策略將你所有的腳本包裹起來,并置于`if __name__ ==" __main__":`的檢測中,總有一天你會發現,當你寫了一個可能會被其它代碼導入的函數時,這將是很有用處的策略。 </b> 所以,方法放在類中,類放在模塊中,模塊放在包中。就這些嗎? </b> 事實上,這只是Python程序中的典型順序,但不是所有可能的布局。類可以在任何地方定義。它們通常定義在模塊級里,但也可以在函數或方法中定義,如下所示: ``` def format_string(string, formatter=None): '''使用格式化對象格式化一個字符串,格式化對象 應具有接受字符串的format()方法。''' class DefaultFormatter: '''將字符串的首字母大寫''' def format(self, string): return str(string).title() if not formatter: formatter = DefaultFormatter() return formatter.format(string) hello_string = "hello world, how are you today?" print(" input: " + hello_string) print("output: " + format_string(hello_string)) ``` 輸出如下: ``` input: hello world, how are you today? output: Hello World, How Are You Today? ``` `format_string`函數接受一個字符串和可選的格式化對象,然后對該字符串進行格式化。如果沒有提供格式化對象,則會創建一個格式化對象。因為它是在函數內部創建的,該類不能被該函數之外的任何地方訪問。同樣,函數也可以在其他函數內部中定義;一般來說,任何Python語句都可以隨時被執行。 </b> 這些內部類和函數,偶爾對那些一次性項目有用,這些內部類或函數不需要或不值得在模塊級別擁有自己范圍,或者它們只在單一方法有意義。然而,這項技術在Python代碼中已經不常見了。 ## 誰可以訪問我的數據 大多數面向對象編程語言都有訪問控制的概念。這與抽象有關。對象上的一些屬性和方法被標記為私有,意味著只有對象本身可以訪問它們。一些被標記為受保護,意味著只有對象所屬的類和任何子類可以訪問。其余的是公開的,意思是允許任何其他對象訪問它們。 </b> python不會這么做。python并不真的相信必須要遵守的強制性規則。相反,它提供了非強制的指導和最佳實踐。從技術上講,類的所有方法和屬性都是公開可用的。如果我們想建議不要公開使用某一種方法,我們可以在`docstrings`上注明,表示該方法僅用于內部(如果可以的話,最好添加API如何工作的解釋!)。 </b> 按照慣例,我們還可以在屬性或方法前加下劃線前綴,`_`。Python程序員會將其解釋為“這是一個內部變量,在直接訪問它之前,請三思而后行”。但在解釋器里面沒有什么可以阻止訪問它們,只要程序員們認為這樣做符合他們的最大利益。況且如果他們想這樣做,我們為什么要阻止他們?我們可能并不知道我們將如何使我們的類。 </b> 還有一件事你可以做,就是強烈建議外部對象不要訪問有雙下劃線`__`前綴的屬性或方法。這將在有問題的屬性上執行命名矯正。這基本上意味著外部對象仍然可以調用這個方法,只要我們想這樣做,但需要一些額外的工作才能實現。而且這將暗示你的屬性將保持私有。例如: ``` class SecretString: '''一種不怎么安全的存儲秘密字符串的方法''' def __init__(self, plain_string, pass_phrase): self.__plain_string = plain_string self.__pass_phrase = pass_phrase def decrypt(self, pass_phrase): '''僅僅當pass_phrase是正確的,才顯示字符串''' if pass_phrase == self.__pass_phrase: return self.__plain_string else: return '' ``` 如果我們加載這個類并在交互式解釋器中測試它,我們可以看到它對外部世界隱藏了純文本字符串: ``` >>> secret_string = SecretString("ACME: Top Secret", "antwerp") >>> print(secret_string.decrypt("antwerp")) ACME: Top Secret >>> print(secret_string.__plain_text) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'SecretString' object has no attribute '__plain_text' ``` 看起來很有效;沒有密碼,沒有人能訪問`plain_text`屬性,所以它是安全的。不過,在我們過于興奮之前,讓我們看看如何很容易地侵入我們的安全系統: ``` >>> print(secret_string._SecretString__plain_string) ACME: Top Secret ``` 哦不!有人黑了我們的秘密字符串。幸好我們檢查過了!這是python的命名矯正在起作用。當我們在屬性前使用雙下劃線時,屬性的前綴將變成`_<classname>'(單下劃線+類名)。類中方法內部訪問這種雙下劃線變量,它們不會被自動矯正。當外部類希望訪問時,它們不得不自己動手進行命名矯正。所以,命名矯正并沒有保證隱私,它只是推薦做法。大多數Python程序員不會在另一個對象上接觸雙下劃線變量,除非它們有非常有說服力的理由這樣做。 </b> 然而,大多數Python程序員在沒有令人信服的理由下,也不會接觸單個下劃線變量。因此,沒有什么好的理由在Python中使用命名矯正后的變量,這樣做只會導致悲傷。例如,命名矯正后的變量可能對子類有用,但我們必須得自己動手矯正。如果其他對象想訪問你的隱藏信息,就讓他們訪問好了,使用單下劃線做前綴,或一些清晰的文檔字符串,只是提醒他們,你并不認為這是個好主意。 ## 第三方工具庫 Python附帶了一個可愛的標準庫,這是許多包和模塊的集合,這個標準庫對于運行Python的每臺機器都是可用的。然而,你很快就會發現它并沒有包含你需要的一切。當這種情況發生時,你有兩個選擇: * 自己寫一個支持包 * 使用別人的代碼 我們不會涉及將包轉化為庫的細節,但是如果你有一個需要解決的問題,你不想編碼(最好的程序員非常懶惰,他們更喜歡重用現有的、經過驗證的代碼,而不是寫他們自己的),你可以在**Python Package Index (PyPI)** [地址](http://pypi.python.org)中找到你想要的庫。一旦你確定了你想要安裝的庫,可以使用名為`pip`的工具來安裝它。然而,`pip`并不是Python自帶的,但是Python 3.4包含一個名為`ensurepip`的有用工具,它將安裝`pip`: ``` python -m ensurepip ``` 在Linux、蘋果操作系統或其他Unix系統上,這可能會失敗,在這種情況下,你需要成為根用戶才能讓它起作用。在大多數現代Unix系統上,這可以用`sudo python -m ensurepip`完成。 > 如果你使用的是比Python 3.4更舊的版本,因為不存在ensurepip,你得自己下載并安裝`pip`。你可以按照[http://pip.readthedocs.org](http://pip.readthedocs.org)上的說明進行操作。 一旦`pip`安裝完畢,并且你知道要安裝的軟件包的名稱,你可以使用以下語法安裝它: ``` pip install requests ``` 但是,如果你這樣做,要么第三方庫被直接安裝在你的系統Python目錄,或者更有可能的是,得到一個不允許這樣做的錯誤。你可以作為管理員強制安裝,但是Python社區的普遍共識是,你應該只使用系統安裝程序將第三方庫安裝到你的系統Python目錄中。 </b> 相反,Python 3.4提供了`venv`工具。這個實用程序在你的工作目錄中,給了你一個基本的、迷你的、被稱為*虛擬環境* 的Python安裝環境。當你激活這個迷你Python,與Python相關的命令將在工作目錄下而不是系統目錄下運行。所以當你運行`pip`或`python`時,將不會碰到系統Python。以下是如何使用它: ``` cd project_directory python -m venv env source env/bin/activate # Linux 或 MacOS env/bin/activate.bat # Windows ``` 通常,你可以為你的每個Python項目創建不同的虛擬環境。你可以把你的虛擬環境存儲在任何地方,但是記住虛擬環境與項目文件應該放在相同的目錄里(這點在版本控制中被忽略),所以,我們先`cd`進入那個目錄。然后我們運行`venv`來創建一個名為env的虛擬環境。最后,我們使用最后兩行中的一行(取決于操作系統,如注釋所示)來激活環境。當我們每次我們想使用這個特定的虛擬環境時,都需要執行這一行。然后,當我們想退出這個項目時,使用`deactivate`命令。 </b> 虛擬環境是一種很好的方法,可以建立不同的庫依賴環境。如果你有很多項目,每個項目依賴不同版本的庫,使用虛擬環境是很普遍的(例如,一個老網站可能在Django 1.5上運行,而較新的版本在Django 1.8上運行)。將每個項目放在不同的虛擬環境中,使得在Django的任何版本中工作都變得容易。此外,它還能防止系統安裝包和`pip`安裝包之間的沖突,如果嘗試使用不同工具安裝同一個包的話。 ## 案例研究 為了將它們聯系在一起,讓我們構建一個簡單的命令行筆記本應用程序。這是一個相當簡單的任務,所以我們不會嘗試多個包。然而,我們會看到類、函數、方法和文檔字符串的常見用法。 </b> 讓我們快速分析一下:筆記是存儲在筆記本中的簡短備忘錄。每個筆記應該記錄它寫的日期,并且可以添加標簽以便于查詢。修改筆記是有可能的。我們還需要能夠搜索筆記的功能。所有這些都應該通過命令行完成。 </b> 很明顯我們需要一個筆記對象`Note`;另外一個不太明顯的是筆記本容器對象`Notebook`。標簽和日期似乎也是對象,但是我們可以使用Python標準庫中的日期,并用逗號分隔字符串作為標簽。為了避免復雜性,在原型中,我們不為這些對象定義單獨的類。 </b> `Note`對象具有備忘`memo`、標簽`tags`和創建日期`creation_date`的屬性。每個`note`對象還需要一個唯一的整數`id`,以便用戶可以在菜單界面中選擇它們。`Note`對象可能還需要方法,一種用于修改筆記內容,另一種方法修改標簽,或者我們可以讓筆記本直接訪問這些屬性。為了使搜索更容易,我們應該在`Note`對象添加匹配方法`match()`。這個方法將接受一個字符串,在不直接訪問屬性的情況下,告訴我們是否有`Note`與字符串匹配。這樣,如果我們希望修改搜索參數(例如,搜索標簽,而不是`Note`內容,或者為了使搜索不區分大小寫),我們只需要在一個地方進行。 </b> 筆記本對象`Notebook`顯然有個筆記列表的屬性。它還需要一個搜索方法,用于返回過濾后的筆記列表。 </b> 但是我們如何與這些對象互動呢?我們已經指定了這是一個命令行應用程序,這意味著我們通過不同的添加或編輯命令運行程序,或者我們有某種菜單,允許我們選擇對筆記本做不同的事情。我們應該嘗試設計它,以便支持任何一個現在或未來的接口,如圖形用戶界面工具包或基于網絡的接口,這些都可以在未來添加。 </b> 作為設計決策,我們先實現菜單接口,但保留命令行選項版本,以確保我們在設計`Notebook`類時仍然具有可擴展性。 </b> 如果我們有兩個命令行接口,每個都與`Notebook`對象交互,那么`Notebook`需要一些方法來與這些接口進行交互。除了我們已經討論過的搜索方法,我們還需要新建筆記的`add`方法、根據`id`修改筆記的`modify`方法。接口還需要能夠列出所有筆記,但是我們可以通過直接訪問`notes`列表屬性來完成。 </b> 我們可能遺漏了一些細節,但我們對我們需要寫的代碼有一個大概的認識。我們可以用一個簡單的類圖來總結這一切: ![](https://box.kancloud.cn/6b4cb8f4fc32424ff7a0f3ca420ad329_476x391.png) 在編寫任何代碼之前,讓我們先定義這個項目的文件夾結構。菜單接口顯然應該在自己的模塊中,因為它將是一個可執行腳本,將來我們可能會有其他可執行腳本訪問筆記本。筆記本對象`Notebook`和筆記對象`notes`可以一起放在同一個模塊中。這些模塊都可以放在同一級目錄中,而不必將它們放入包中。一個空的`command_option.py`提醒我們未來將添加新的用戶界面。 ``` parent_directory/ notebook.py menu.py command_option.py ``` 現在讓我們看看一些代碼。我們從定義`notes`類開始,因為它看起來最簡單。以下示例完整展現了定義`notes`的過程。示例中的文檔字符串解釋這一切是如何結合在一起的。 ``` import datetime # 為所有新的筆記存儲下一個可用的id last_id = 0 class Note: '''代表筆記本中的一個筆記。在搜索中匹配一個字符串,為每個筆記存儲標簽''' def __init__(self, memo, tags=''): '''用memo和可選空格分離標簽初始化一個筆記,自動設置筆記的創建日期和唯一的id''' self.memo = memo self.tags = tags self.creation_date = datetime.date.today() global last_id last_id += 1 self.id = last_id def match(self, filter): '''判定這個筆記是否匹配fliter文本。如果匹配返回True,否則False。 搜索是大小寫敏感的,同時匹配文本和標簽''' return filter in self.memo or filter in self.tags ``` 在繼續之前,我們應該快速啟動交互式解釋器并測試我們剛剛寫下的代碼。測試要經常做,因為事情從來不會按照你期望的方式進行下去。事實上,當我測試這個例子的第一個版本時,我發現我忘記了`match`函數中的`self`參數!我們將在第10章*Python設計模式I*中討論自動化測試。目前,使用解釋器檢查就足夠了: ``` >>> from notebook import Note >>> n1 = Note("hello first") >>> n2 = Note("hello again") >>> n1.id 1 >>> n2.id 2 >>> n1.match('hello') True >>> n2.match('second') False ``` 看起來一切都像預期的那樣。接下來讓我們創建我們的筆記本: ``` class Notebook: '''代表可被標簽、被修改和被搜索的筆記的集合''' def __init__(self): '''用一個空列表初始化一個筆記本''' self.notes = [] def new_note(self, memo, tags=''): '''創建一個新筆記,并把它加入列表中''' self.notes.append(Note(memo, tags)) def modify_memo(self, note_id, memo): '''找到給定id的筆記,并將memo改為給定的值''' for note in self.notes: if note.id == note_id: note.memo = memo break def modify_tags(self, note_id, tags): '''找到給定id的筆記,并將標簽改為給定的值''' for note in self.notes: if note.id == note_id: note.tags = tags break def search(self, filter): '''發現匹配給定字符串的筆記''' return [note for note in self.notes if note.match(filter)] ``` 我們停一下。首先,讓我們測試它,以確保它工作正常: ``` >>> from notebook import Note, Notebook >>> n = Notebook() >>> n.new_note("hello world") >>> n.new_note("hello again") >>> n.notes [<notebook.Note object at 0xb730a78c>, <notebook.Note object at 0xb73103ac>] >>> n.notes[0].id 1 >>> n.notes[1].id 2 >>> n.notes[0].memo 'hello world' >>> n.search("hello") [<notebook.Note object at 0xb730a78c>, <notebook.Note object at 0xb73103ac>] >>> n.search("world") [<notebook.Note object at 0xb730a78c>] >>> n.modify_memo(1, "hi world") >>> n.notes[0].memo 'hi world' ``` 確實有用。盡管代碼有點混亂;我們的修改標簽`modify_tags`和修改備忘錄`modify_memo`的方法幾乎相同。這不是好的編碼實踐。讓我們看看我們如何改進它。 </b> 兩種方法都試圖在做某事之前通過給定的ID來識別某個筆記。認識到這一點,讓我們添加一個方法來定位帶有特定標ID的筆記。我們給這個方法名稱加一個單下劃線的前綴,表示該方法只能供內部使用,當然,我們的菜單接口仍然可以訪問這個方法,如果它想訪問的話: ``` def _find_note(self, note_id): '''定位給定id的筆記''' for note in self.notes: if note.id == note_id: return note return None def modify_memo(self, note_id, memo): '''找到給定id的筆記,并將memo改為給定的值''' self._find_note(note_id).memo = memo ``` 現在應該可以了。讓我們看看菜單接口。接口只需要顯示一個菜單,讓用戶輸入選擇。這是我們的第一次嘗試: ``` import sys from notebook import Notebook, Note class Menu: '''顯示一個菜單,當運行時,對選擇作出反應''' def __init__(self): self.notebook = Notebook() self.choices = { "1": self.show_notes, "2": self.search_notes, "3": self.add_note, "4": self.modify_note, "5": self.quit } def display_menu(self): print(""" Notebook Menu 1. Show all Notes 2. Search Notes 3. Add Note 4. Modify Note 5. Quit """) def run(self): '''顯示一個菜單,對選擇作出反應''' while True: self.display_menu() choice = input("Enter an option: ") action = self.choices.get(choice) if action: action() else: print("{0} is not a valid choice".format(choice)) def show_notes(self, notes=None): if not notes: notes = self.notebook.notes for note in notes: print("{0}: {1}\n{2}".format( note.id, note.tags, note.memo)) def search_notes(self): filter = input("Search for: ") notes = self.notebook.search(filter) self.show_notes(notes) def add_note(self): memo = input("Enter a memo: ") self.notebook.new_note(memo) print("Your note has been added.") def modify_note(self): id = input("Enter a note id: ") memo = input("Enter a memo: ") tags = input("Enter tags: ") if memo: self.notebook.modify_memo(id, memo) if tags: self.notebook.modify_tags(id, tags) def quit(self): print("Thank you for using your notebook today.") sys.exit(0) if __name__ == "__main__": Menu().run() ``` 代碼首先絕對導入notebook模塊中的對象。相對導入將不起作用,因為我們沒有將代碼放入包中。菜單`Menu`類的運行`run`方法反復顯示菜單,并通過調用notebook模塊上的函數返回待選擇項。對于Python來講,這是相當奇特的習慣作法;這是我們將在(第10章Python設計模式I)討論命令模式的輕量級版本。用戶輸入的選項是字符串。在菜單的`__init__`方法,我們創建了一個將字符串映射到菜單對象本身上的函數的字典。然后,當用戶做出選擇時,我們將從字典里檢索對象。動作`action`變量實際上是指一種特定的方法,通過給變量附加空括號調用這個方法(這種方式產生的方法都沒有參數)。當然,用戶可能輸入了不適當的選擇,所以在調用它之前,我們檢查這個變量是否真的存在。 </b> 各種方法都要求用戶提供輸入,并調用與輸入關聯的`Notebook`對象中適當的方法。對于搜索`search`實現,我們注意到,當我們過濾完筆記后,我們需要向用戶展示結果,所以我們提供具有雙重功能的`show_notes`函數;它接受一個可選的`notes`參數。如果提供了這個參數,它將顯示過濾后的筆記清單,但如果沒有,它將顯示所有的筆記。由于`notes`參數是可選的,`show_notes`函數仍然可以作為空菜單項,在沒有參數的情況下被調用。 </b> 如果我們測試這段代碼,我們會發現修改筆記不起作用。這里有兩個` bug`,即: * 當我們輸入不存在的筆記ID時,筆記本崩潰。我們永遠不應該相信我們的用戶輸入了正確的數據! * 即使我們輸入了正確的ID,它也會崩潰,因為筆記ID是整數,但是我們的菜單傳遞了一個字符串。 后一個錯誤可以通過修改筆記本`Notebook`類的`_find_note`方法來解決。將輸入參數和存儲在整數ID轉換為字符串后進行比較,如下所示: ``` def _find_note(self, note_id): '''定位給定id的筆記''' for note in self.notes: if str(note.id) == str(note_id): return note return None ``` 我們只需將輸入(`note_id`)和筆記ID轉換為字符串后,然后再比較它們。我們也可以將輸入轉換成整數,但是如果用戶輸入的是字母`a`而不是數字`1`,就會有問題。 </b> 用戶輸入不存在的筆記ID的問題可以通過更改兩個`modify`方法來解決。在方法中,通過`_find_note`檢查是否真的返回了一個筆記,像這樣: ``` def modify_memo(self, note_id, memo): '''找到給定id的筆記,并將memo改為給定的值''' note = self._find_note(note_id) if note: note.memo = memo return True return False ``` 此方法已更新為返回真或假,具體取決于能否找到某個筆記。如果用戶輸入了無效筆記,菜單將使用這個返回值來顯示一條錯誤信息。這個代碼有點笨拙,如果能返回一個異常就好了,我們將在第4章“異常處理”中講述這些。 ## 總結 在本章中,我們學習了在python中創建類和分配屬性和方法是多么簡單的事情。與許多語言不同,Python區分了構造函數和初始化函數。它對訪問控制的態度很靈活。作用域有許多不同的級別,包括包、模塊、類和函數。我們理解了相對導入和絕對導入之間的區別,以及如何管理Python的第三方包。 </b> 在下一章中,我們將學習如何使用繼承來共享實現。
                  <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>

                              哎呀哎呀视频在线观看