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

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                [TOC] Python在許多方面似乎比面向對象編程更讓人想起結構化或函數式編程。盡管面向對象編程是過去二十年中最明顯的編程范式,函數式編程最近有所復蘇。就像Python的數據結構一樣,其中大多數工具是底層面向對象實現的語法糖;我們可以將它們看作是建立在(已經抽象的)面向對象范式之上的另一個抽象層。在本章中,我們將介紹一些Python不是嚴格面向對象的特性: * 在一次調用中處理多個常見任務的內置函數 * 文件輸入/輸出和上下文管理器 * 方法重載的替代方法 * 函數也是對象 ## Python內置函數 Python中有許多函數可以在一些特定類型的對象上執行任務或計算,而不需要使用對象類的方法。它們通常是適用于多種類型類的抽象通用計算。這是鴨子類型最好的應用;這些函數接受具有某些屬性或者方法的對象,并且能夠使用這些方法執行泛型操作。其中一些都是具有特殊的雙下劃線的方法。我們已經使用了許多內置函數,但是讓我們快速瀏覽一下重要的函數,然后挑選一些學習。 ### `len()`函數 最簡單的例子是`len()`函數,它計算某種容器對象內項目的數量,例如,字典或列表。你以前已經見過這個函數: ``` >>> len([1,2,3,4]) 4 ``` 為什么這些對象沒有`length`屬性,而必須調用一個函數?技術上來說,它們有。可以調用`len()`的大多數對象都有一個方法`__len __()`,可以返回相同的值。所以`len(myobj)`和`myobj.__len__()`是一樣的。 </b> 那為什么我們使用`len()`函數而不是`__len__`方法呢?很明顯`__len__`是一種特殊的雙下劃線方法,我們最好不要直接調用這個方法。這僅僅是一個解釋。Python開發人員不會輕而易舉作出這種設計決策。 </b> 主要原因是效率。當我們在一個對象上調用`__len__`時,該對象在其命名空間中查找該方法,如果在這個對象上定義了特殊的`__getattribute__`方法(每次訪問對象的屬性或方法時都會被調用),那么`__getattribute__`也必須被調用。此外,`__getattribute__`這種特殊的方法可能被設計成做一些討厭的事情,比如拒絕讓我們可以使用特殊的方法,例如`__len__`!`len()`函數不會有這樣的問題。它實際上調用對象類上的`__len__`函數,所以`len(myobj)`被映射到`MyObj.__len__(myobj)`。 </b> 另一個原因是可維護性。將來,Python開發人員可能希望更改`len()`,以便它可以計算沒有`__len__`方法的對象的長度,例如,通過計算迭代器中返回的項數。他們只要改變一個函數即可,而不是修改程序中所有的無數的`__len__`方法。 </b> `len()`作為外部函數存在,還有另一個極其重要且經常被忽視的原因:向后兼容性。這在很多文章中經常被引用為“歷史原因”,這是一個溫和的輕蔑短語,作者會用它來說明,事情是這樣的,因為很久以前犯了一個錯誤,但我們一直堅持不修改它。嚴格地說,`len()`不是一個錯誤,這是一個設計決定,這個決定,是在一個不太面向對象的時代決定的。它經受住了時間的考驗,有一些好處,所以一定要習慣。 ### `Reversed`反轉函數 `reversed()`函數將任何序列作為輸入,并返回順序相反的副本。當我們想要循環時,它通常用于形成循環從后到前的項目。 </b> 類似于`len`,`reversed`調用類上的`__reversed__()`函數。如果該方法不存在,`reversed`通過調用`__len__`和`__getitem__`構建反向序列,這兩個方法用于定義序列。如果我們想以某種方式定制或優化反向過程,只需要重寫`__reversed__`即可: ``` normal_list=[1,2,3,4,5] class CustomSequence(): def __len__(self): return 5 def __getitem__(self, index): return "x{0}".format(index) class FunkyBackwards(): def __reversed__(self): return "BACKWARDS!" for seq in normal_list, CustomSequence(), FunkyBackwards(): print("\n{}: ".format(seq.__class__.__name__), end="") for item in reversed(seq): print(item, end=", ") ``` 結尾的`for`循環打印了一個正常列表和兩個自定義序列實例的反轉版本(譯注:`FunkyBackwards()`代碼在`python3.5`運行時并不成功)。輸出顯示`reversed`對所有三個都起作用,但是當我們自定義`__reversed__`時,結果卻大不相同: ``` list: 5, 4, 3, 2, 1, CustomSequence: x4, x3, x2, x1, x0, FunkyBackwards: B, A, C, K, W, A, R, D, S, !, ``` 當我們反轉`CustomSequence`時,會為每個項目調用`__getitem__`方法,它只是在索引前插入一個`x`。對于`FunkyBackwards`,`__reversed__`方法返回一個字符串,其每個字符都在`for`循環中被單獨輸出。 > 這兩個類不是很好的序列,因為它們沒有定義一個正確版本的`__iter__`,這樣作用在它們的向前`for`循環永遠不會結束。 ### `Enumerate`枚舉函數 有時,當我們在`for`循環中遍歷容器時,我們希望返回正在處理的當前項目的索引(列表中的當前位置)。`for`循環沒有給我們提供索引,但是`Enumerate`函數可以提供:它創建元組序列,其中每個元組中的第一個對象是索引,第二個是原始項目。 </b> 如果我們需要直接使用索引號,這很有用。考慮一些輸出文件中每一行的行號簡單的代碼: ``` import sys filename = sys.argv[0] #原書寫的是sys.argv[1],正確的應該是sys.argv[0] with open(filename) as file: for index, line in enumerate(file): print("{0}: {1}".format(index+1, line), end='') ``` 使用自己的`filename`作為輸入文件運行此代碼,顯示了它是如何工作的: ``` 1: import sys 2: filename = sys.argv[1] 3: 4: with open(filename) as file: 5: for index, line in enumerate(file): 6: print("{0}: {1}".format(index+1, line), end='') ``` `Enumerate`函數返回元組序列,我們的`for`循環拆分每個元組轉換成兩個值,`print`語句將它們格式化。它給每個行號的索引加一,因為像所有序列一樣,`enumerate`是從零開始的。 </b> 我們只涉及了幾個更重要的Python內置函數。如同你所看到的,這些內置函數中,有些調用的是面向對象的概念,而有些則是純粹的函數或程序范式。標準庫中還有很多其他的例子;一些更有趣的包括: * `all`和`any`,它們接受可迭代對象,并判斷對象中所有項或任意項的值(如非空字符串或列表,非零值數字、不是`None`的對象或字面`True`),如果為真,則返回`True` * `eval`、`exec`和`compile`,它們將字符串作為代碼在解釋器中執行。注意,它們并不安全,所以不要執行未知用戶向你提供的代碼(通常,假設所有未知用戶是惡意的、愚蠢的或兩者兼有) * `hasattr`、`getattr`、`setattr`和`delattr`,它們允許通過字符串名稱操作對象屬性 * `zip`,它接受兩個或多個序列并返回一個新的元組序列,其中每個元組包含來自每個序列的單個值 * 還有更多!請參閱每個的解釋器幫助文檔,使用` dir(__builtins__)`中列出的函數。 ### `File I/O` 到目前為止,我們接觸文件系統的例子完全是在文本文件上操作的,并不需要我們去想這背后發生了什么。然而,操作系統實際上將文件表示為字節序列,而不是文本。我們會做在第8章“字符串和序列化中”深入探討字節和文本之間的關系。現在,我們只要知道從文件中讀取文本數據是一個相當復雜的過程。Python,尤其是Python 3,將替我們完成這些復雜的過程。我們不幸運嗎? </b> 文件的概念早在任何人創造這個面向對象編程術語之前就已經存在了。然而,Python已經包裝了一個接口,操作系統在這個接口中提供了一種甜蜜的抽象,允許我們對文件(或者類似文件,相對于鴨子類型)對象進行操作。 </b> `open()`內置函數用于打開一個文件并返回一個文件對象。為從文件中讀取文本,我們只需要將文件的名稱傳遞給函數。文件將被打開并被讀取,字節將被平臺默認編碼器轉換為文本。 </b> 當然,我們并不總是想讀文件;我們經常想給他們寫數據!要打開文件進行寫入,我們需要傳遞一個`mode`參數作為第二個位置參數,值為“w”: ``` contents = "Some file contents" file = open("filename", "w") file.write(contents) file.close() ``` 我們也可以提供值“a”作為模式參數,這會寫入的內容附加到文件的末尾,而不是完全覆蓋現有文件內容。 </b> 這些帶有內置包裝器將字節轉換成文本是很棒的,但是如果我們想要打開的文件是圖像、可執行文件或其他二進制文件時,該怎么辦呢? </b> 為了打開一個二進制文件,我們修改模式字符串添加一個“b”。所以,“wb”會打開一個文件來寫字節,而“rb”允許我們讀取它們。這么做很像文本文件操作,但是沒有自動將文本編碼成字節。當我們閱讀時這樣一個文件,它將返回字節`bytes`對象而不是字符串`str`,當我們寫入它時,如果我們試圖傳遞文本對象,將會失敗。 > 這些用于控制文件打開方式的模式字符串相當神秘,既不pythonic也不是面向對象所特有的。然而,處理方式幾乎與所有其他編程語言都一致。文件輸入輸出是操作系統的基本工作之一,所有編程語言都必須與操作系統對話,使用相同的系統調用。很高興Python返回了一個帶有很多方法的文件對象,而不是主流操作系統用于識別文件句柄的整數! 一旦打開一個文件進行讀取,我們可以使用`read`、`readline`或`readlines`方法獲取文件的內容。`read`方法返回全部內容的`str`或`bytes`對象,這取決于模式中是否有“b”。小心不要在大型文件上沒有參數的情況下使用此方法。如果你試圖將那么多數據加載到內存中,后果自負! </b> 也可以從文件中讀取固定數量的字節;我們傳遞一個描述我們要讀取多少字節的整數,作為`read`方法的參數。下一次讀取調用將加載下一個字節序列,依此類推。我們可以在`while`循環中以可管理的塊讀取整個文件。 </b> `readline`方法從文件中返回一行(每行的結尾以換行符、回車符或兩者都有,具體取決于創建文件的操作系統)。我們可以反復調用它來獲得額外的行。多個`readlines`方法一起使用,可以返回一個文件中所有行的列表。像讀取方法一樣,在非常大的文件上使用`readlines`是不安全的。這兩種方法都以字節模式打開文件,但是只有當我們解析類文本的數據,在合理的位置有新的一行時,才是有意義的。例如,圖像或音頻文件沒有換行符(除非換行符恰好代表某些像素或聲音),所以`readlines`是沒有意義的。 </b> 為了可讀性,并避免一次將一個大文件讀入內存,通常最好直接在文件對象上使用`for`循環。對于文本文件,它將每次讀取一行,我們可以在循環體內處理它。對于二進制文件,使用`read()`方法讀取固定大小的數據塊,通過傳遞參數獲取要讀取的最大字節數。 </b> 寫一個文件也一樣容易;文件對象上的`write`方法寫入字符串(或字節,如果是二進制數據)。可以重復調用它來寫入多個字符串。`writelines`方法接受字符串序列,并將每個序列值按順序寫入文件。但`writelines`方法不在序列中的每個項目后追加一行。它基本上算是一個命名的函數,用來將字符串序列的內容寫入文件中,而無需使用`for`循環顯式迭代它。 </b> 最后,我們看一下`close`方法。這種方法應該當我們完成讀寫文件時調用,以確保任何寫入的緩沖被寫入磁盤,文件已被正確清理,以及與該文件相關聯的所有資源都被釋放回操作系統。從技術上講,這將在腳本退出時自動發生,但是最好顯式地自我清理,尤其是在長期運行的過程中。 ### 把它放在上下文中 當我們完成文件時,需要關閉它們,這可能我們的代碼變得相當丑陋。因為在文件輸入/輸出期間任何時候都可能發生異常,所以我們應該嘗試將所有對一個文件的調用包裝到`try...finally`。文件應該在`finally`中被關閉,不管輸入/輸出是否成功。這不是很Python。當然,還有更優雅的方法。 </b> 如果我們在一個類似文件的對象上運行`dir`,我們會看到它有兩個名為`__enter__`和`__exit__`的方法。這些方法將文件對象轉換成所謂的**上下文管理器**。基本上,如果我們使用一種叫做`with`語句的特殊語法,這些方法將在嵌套代碼執行前后調用。在文件對象上,`__exit__`方法確保文件被關閉,即使出現異常。我們不再需要顯式管理文件的關閉。`with`語句實際上看起來像這樣: ``` with open('filename') as file: for line in file: print(line, end='') ``` `open`調用返回一個文件對象,該對象具有`__enter__`和`__exit__`方法。返回的對象由`as`子句分配給名為`file`的變量。我們知道當代碼返回到外部縮進級別時文件將被關閉,即使出現異常,文件也會被關閉。 </b> `with`語句在標準庫中的幾個地方,用于啟動或者清理代碼。例如,`urlopen`調用返回一個對象,該對象可以在`with`語句中使用,以便通信結束時清理socket。線程模塊中的鎖可以在`with`語句被執行后釋放鎖。 </b> 最有趣的是,因為`with`語句可以應用于任何具有適當的特殊方法的對象上,我們可以在自己的框架中使用它。例如,請記住,字符串是不可變的,但是有時你需要從多個片段建立一個字符串。為了提高效率,通常通過存儲片段字符串,然后把它們合并來實現。讓我們創建一個簡單的上下文管理器,它允許我們構建一個字符序列,并在退出時自動將其轉換為字符串: ``` class StringJoiner(list): def __enter__(self): return self def __exit__(self, type, value, tb): self.result = "".join(self) ``` 這段代碼將所需的兩種特殊方法添加到繼承自列表的上下文管理器類中。`__enter__`方法執行任何必需的啟動代碼(此處沒有啟動代碼),然后在with語句中將返回一個分配給`as`之后變量的對象。通常,正如我們在這里所做的,對象只是上下文管理器本身。`__exit__`方法接受三個參數。正常情況下情況下,這些都被賦予`None`。但是,如果`with塊`內部發生異常,它們將被設置為與的類型、值和回溯相關的異常。這允許`__exit__`方法即使出現異常時也能執行任何必需的代碼清理工作。在我們的例子中,我們采取了一個粗略的方法,通過連接字符串中的字符來創建結果字符串,而不管是否發生了異常。 </b> 雖然這是我們可以編寫的最簡單的上下文管理器之一,它的用處可能不大,但它確實可以與`with`語句一起工作。看看它的運行情況: ``` import random, string with StringJoiner() as joiner: for i in range(15): joiner.append(random.choice(string.ascii_letters)) print(joiner.result) ``` 這段代碼構造了一個由15個隨機字符組成的字符串。它將使用從列表繼承的`append`方法這些字符附加到`StringJoiner`對象。當`with`語句超出范圍(回到外部縮進級別),則調用`__exit__`方法,`result`屬性在`StringJoiner`對象上將變得可用。我們打印該值以查看隨機字符串。 (譯注:更簡單的例子如下:) ``` a = StringJoiner(['a','b']) with a as f: pass print(f.result) ``` ## 方法重載的替代方法 許多面向對象編程語言的一個突出特征是**方法重載**。方法重載指擁有多個接受不同參數集的同名方法。靜態類型化語言,如果我們想要一個方法接受整數或字符串,方法重載會很有用處。在非面向對象語言中,我們可能需要定義兩個稱為`add_s`和`add_i`的函數,以適應這種情況。靜態類型面向對象語言,我們需要兩種方法,都叫做`add`,一種接受字符串,另一種接受整數。 </b> 在Python中,我們只需要一種方法,它接受任何類型的對象。可能有要對對象類型進行一些測試(例如,如果它是字符串,則將其轉換為整數),但只需要一種方法。 </b> 但是,當我們想要一個具有相同名稱的方法可以接受不同數量或不同組的參數,那還是得使用方法重載。例如,電子郵件的`message`方法可能有兩個版本,一個版本接受“發件人”電子郵件地址。另一個版本查找默認的“發件人”地址。Python不允許存在多個同名的方法,但它提供了一個不同的、同樣靈活的接口。 </b> 我們已經在前面的例子里,看到了將參數發送到方法和函數的一些可能的方法,現在我們將涵蓋所有細節。最簡單的函數沒有參數。我們可能不需要下面這個例子,但它仍然一個完整的例子: ``` def no_args(): pass ``` 然后調用這個函數: ``` no_args() ``` 接受參數的函數,會提供一個逗號分隔的、具有參數名字的列表。只需要提供參數名稱即可。當調用函數時,這些參數的值必須按照參數列表中的位置被提供,也可以忽略其中的參數。下面是之前例子中,我們最常用的定義有參函數的方法: ``` def mandatory_args(x, y, z): pass ``` 然后調用這個函數: ``` mandatory_args("a string", a_variable, 5) ``` 任何類型的對象都可以作為參數傳遞:對象、容器、主數據類型、甚至函數和類。上面這個例子被傳遞的參數分別是字符串、未知變量,以及一個整數。 ### 默認參數 如果我們想讓參數是可選的,不需要再創建一個有不同參數組的方法,我們可以在一個方法中,用等號給參數一個默認值。如果被調用的方法沒有這個參數值,參數將被分配一個默認值。當然,我們也可以選擇一個不同的值來覆蓋這個默認值。通常,使用`None`、空字符串、或列表作為默認值是合適的。 </b> 下面是一個帶有默認值參數的函數定義: ``` def default_arguments(x, y, z, a="Some String", b=False): pass ``` 前三個參數仍然是強制性的,必須被傳入被調用的函數。剩下兩個參數提供了默認參數值。 </b> 有許多調用這個函數的方法。我們可以按順序提供所有的參數值,就好像它們是位置參數(都得被傳入): ``` default_arguments("a string", variable, 8, "", True) ``` 或者,我們只按順序提供強制性參數,而把默認值分配給剩下的相應的參數: ``` default_arguments("a longer string", some_variable, 14) ``` 我們也可以用等號語法按不同的傳參順序調用函數,或者是忽略我們不感興趣的參數。例如,我們可以忽略第一個鍵參數,而僅提供第二個鍵參數: ``` default_arguments("a string", variable, 14, b=True) ``` 我們甚至可以使用等號語法打亂必填參數的順序,只要提供它們的值即可。 ``` >>> default_arguments(y=1,z=2,x=3,a="hi") 3 1 2 hi False ``` 這么多選擇,似乎很難選,但是如果你把位置參數看作是順序列表,把鍵參數看作字典,你會發現其實很容易選擇的。如果你需要一個有特定參數的函數,把這個參數定義為強制性的;如果你有一個有意義的默認值,把它定義為鍵參數。選擇什么樣的傳參方法,主要依賴于哪些參數值是需要被提供的,以及哪些參數值可以用默認值。 </b> 鍵參數有一個要注意的地方,一旦給函數提供了一個默認值給鍵參數,這個默認值在函數被解釋后就固定了,我們不能在調用函數時修改這個默認值。或者說我們不能動態地生成默認值。例如,下面這段代碼并不能像我們希望的那樣執行: ``` number = 5 def funky_function(number=number): print(number) number=6 funky_function(8) funky_function() print(number) ``` 如果我們運行這段代碼,它會先輸出8,然后調用沒有參數的函數,輸出5。我們已經給變量在`number`重新賦值為6,這點最后代碼可以證明,但當這個函數被調用的時候,仍然打印5;這說明參數默認值時在函數被定義的時候就被計算了,而不是在調用函數的時候再確認默認值。 </b> 這對空容器,例如列表、集合和字典,將會有點小問題。例如,我們調用一個函數,函數將提供一個我們將要操作的列表,但是這個列表是可選的。我們可能選擇一個空列表作為參數默認值。我們不要這么做;這段代碼將在第一次執行的時候,生成一個唯一的列表: ``` >>> def hello(b=[]): ... b.append('a') ... print(b) ... >>> hello() ['a'] >>> hello() ['a', 'a'] ``` 看到沒,這并不是我們所希望的。解決這個問題的辦法是,將參數默認值設置為`None`,然后在方法內使用`iargument = argument if argument else [ ]`(譯注:不知道這是什么解決辦法,我改成下面這樣)。 ``` >>> def hello(b=None): ... f = list(b) if b else ["a"] ... print(f) ... >>> hello() ['a'] >>> hello("b") ['b'] ``` ### 可變參數列表 缺省值不允許我們擁有方法重載的所有好處。但Python有一個平滑的技巧,就是在定義方法時,可以定義任意數量的位置參數或鍵參數,但不需要顯示命名這些參數。我們也可以將任意列表和字典傳遞到這些函數中。 </b> 例如,接受鏈接或鏈接列表并下載網頁的函數,使用這樣的可變參數或`varargs`。而不是接受單一的參數值。這是一個鏈接列表,我們可以接受任意數量的參數,其中每個參數都是不同的鏈接。為此,我們在函數定義中指定`*`運算符: ``` def get_pages(*links): for link in links: #download the link with urllib print(link) ``` `*links`參數表示“我會接受任意數量的參數并把它們放進一個名為`links`的列表中”。如果我們只提供一個參數,它將是一個包含一個參數的列表元素;如果我們不提供參數,它將是一個空列表。因此,下面這些函數調用都是有效的: ``` get_pages() get_pages('http://www.archlinux.org') get_pages('http://www.archlinux.org', 'http://ccphillips.net/') ``` 我們也可以接受任意數量的鍵參數。這些以字典方式傳入函數。它們在函數聲明中用兩個星號指定(如`**kwargs`)。工具通常用于配置設置。下列類允許我們用默認值指定一組選項: ``` class Options: default_options = { 'port': 21, 'host': 'localhost', 'username': None, 'password': None, 'debug': False, } def __init__(self, **kwargs): self.options = dict(Options.default_options) self.options.update(kwargs) def __getitem__(self, key): return self.options[key] ``` 這個類中所有有趣的東西都發生在`__init__`方法中。我們有一個類層級的默認選項和相應值的字典。`__init__`方法中的第一件事是復制這個字典。我們這樣做而不去修改字典的原因是,以防我們實例化兩個獨立的選項集(記住,類級變量在類的實例之間是共享的)。然后,`__init__`使用新字典上的更新方法,將任何非默認值的鍵參數更改為傳入的參數值。`__getitem__`方法允許我們在新類上使用更簡單得索引語法。下面是一個例子: ``` >>> options = Options(username="dusty", password="drowssap", debug=True) >>> options['debug'] True >>> options['port'] 21 >>> options['username'] 'dusty' ``` 我們能夠使用字典索引語法訪問我們的選項實例,字典包括默認值和我們使用鍵參數設置的值。 </b> 關鍵字參數語法可能很危險,因為它可能會破壞“顯式永遠比隱式好”的規則。在前面的例子中,可以傳遞任意鍵參數到`options`的初始化方法中,甚至是默認詞典不存在的選項。這可能不是一件壞事,這取決于類的目的,但是使用該類的人很難發現哪些有效選項是可用的。這很容易輸入令人困惑的打字錯誤(例如,“Dedug”而不是“debug”),它增加了兩個選項,然而應該只有一個應該存在。 </b> 有時候我們需要將任意參數傳遞給第二個函數,這時候鍵參數是非常有用的,但是我們不知道這些參數將會是什么。在第三章“當對象是相似的”,我們建立了對多重繼承的支持。當然,我們可以組合可變參數和可變鍵參數都放到一個函數中,我們也可以使用普通的位置參數和默認參數。下面的例子有點做作,但展示了四種類型的參數: ``` import shutil import os.path def augmented_move(target_folder, *filenames, verbose=False, **specific): '''將所有的文件移動到指定的文件夾中, 允許對特定的文件做特殊處理''' def print_verbose(message, filename): '''如果打開verbose,則打印消息''' if verbose: print(message.format(filename)) for filename in filenames: target_path = os.path.join(target_folder, filename) if filename in specific: if specific[filename] == 'ignore': print_verbose("Ignoring {0}", filename) elif specific[filename] == 'copy': print_verbose("Copying {0}", filename) shutil.copyfile(filename, target_path) else: print_verbose("Moving {0}", filename) shutil.move(filename, target_path) ``` 本示例將處理一個含有任意數量的文件列表。第一個參數是目標文件夾,默認行為是將所有剩余的非鍵參數文件移動到那個文件夾。然后有一個包含默認值的鍵參數`verbose`,它告訴我們是否打印每個已處理文件的信息。最后,我們可以提供一本字典,包含對特定文件名執行的操作;默認行為是移動文件,但是如果在鍵參數中指定了有效的字符串操作,它將被忽略或復制。請注意函數中參數的順序;首先指定位置參數,然后是`*filenames`列表,然后是任何指定的鍵參數,最后是一個`**specific`字典,用來保存剩余的鍵參數。 </b> 我們創建了一個內部幫助函數`print_verbose`,只有`verbose`被設置了,它才會打印消息。這個函數通過封裝這個功能來保持代碼可讀。 </b> 在一般情況下,假設文件存在,這個函數可以這樣被調用: ``` >>> augmented_move("move_here", "one", "two") ``` 該命令將文件`one`和`two`移動到`move_here`目錄中, 假設文件存在(函數中沒有錯誤檢查或異常處理,因此,如果文件或目標目錄不存在,它會非常失敗)。移動后將在沒有任何輸出,因為`verbose`默認為`False`。 </b> 如果我們想看到輸出,我們可以用: ``` >>> augmented_move("move_here", "three", verbose=True) Moving three ``` 這會移動一個名為`three`的文件,并告訴我們它在做什么。請注意,在此示例中,無法將`verbose`指定為位置參數;我們必須傳遞鍵參數。否則,Python會認為這是在`*filenames`列表中的另一個文件名。 </b> 如果我們想復制或忽略列表中的一些文件,而不是移動它們, 我們可以傳遞額外的鍵參數: ``` >>> augmented_move("move_here", "four", "five", "six", four="copy", five="ignore") ``` 這將移動第六個文件并復制第四個文件,但不會顯示任何輸出,因為我們沒有指定`verbose`。當然,我們也可以這樣做,鍵參數可以以任何順序傳遞: ``` >>> augmented_move("move_here", "seven", "eight", "nine", seven="copy", verbose=True, eight="ignore") Copying seven Ignoring eight Moving nine ``` ### 解包參數 還有一個巧妙的技巧,涉及可變參數和鍵參數。我們已經在前面的一些例子中使用了它,但是還缺一個解釋。給定一個值列表或字典,我們可以將這些值傳遞到函數,就像它們是正常的位置參數或鍵參數一樣。看一看下面的代碼: ``` def show_args(arg1, arg2, arg3="THREE"): print(arg1, arg2, arg3) some_args = range(3) more_args = { "arg1": "ONE", "arg2": "TWO"} print("Unpacking a sequence:", end=" ") show_args(*some_args) print("Unpacking a dict:", end=" ") show_args(**more_args) ``` 運行這段代碼,將顯示: ``` Unpacking a sequence: 0 1 2 Unpacking a dict: ONE TWO THREE ``` 該函數接受三個參數,其中一個具有默認值。但當我們提供一個包含三個參數的列表,我們可以在函數調用中使用`*`運算符把它分解成三個參數。如果我們有一個參數詞典,我們可以使用`**`語法將其分解為鍵參數的集合。 </b> 這很有用,當把用戶輸入或外部來源(例如,互聯網頁面或文本文件)收集到的信息傳遞到函數或被調用的方法時。 </b> 還記得我們前面的例子嗎,它使用文本文件中的標題和行來創建包含聯系信息的字典列表?除了在列表中增加字典,我們還可以使用鍵拆包將參數傳遞給`__init__ `方法,該方法接受相同的參數集。看看你能不能實現這件事。(譯注:待補充) ## 函數也是對象 過分強調面向對象原則的編程語并不喜歡不是方法的函數。在這種語言中,你需要創建一個對象來包裝所涉及的方法。有許多情況,我們想要傳遞一個被調用的對象去執行一個動作。這在事件驅動編程中最常見,例如圖形編程工具包或異步服務器;我們會在第10章“Python設計模式I”和第11章“Python設計模式II”看到一些這樣做的設計模式。 </b> 在Python中,我們不需要將這樣的方法包裝在對象中,因為函數已經是對象!我們可以設置函數的屬性(盡管這不是一個常見的活動),我們可以把它們傳過去,以后再調用他們。他們甚至有一些特別的可以直接訪問的屬性。下面是另一個人為的例子: ``` def my_function(): print("The Function Was Called") my_function.description = "A silly function" def second_function(): print("The second was called") second_function.description = "A sillier function." def another_function(function): print("The description:", end=" ") print(function.description) print("The name:", end=" ") print(function.__name__) print("The class:", end=" ") print(function.__class__) print("Now I'll call the function passed in") function() another_function(my_function) another_function(second_function) ``` 如果我們運行這段代碼,我們可以看到我們能夠傳遞兩個不同的函數進入我們的第三個函數,并為每個函數獲得不同的輸出: ``` The description: A silly function The name: my_function The class: <class 'function'> Now I'll call the function passed in The Function Was Called The description: A sillier function. The name: second_function The class: <class 'function'> Now I'll call the function passed in The second was called ``` 我們在函數上設置了一個屬性,命名為`description`(不是很好的描述,誠然)。我們還能夠看到函數的`__name__`屬性,并訪問它的類,表明該函數實際上是一個具有屬性的對象。那我們通過使用可調用語法(括號)去調用函數。 </b> 函數作為頂級對象這一事實最常用于將它們在稍后的日期執行,例如,當滿足某個條件時。讓我們構建一個事件驅動定時器,它是這樣做的: ``` import datetime import time class TimedEvent: def __init__(self, endtime, callback): self.endtime = endtime self.callback = callback def ready(self): return self.endtime <= datetime.datetime.now() class Timer: def __init__(self): self.events = [] def call_after(self, delay, callback): end_time = datetime.datetime.now() + \ datetime.timedelta(seconds=delay) self.events.append(TimedEvent(end_time, callback)) def run(self): while True: ready_events = (e for e in self.events if e.ready()) for event in ready_events: event.callback(self) self.events.remove(event) time.sleep(0.5) ``` 在生產環境中,這段代碼肯定需要使用docstrings做額外的文檔說明!`call_after`方法至少應該提到`delay`參數以秒為單位,`callback`應該接受一個參數:用來調用的計時器。 </b> 我們這里有兩個類。`TimedEvent`類實際上并不打算被其他類訪問;它所做的只是存儲`endtime`和`callback`。我們甚至可以在這里使用元組或命名元組,但給對象一個行為,用來通知我們事件是否已經準備好運行,可能更加方便,所以我們使用一個類來代替。 </b> 計時器`Timer`類只存儲即將發生的事件的列表。它有一個`call_after`方法用于添加新事件。此方法接受一個延遲`delay`參數,作為執行回調之前等待的秒數,`callback`方法本身是一個要在正確時間執行的函數。`callback`方法應該接受一個參數。 </b> `run`方法非常簡單;它使用生成器表達式濾掉任何已經發生的事件,并按順序執行。定時器循環會一直運行,所以必須用鍵盤中斷(*Ctrl + C*或*Ctrl +break*)。每次迭代后,我們都要暫停半秒鐘,給系統休息一下。 </b> 需要注意的是使用回調函數的行。這個函數像任何其他對象一樣傳遞,計時器永遠不會知道或關心函數的原始名稱是什么或者在哪里定義的。當調用這個函數時,計時器只是將括號語法應用于這個存儲的變量。 </b> 這里有一組測試定時器的回調函數: ``` from timer import Timer import datetime def format_time(message, *args): now = datetime.datetime.now().strftime("%I:%M:%S") print(message.format(*args, now=now)) def one(timer): format_time("{now}: Called One") def two(timer): format_time("{now}: Called Two") def three(timer): format_time("{now}: Called Three") class Repeater: def __init__(self): self.count = 0 def repeater(self, timer): format_time("{now}: repeat {0}", self.count) self.count += 1 timer.call_after(5, self.repeater) timer = Timer() timer.call_after(1, one) timer.call_after(2, one) timer.call_after(2, two) timer.call_after(4, two) timer.call_after(3, three) timer.call_after(6, three) repeater = Repeater() timer.call_after(5, repeater.repeater) format_time("{now}: Starting") timer.run() ``` 這個例子讓我們看到多個回調函數如何與定時器交互。第一個回調函數是`format_time`函數。它使用字符串`format`方法添加消息的當前時間,并說明了活動中的可變參數。`format_time`方法將使用可變參數語法接受任意數量的位置參數,然后作為位置參數轉發給字符串的`format`方法。之后,我們創建了三個簡單的回調方法,輸出當前時間和一條短消息,告訴我們哪個回調方法已經調用過了。 </b> `Repeater`類演示了方法也可以用作回調,因為它們實際上是函數。它還顯示了回調函數的`timer`參數很有用:我們可以從當前正在運行回調,給計數器添加一個時間事件。然后我們創建一個計時器,并向其中添加幾個事件,這些事件經過不同的時間之后被調用。最后,我們啟動計時器運行;輸出顯示事件按預期順序運行: ``` 02:53:35: Starting 02:53:36: Called One 02:53:37: Called One 02:53:37: Called Two 02:53:38: Called Three 02:53:39: Called Two 02:53:40: repeat 0 02:53:41: Called Three 02:53:45: repeat 1 02:53:50: repeat 2 02:53:55: repeat 3 02:54:00: repeat 4 ``` Python 3.4引入了與此類似的通用事件循環架構。我們會稍后在第13章“并發”中討論它。 ### 使用函數作為屬性 函數作為對象的一個有趣效果是它們可以被設置為其他對象上的可調用屬性。可以為實例化的對象添加或更改函數: ``` class A: def print(self): print("my class is A") def fake_print(): print("my class is not A") a = A() a.print() a.print = fake_print a.print() ``` 這段代碼創建了一個非常簡單的帶有`print`方法的類,`print`并沒有告訴我們任何我們不知道的事情。然后我們創建一個新的函數來告訴我們一些我們不相信的事情。 </b> 當我們調用`A`類實例上的`print`方法時,它的行為與預期的一樣。但如果我們將`print`方法設置為一個新方法,它告訴我們一些不同的東西: ``` my class is A my class is not A ``` 也可以替換類方法而不是對象方法,盡管這種情況下,我們必須將`self`參數添加到參數列表中。這將改變所有實例對象的方法,即使是對已經實例化的實例。顯然,對于代碼維護,這樣做是危險和令人困惑的。閱讀代碼的人會看到一個方法被調用,并在原始類中查找該方法。但是原始類上的方法并不是被調用的那個。弄清楚到底發生了什么會變得棘手和令人沮喪。 </b> 但是它確實有它的用途。通常,在運行時替換或添加方法(稱為猴子修補)用于自動化測試。如果測試客戶端-服務器應用程序,在測試客戶端時,我們可能不想實際連接到服務器;這可能導致資金意外轉移或尷尬的測試電子郵件被發送到真實的人們那里。相反,我們可以用測試代碼來替換對象上的一些關鍵方法,向服務器發送請求,因此它只記錄方法被調用了。 </b> 猴子補丁也可以用來修復bug或者在第三方代碼中添加一些互動的特征,但行為卻不盡如人意。我們應該謹慎使用它;它幾乎總是一個“混亂的黑客”。但有時候,它是調整現有庫以滿足我們需求的唯一方法。 ### 可調用的對象 正如函數可以作為對象一樣,也可以創建一個可以像函數一樣調用的對象。 </b> 任何對象都可以通過簡單地給它一個可接受必需參數的`__call__`方法來調用。讓我們將計時器示例的`Repeater`類修改成可調用的對象: ``` class Repeater: def __init__(self): self.count = 0 def __call__(self, timer): format_time("{now}: repeat {0}", self.count) self.count += 1 timer.call_after(5, self) timer = Timer() timer.call_after(5, Repeater()) format_time("{now}: Starting") timer.run() ``` 這個例子與前面的類沒有太大不同;我們所做的就是將`repeater`方法的名字改為`__call__`,然后將對象本身作為一個可調用的函數。請注意,當我們調用`call_after`時,我們傳遞的參數是`Repeater()`。這兩個括號正在創建類的新實例;它們并沒有顯式地調用類。這將在計時器內稍后發生。如果我們想在新實例化的對象上執行`__call__`方法,我們會使用相當奇怪的語法:`Repeater()()`。第一組括號構成了對象;第二組執行`__call__`方法。如果我們發現自己在這樣做,我們可能沒有使用正確的抽象。只有當對象應該像函數一樣對待時再使用`__call__`方法。 ## 個案研究 為了結合本章中介紹的一些原則,讓我們構建一個郵件列表經理。經理將跟蹤分到各個命名組中的電子郵件地址。當該發信息的時候,我們可以選擇一個小組,將郵件發送給該組的所有電子郵件地址。 </b> 現在,在我們開始運行這個項目之前,我們應該有一個安全的測試方法,它不需要給一群真實的人發電子郵件。幸運的是,Python支持我們這樣做;像測試HTTP服務器一樣,它有一個內置的簡單郵件傳輸協議(SMTP)服務器,我們可以指示它捕獲我們想發送的任何消息,而不是實際發送它們。我們可以使用以下命令運行服務器: ``` python -m smtpd -n -c DebuggingServer localhost:1025 ``` 在命令提示符下運行此命令將啟動運行在本地機器上的端口1025。但是我們已經指示它使用`DebuggingServer`類(它來自內置的SMTP模塊),它不發送郵件給可能的接收方,而只是在接收時在終端屏幕上打印出來它們。不錯吧。 </b> 現在,在編寫郵件列表之前,讓我們編寫一些實際發送郵件的代碼。當然,Python在標準庫中也支持這一點,它提供一個有點奇怪的接口,所以我們將編寫一個新函數來包裝它: ``` import smtplib from email.mime.text import MIMEText def send_email(subject, message, from_addr, *to_addrs, host="localhost", port=1025, **headers): email = MIMEText(message) email['Subject'] = subject email['From'] = from_addr for header, value in headers.items(): email[header] = value sender = smtplib.SMTP(host, port) for addr in to_addrs: del email['To'] email['To'] = addr sender.sendmail(from_addr, addr, email.as_string()) sender.quit() ``` 我們不會太過詳細地講解這個方法中的代碼;標準庫文檔可以為你提供更有效使用`smtplib`和`email`模塊所需的所有信息。 </b> 我們在函數調用中使用了可變參數和鍵參數語法。可變參數列表允許我們在默認情況下提供單個收件人地址,同時也允許必要時提供多個收件人地址。任何額外的鍵參數都映射到電子郵件頭。這是一個可變參數和鍵參數的令人興奮的用法,但對于調用函數的人來說,這真不是一個很好的界面。事實上,它做了了許多程序員想做的不可能的事情。 </b> 傳遞到函數中的頭代表可以附加到方法上的輔助頭。頭可能包括 `Reply-To`,`Return-Path`或者幾乎任何東西。但是為了在Python中成為一個有效的標識符,一個名字不能包含-字符。一般來說,這個字符代表減法。因此,不可能調用`Reply-To = my@email.com`的函數。看來我們太急于使用鍵參數,因為它們是我們剛剛在本章中了解到的新工具。 </b> 我們將不得不把這個參數改成普通的字典;這將會奏效,因為任何字符串都可以用作字典中的鍵。默認情況下,我們想要這本字典為空,但是我們不能將默認參數設置為空字典。所以,我們必須將默認參數設為`None`,然后在方法的開始設置字典: ``` def send_email(subject, message, from_addr, *to_addrs, host="localhost", port=1025, headers=None): headers = {} if headers is None else headers ``` 如果我們在一個終端上運行調試SMTP服務器,我們可以在Python解釋器中測試代碼: ``` >>> send_email("A model subject", "The message contents", "from@example.com", "to1@example.com", "to2@example.com") ``` 然后,如果我們檢查調試SMTP服務器的輸出,我們會得到以下結果: ``` ---------- MESSAGE FOLLOWS ---------- Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: A model subject From: from@example.com To: to1@example.com X-Peer: 127.0.0.1 The message contents ------------ END MESSAGE ------------ ---------- MESSAGE FOLLOWS ---------- Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: A model subject From: from@example.com To: to2@example.com X-Peer: 127.0.0.1 The message contents ------------ END MESSAGE ------------ ``` 太好了,它已經把我們的包含主題和消息內容的電子郵件“發送”到了兩個預期的地址。現在我們可以發送消息了,讓我們開始處理電子郵件 分組管理系統。我們需要一個對象,能夠和電子郵件地址組相匹配。因為這是一種多對多的關系(任何一個電子郵件地址可以在多個組中;任何一個組都可以關聯有多個電子郵件地址),我們研究過的數據結構似乎沒有一個是非常理想的。我們可以嘗試用一個組名字典,與相關的電子郵件地址列表相匹配,但這將重復電子郵件地址。我們也可以試試一個電子郵件地址字典與組匹配,但導致組重復。兩者似乎都不是最佳選擇。讓我們試試后一個版本,盡管直覺告訴我,組到電子郵件地址的解決方案會更簡單。 </b> 因為我們字典中的值總是唯一電子郵件地址的集合,我們可能應該將它們存儲在一個`set`容器中。我們可以使用`defaultdict`確保每個鍵始終有一個可用的`set`容器: ``` from collections import defaultdict class MailingList: '''管理發送電子郵件地址的分組''' def __init__(self): self.email_map = defaultdict(set) def add_to_group(self, email, group): self.email_map[email].add(group) ``` 現在,讓我們添加一個方法,允許我們從一個或更多組中收集所有電子郵件地址。這可以通過將組列表轉換為集合來實現: ``` def emails_in_groups(self, *groups): groups = set(groups) emails = set() for e, g in self.email_map.items(): if g & groups: emails.add(e) return emails ``` 首先,看看我們迭代的內容:`self.email_map.items()`。這種方法 ,當然會為字典中的每個項目返回鍵值對元組。值是代表組的字符串集合。我們把它們分成兩個變量,命名為`e`和`g`,代表電子郵件地址和組的縮寫。僅當傳遞的組與電子郵件地址組相交時,我們才將電子郵件地址添加到返回集合中。`g & groups`語法是`g.intersection(groups)`的快捷方式;`set`類通過使用特殊的`__add__`方法調用`intersection`來實現這一點。 > 使用集合解析器可以使這段代碼更加簡潔,我們將在第9章“迭代器模式”中討論。 現在,有了這些構造塊,我們可以簡單地將一個方法添加到我們的`MailingList`類中,向特定組發送消息: ``` def send_mailing(self, subject, message, from_addr, *groups, headers=None): emails = self.emails_in_groups(*groups) send_email(subject, message, from_addr, *emails, headers=headers) ``` 該函數依賴于可變參數列表。作為輸入,它需要一個組列表作為可變參數。它獲取指定組的電子郵件列表,并將它們可變參數傳遞給`send_email`,其他參數也被傳遞到這個方法中。 </b> 可以在一個命令行窗口測試該程序,確保SMTP調試服務器運行正常,在第二個命令行窗口中,使用以下命令加載代碼: ``` python -i mailing_list.py ``` 創建一個`MailingList`對象: ``` >>> m = MailingList() ``` 然后創建幾個假的電子郵件地址和組,大致如下: ``` >>> m.add_to_group("friend1@example.com", "friends") >>> m.add_to_group("friend2@example.com", "friends") >>> m.add_to_group("family1@example.com", "family") >>> m.add_to_group("pro1@example.com", "professional") ``` 最后,使用這樣的命令向特定的組發送電子郵件: ``` >>> m.send_mailing("A Party", "Friends and family only: a party", "me@example.com", "friends", "family", headers={"Reply-To": "me2@example.com"}) ``` 給定組中每個電子郵件地址應該顯示在SMTP服務器上的控制臺上。 </b> 郵件列表可以正常工作,但是有點沒用;一旦我們離開程序,我們的信息數據庫丟失了。讓我們修改它,添加一組方法,將電子郵件組列表從文件加載并保存到文件中。 </b> 通常,在磁盤上存儲結構化數據時,有必要思考它是如何儲存的。海量數據庫系統存在的原因之一是如果其他人已經把這種想法應用到數據存儲中,你就不必這么做了。我們會在下一章中,研究一些數據序列化機制,但對于這個例子,讓我們保持簡單,并采用第一個可行的解決方案。 </b> 我想到的數據格式是存儲每個電子郵件地址,后面跟一個空格,再跟逗號分隔的組列表。這種格式似乎合理,我們會在后面繼續討論格式,因為數據格式不是本章的主題。然而,為了說明為什么你需要努力思考如何在磁盤上以什么樣的格式存儲數據,讓我們強調格式的幾個問題。 </b> 首先,電子郵件地址中的空格字符在技術上是合法的。大多數電子郵件提供商禁止空格(他們有充分的理由),但是定義電子郵件地址的規范說,如果電子郵件地址有引號,它可以包含空格。如果我們要用一個空格作為我們的數據格式中的哨兵,技術上我們應該能夠區分這個空格和作為電子郵件一部分的空格。為簡單起見,我們假裝這不是真的,但現實生活中的數據編碼充滿了像這樣的愚蠢問題。第二,考慮逗號分隔的組列表。如果有人決定在一個組名里放置一個逗號,會發生什么呢?如果我們決定在組名中使用逗號是非法的,我們應該在`add_to_group`添加驗證來確保這一點。為了教學清楚,我們也將忽略這個問題。最后,還有許多安全隱患需要考慮:有人會通過在他們電子郵件地址中放一個假的逗號,讓他們分到錯誤的組里嗎?如果解析器遇到無效文件,它會做什么? </b> 這次討論的要點是嘗試使用已經在真實環境測試過的一種數據存儲方法,而不是設計自己的數據序列化協議。那里有一大堆你可能忽略的奇怪的異常案例,最好使用已經遇到并修復了這些異常案例的代碼。 </b> 但是忘記這一點,讓我們寫一些基本的代碼,但不要指望這么點代碼可以確保這種簡單的數據格式是安全: ``` email1@mydomain.com group1,group2 email2@mydomain.com group2,group3 ``` 這段代碼如下: ``` def save(self): with open(self.data_file, 'w') as file: for email, groups in self.email_map.items(): file.write('{} {}\n'.format(email, ','.join(groups)) ) def load(self): self.email_map = defaultdict(set) try: with open(self.data_file) as file: for line in file: email, groups = line.strip().split(' ') groups = set(groups.split(',')) self.email_map[email] = groups except IOError: pass ``` 在`save`方法中,我們在上下文管理器中打開該文件,并將格式化字符串寫入該文件。記住換行符;Python沒有為我們添加這些。`load`方法首先重置字典(以防它包含以前調用加載的數據),使用`for...in`語法,循環遍歷文件中的每一行。同樣,換行符也應包含在行變量中,所以我們必須調用`.strip()`。我們將在下一章中學習更多關于這種字符串操作的知識。 </b> 在使用這些方法之前,我們需要確保對象有一個`self.data_file`屬性,這可以通過修改`__init__`來完成: ``` def __init__(self, data_file): self.data_file = data_file self.email_map = defaultdict(set) ``` 我們可以在解釋器中測試這兩個方法: ``` >>> m = MailingList('addresses.db') >>> m.add_to_group('friend1@example.com', 'friends') >>> m.add_to_group('family1@example.com', 'friends') >>> m.add_to_group('family1@example.com', 'family') >>> m.save() ``` `addresses.db`文件將包含下面兩行: ``` friend1@example.com friends family1@example.com friends,family ``` We can also load this data back into a MailingList object successfully: 我們也可以將這個文件加載回`MailingList`對象中: ``` >>> m = MailingList('addresses.db') >>> m.email_map defaultdict(<class 'set'>, {}) >>> m.load() >>> m.email_map defaultdict(<class 'set'>, {'friend2@example.com': {'friends\n'}, 'family1@example.com': {'family\n'}, 'friend1@example.com': {'friends\n'}}) ``` 如你所見,我忘記了`load`命令,也可能很容易忘記`save`命令。讓任何想在他們代碼中使用我們的`MailingList`API人都容易一點,讓我們提供支持上下文管理器的方法: ``` def __enter__(self): self.load() return self def __exit__(self, type, value, tb): self.save() ``` 這些簡單的方法只是將它們的工作委托給加載和保存,但是我們現在可以在交互式解釋器中編寫這樣的代碼,并且知道所有以前存儲的地址會被加載,當我們完成時,整個列表將被保存: ``` >>> with MailingList('addresses.db') as ml: ... ml.add_to_group('friend2@example.com', 'friends') ... ml.send_mailing("What's up", "hey friends, how's it going", 'me@ example.com', 'friends') ```
                  <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>

                              哎呀哎呀视频在线观看