一個很全的在線手冊中心:
https://docs.pythontab.com/
《Intermediate Python》在線中文手冊:https://docs.pythontab.com/interpy/
1.0 *args 和 **kwargs
用法
*args 和 **kwargs 主要用于函數定義。 你可以將不定數量的參數傳遞給一個函數。
其中,**kwargs 允許你將不定長度的鍵值對, 作為參數傳遞給一個函數。 如果你想要在一個函數里處理帶名字的參數, 你應該使用**kwargs。
這里的不定的意思是:預先并不知道, 函數使用者會傳遞多少個參數給你, 所以在這個場景下使用這兩個關鍵字。 *args 是用來發送一個非鍵值對的可變數量的參數列表給一個函數.
示例
def test_var_args(f_arg, *argv):
print("first normal arg:", f_arg)
for arg in argv:
print("another arg through *argv:", arg)
test_var_args('yasoob', 'python', 'eggs', 'test')
def greet_me(**kwargs):
for key, value in kwargs.items():
print("{0} == {1}".format(key, value))
greet_me(var1="arg1", var2="arg2")
def test_args_kwargs(arg1, arg2, arg3):
print("arg1:", arg1)
print("arg2:", arg2)
print("arg3:", arg3)
args = ("two", 3, 5)
test_args_kwargs(*args)
kargs = {"arg3": 3, "arg2": "two", "arg1": 5}
test_args_kwargs(**kargs)
標準參數與args、*kwargs在使用時的順序
那么如果你想在函數里同時使用所有這三種參數, 順序是這樣的:
some_func(fargs, *args, **kwargs)
什么時候用他們
最常見的用例是在寫函數裝飾器的時候
此外它也可以用來做猴子補丁(monkey patching)。猴子補丁的意思是在程序運行時(runtime)修改某些代碼。 打個比方,你有一個類,里面有個叫get_info的函數會調用一個API并返回相應的數據。如果我們想測試它,可以把API調用替換成一些測試數據。例如:
import someclass
def get_info(self, *args):
return "Test data"
someclass.get_info = get_info
猴子補丁:monkey patch指的是在運行時動態替換,一般是在startup的時候.
用過gevent就會知道,會在最開頭的地方gevent.monkey.patch_all();把標準庫中的thread/socket等給替換掉.這樣我們在后面使用socket的時候可以跟平常一樣使用,無需修改任何代碼,但是它變成非阻塞的了.
例如,想把json 替換為ujson(ujson效率更高),不需要把每個文件的import json 修改為import ujson as json
其實只需要在進程startup的地方monkey patch就行了.是影響整個進程空間的.
同一進程空間中一個module只會被運行一次.
下面是代碼main.py:import json import ujson def monkey_patch_json(): json.__name__ = 'ujson' json.dumps = ujson.dumps json.loads = ujson.loads monkey_patch_json() print 'main.py',json.__name__
2.0 調試
從命令行運行
你可以在命令行使用Python debugger運行一個腳本:
這會觸發debugger在腳本第一行指令處停止執行。這在腳本很短時會很有幫助。你可以通過(Pdb)模式接著查看變量信息,并且逐行調試。
從腳本內部運行
同時,你也可以在腳本內部設置斷點,這樣就可以在某些特定點查看變量信息和各種執行時信息了。這里將使用pdb.set_trace()方法來實現。
import pdb
def make_bread():
pdb.set_trace()
return "I don't have time"
print(make_bread())
命令列表:
c: 繼續執行
w: 顯示當前正在執行的代碼行的上下文信息
a: 打印當前函數的參數列表
s: 執行當前代碼行,并停在第一個能停的地方(相當于單步進入)
n: 繼續執行到當前函數的下一行,或者當前行直接返回(單步跳過)
3.0 生成器(Generators)
迭代器:
迭代器是一個讓程序員可以遍歷一個容器(特別是列表)的對象。然而,一個迭代器在遍歷并讀取一個容器的數據元素時,并不會執行一個迭代。
- 可迭代對象(Iterable)
Python中任意的對象,只要它定義了可以返回一個迭代器的iter方法,或者定義了可以支持下標索引的getitem方法(這些雙下劃線方法會在其他章節中全面解釋),那么它就是一個可迭代對象。簡單說,可迭代對象就是能提供迭代器的任意對象。 - 迭代器(Iterator)
任意對象,只要定義了next(Python2) 或者next方法,它就是一個迭代器 - 迭代(Iteration)
用簡單的話講,它就是從某個地方(比如一個列表)取出一個元素的過程。當我們使用一個循環來遍歷某個東西時,這個過程本身就叫迭代。現在既然我們有了這些術語的基本理解,那我們開始理解生成器吧。
生成器(Generators):
生成器也是一種迭代器,但是你只能對其迭代一次。這是因為它們并沒有把所有的值存在內存中,而是在運行時生成值。你通過遍歷來使用它們,要么用一個“for”循環,要么將它們傳遞給任意可以進行迭代的函數和結構。大多數時候生成器是以函數來實現的。然而,它們并不返回一個值,而是yield(暫且譯作“生出”)一個值。
生成器函數的例子:
def generator_function():
for i in range(10):
yield i
for item in generator_function():
print(item)
# Output: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
這個案例并不是非常實用(這樣做會消耗大量資源)。生成器最佳應用場景是:你不想同一時間將所有計算出來的大量結果集分配到內存當中,特別是結果集里還包含循環。
下面是一個計算斐波那契數列的生成器:
# generator version
def fibon(n):
a = b = 1
for i in range(n):
yield a
a, b = b, a + b
函數使用方法如下:
for x in fibon(1000000):
print(x)
用這種方式,我們可以不用擔心它會使用大量資源。然而,之前如果我們這樣來實現的話:
def fibon(n):
a = b = 1
result = []
for i in range(n):
result.append(a)
a, b = b, a + b
return result
這也許會在計算很大的輸入參數時,用盡所有的資源。我們已經討論過生成器使用一次迭代,但我們并沒有測試過。在測試前你需要再知道一個Python內置函數:next()。它允許我們獲取一個序列的下一個元素。那我們來驗證下我們的理解:
def generator_function():
for i in range(3):
yield i
gen = generator_function()
print(next(gen))
# Output: 0
print(next(gen))
# Output: 1
print(next(gen))
# Output: 2
print(next(gen))
# Output: Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
結果驗證:
我們可以看到,在yield掉所有的值后,next()觸發了一個StopIteration的異常。基本上這個異常告訴我們,所有的值都已經被yield完了。你也許會奇怪,為什么我們在使用for循環時沒有這個異常呢?啊哈,答案很簡單。for循環會自動捕捉到這個異常并停止調用next()。
Python中一些內置數據類型也支持迭代:
my_string = "Yasoob"
next(my_string)
# Output: Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: str object is not an iterator
好吧,這不是我們預期的。這個異常說那個str對象不是一個迭代器。對,就是這樣!它是一個可迭代對象,而不是一個迭代器。這意味著它支持迭代,但我們不能直接對其進行迭代操作。那我們怎樣才能對它實施迭代呢?是時候學習下另一個內置函數,iter。它將根據一個可迭代對象返回一個迭代器對象。這里是我們如何使用它:
my_string = "Yasoob"
my_iter = iter(my_string)
next(my_iter)
# Output: 'Y'
4.0 Map,Filter和Reduce
5.0 set(集合)數據結構
set(集合)是一個非常有用的數據結構。它與列表(list)的行為類似,區別在于set不能包含重復的值。
交集
可以對比兩個集合的交集(兩個集合中都有的數據),如下:
valid = set(['yellow', 'red', 'blue', 'green', 'black'])
input_set = set(['red', 'brown'])
print(input_set.intersection(valid))
### 輸出: set(['red'])
差集
你可以用差集(difference)找出無效的數據,相當于用一個集合減去另一個集合的數據,例如:
valid = set(['yellow', 'red', 'blue', 'green', 'black'])
input_set = set(['red', 'brown'])
print(input_set.difference(valid))
### 輸出: set(['brown'])
6.0 三元運算符
三元運算符通常在Python里被稱為條件表達式,這些表達式基于真(true)/假(not)的條件判斷,在Python 2.4以上才有了三元操作..
例子:
is_fat = True
state = "fat" if is_fat else "not fat"
或使用元組:
fat = True
fitness = ("skinny", "fat")[fat]
print("Ali is ", fitness)
#輸出: Ali is fa
7.0 裝飾器
裝飾器(Decorators)是Python的一個重要部分。簡單地說:他們是修改其他函數的功能的函數。他們有助于讓我們的代碼更簡短,也更Pythonic(Python范兒)。
一切皆對象
首先我們來理解下Python中的函數
def hi(name="yasoob"):
return "hi " + name
print(hi())
# output: 'hi yasoob'
# 我們甚至可以將一個函數賦值給一個變量,比如
greet = hi
# 我們這里沒有在使用小括號,因為我們并不是在調用hi函數
# 而是在將它放在greet變量里頭。我們嘗試運行下這個
print(greet())
# output: 'hi yasoob'
# 如果我們刪掉舊的hi函數,看看會發生什么!
del hi
print(hi())
#outputs: NameError
print(greet())
#outputs: 'hi yasoob'
在函數中定義函數
剛才那些就是函數的基本知識了。我們來讓你的知識更進一步。在Python中我們可以在一個函數中定義另一個函數:
def hi(name="yasoob"):
print("now you are inside the hi() function")
def greet():
return "now you are in the greet() function"
def welcome():
return "now you are in the welcome() function"
print(greet())
print(welcome())
print("now you are back in the hi() function")
hi()
#output:now you are inside the hi() function
# now you are in the greet() function
# now you are in the welcome() function
# now you are back in the hi() function
# 上面展示了無論何時你調用hi(), greet()和welcome()將會同時被調用。
# 然后greet()和welcome()函數在hi()函數之外是不能訪問的,比如:
greet()
#outputs: NameError: name 'greet' is not defined
那現在我們知道了可以在函數中定義另外的函數。也就是說:我們可以創建嵌套的函數。現在你需要再多學一點,就是函數也能返回函數。
從函數中返回函數
其實并不需要在一個函數里去執行另一個函數,我們也可以將其作為輸出返回出來:
def hi(name="yasoob"):
def greet():
return "now you are in the greet() function"
def welcome():
return "now you are in the welcome() function"
if name == "yasoob":
return greet
else:
return welcome
a = hi()
print(a)
#outputs: <function greet at 0x7f2143c01500>
#上面清晰地展示了`a`現在指向到hi()函數中的greet()函數
#現在試試這個
print(a())
#outputs: now you are in the greet() function
再次看看這個代碼。在if/else語句中我們返回greet和welcome,而不是greet()和welcome()。為什么那樣?這是因為當你把一對小括號放在后面,這個函數就會執行;然而如果你不放括號在它后面,那它可以被到處傳遞,并且可以賦值給別的變量而不去執行它。
你明白了嗎?讓我再稍微多解釋點細節。
當我們寫下a = hi(),hi()會被執行,而由于name參數默認是yasoob,所以函數greet被返回了。如果我們把語句改為a = hi(name = "ali"),那么welcome函數將被返回。我們還可以打印出hi()(),這會輸出now you are in the greet() function。
將函數作為參數傳給另一個函數
def hi():
return "hi yasoob!"
def doSomethingBeforeHi(func):
print("I am doing some boring work before executing hi()")
print(func())
doSomethingBeforeHi(hi)
#outputs:I am doing some boring work before executing hi()
# hi yasoob!
現在你已經具備所有必需知識,來進一步學習裝飾器真正是什么了。裝飾器讓你在一個函數的前后去執行代碼。
你的第一個裝飾器
在上一個例子里,其實我們已經創建了一個裝飾器!現在我們修改下上一個裝飾器,并編寫一個稍微更有用點的程序:
def a_new_decorator(a_func):
def wrapTheFunction():
print("I am doing some boring work before executing a_func()")
a_func()
print("I am doing some boring work after executing a_func()")
return wrapTheFunction
def a_function_requiring_decoration():
print("I am the function which needs some decoration to remove my foul smell")
a_function_requiring_decoration()
#outputs: "I am the function which needs some decoration to remove my foul smell"
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
#now a_function_requiring_decoration is wrapped by wrapTheFunction()
a_function_requiring_decoration()
#outputs:I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()
我們剛剛應用了之前學習到的原理。這正是python中裝飾器做的事情!它們封裝一個函數,并且用這樣或者那樣的方式來修改它的行為。現在你也許疑惑,我們在代碼里并沒有使用@符號?那只是一個簡短的方式來生成一個被裝飾的函數。這里是我們如何使用@來運行之前的代碼:
@a_new_decorator
def a_function_requiring_decoration():
"""Hey you! Decorate me!"""
print("I am the function which needs some decoration to "
"remove my foul smell")
a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()
#the @a_new_decorator is just a short way of saying:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
希望你現在對Python裝飾器的工作原理有一個基本的理解。如果我們運行如下代碼會存在一個問題:
print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction
這并不是我們想要的!Ouput輸出應該是“a_function_requiring_decoration”。這里的函數被warpTheFunction替代了。它重寫了我們函數的名字和注釋文檔(docstring)。幸運的是Python提供給我們一個簡單的函數來解決這個問題,那就是functools.wraps。我們修改上一個例子來使用functools.wraps:
from functools import wraps
def a_new_decorator(a_func):
@wraps(a_func)
def wrapTheFunction():
print("I am doing some boring work before executing a_func()")
a_func()
print("I am doing some boring work after executing a_func()")
return wrapTheFunction
@a_new_decorator
def a_function_requiring_decoration():
"""Hey yo! Decorate me!"""
print("I am the function which needs some decoration to "
"remove my foul smell")
print(a_function_requiring_decoration.__name__)
# Output: a_function_requiring_decoration
現在好多了。我們接下來學習裝飾器的一些常用場景。
藍本規范:
from functools import wraps
def decorator_name(f):
@wraps(f)
def decorated(*args, **kwargs):
if not can_run:
return "Function will not run"
return f(*args, **kwargs)
return decorated
@decorator_name
def func():
return("Function is running")
can_run = True
print(func())
# Output: Function is running
can_run = False
print(func())
# Output: Function will not run
注意:@wraps接受一個函數來進行裝飾,并加入了復制函數名稱、注釋文檔、參數列表等等的功能。這可以讓我們在裝飾器里面訪問在裝飾之前的函數的屬性。
使用場景
- 授權
裝飾器能有助于檢查某個人是否被授權去使用一個web應用的端點(endpoint)。它們被大量使用于Flask和Django web框架中。這里是一個例子來使用基于裝飾器的授權:
from functools import wraps
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, **kwargs)
return decorated
- 日志
日志是裝飾器運用的另一個亮點。這是個例子:
from functools import wraps
def logit(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
@logit
def addition_func(x):
"""Do some math."""
return x + x
result = addition_func(4)
# Output: addition_func was called
帶參數的裝飾器
來想想這個問題,難道@wraps不也是個裝飾器嗎?但是,它接收一個參數,就像任何普通的函數能做的那樣。那么,為什么我們不也那樣做呢?
這是因為,當你使用@my_decorator語法時,你是在應用一個以單個函數作為參數的一個包裹函數。記住,Python里每個東西都是一個對象,而且這包括函數!記住了這些,我們可以編寫一下能返回一個包裹函數的函數。
在函數中嵌入裝飾器
我們回到日志的例子,并創建一個包裹函數,能讓我們指定一個用于輸出的日志文件。
from functools import wraps
def logit(logfile='out.log'):
def logging_decorator(func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打開logfile,并寫入內容
with open(logfile, 'a') as opened_file:
# 現在將日志打到指定的logfile
opened_file.write(log_string + '\n')
return func(*args, **kwargs)
return wrapped_function
return logging_decorator
@logit()
def myfunc1():
pass
myfunc1()
# Output: myfunc1 was called
# 現在一個叫做 out.log 的文件出現了,里面的內容就是上面的字符串
@logit(logfile='func2.log')
def myfunc2():
pass
myfunc2()
# Output: myfunc2 was called
# 現在一個叫做 func2.log 的文件出現了,里面的內容就是上面的字符串
裝飾器類
現在我們有了能用于正式環境的logit裝飾器,但當我們的應用的某些部分還比較脆弱時,異常也許是需要更緊急關注的事情。比方說有時你只想打日志到一個文件。而有時你想把引起你注意的問題發送到一個email,同時也保留日志,留個記錄。這是一個使用繼承的場景,但目前為止我們只看到過用來構建裝飾器的函數。
幸運的是,類也可以用來構建裝飾器。那我們現在以一個類而不是一個函數的方式,來重新構建logit。
from functools import wraps
class logit(object):
def __init__(self, logfile='out.log'):
self.logfile = logfile
def __call__(self, func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打開logfile并寫入
with open(self.logfile, 'a') as opened_file:
# 現在將日志打到指定的文件
opened_file.write(log_string + '\n')
# 現在,發送一個通知
self.notify()
return func(*args, **kwargs)
return wrapped_function
def notify(self):
# logit只打日志,不做別的
pass
這個實現有一個附加優勢,在于比嵌套函數的方式更加整潔,而且包裹一個函數還是使用跟以前一樣的語法:
@logit()
def myfunc1():
pass
現在,我們給logit創建子類,來添加email的功能(雖然email這個話題不會在這里展開)。
class email_logit(logit):
'''
一個logit的實現版本,可以在函數調用時發送email給管理員
'''
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(logit, self).__init__(*args, **kwargs)
def notify(self):
# 發送一封email到self.email
# 這里就不做實現了
pass
從現在起,@email_logit將會和@logit產生同樣的效果,但是在打日志的基礎上,還會多發送一封郵件給管理員。
8.0 slots魔法
在Python中,每個類都有實例屬性。默認情況下Python用一個字典來保存一個對象的實例屬性。這非常有用,因為它允許我們在運行時去設置任意的新屬性。
然而,對于有著已知屬性的小類來說,它可能是個瓶頸。這個字典浪費了很多內存。Python不能在對象創建時直接分配一個固定量的內存來保存所有的屬性。因此如果你創建許多對象(我指的是成千上萬個),它會消耗掉很多內存。
不過還是有一個方法來規避這個問題。這個方法需要使用slots來告訴Python不要使用字典,而且只給一個固定集合的屬性分配空間。
這里是一個使用與不使用slots的例子:
不使用 __slots__:
class MyClass(object):
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
self.set_up()
# ...
使用 __slots__:
class MyClass(object):
__slots__ = ['name', 'identifier']
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
self.set_up()
# ...
第二段代碼會為你的內存減輕負擔。通過這個技巧,有些人已經看到內存占用率幾乎40%~50%的減少。
9.0 容器(Collections)
Python附帶一個模塊,它包含許多容器數據類型,名字叫作collections。我們將討論它的作用和用法。
我們將討論的是:
defaultdict
counter
deque
namedtuple
enum.Enum (包含在Python 3.4以上)
defaultdict
與dict類型不同,你不需要檢查key是否存在,所以我們能這樣做:
from collections import defaultdict
colours = (
('Yasoob', 'Yellow'),
('Ali', 'Blue'),
('Arham', 'Green'),
('Ali', 'Black'),
('Yasoob', 'Red'),
('Ahmed', 'Silver'),
)
favourite_colours = defaultdict(list)
for name, colour in colours:
favourite_colours[name].append(colour)
print(favourite_colours)
輸出:
# defaultdict(<type 'list'>,
# {'Arham': ['Green'],
# 'Yasoob': ['Yellow', 'Red'],
# 'Ahmed': ['Silver'],
# 'Ali': ['Blue', 'Black']
# })
另一種重要的是例子就是:當你在一個字典中對一個鍵進行嵌套賦值時,如果這個鍵不存在,會觸發keyError異常。 defaultdict允許我們用一個聰明的方式繞過這個問題。 首先我分享一個使用dict觸發KeyError的例子,然后提供一個使用defaultdict的解決方案。
問題:
some_dict = {}
some_dict['colours']['favourite'] = "yellow"
異常輸出:KeyError: 'colours'
解決方案:
import collections
tree = lambda: collections.defaultdict(tree)
some_dict = tree()
some_dict['colours']['favourite'] = "yellow"
運行正常
你可以用json.dumps打印出some_dict,例如:
import json
print(json.dumps(some_dict))
## 輸出: {"colours": {"favourite": "yellow"}}
counter
Counter是一個計數器,它可以幫助我們針對某項數據進行計數。比如它可以用來計算每個人喜歡多少種顏色:
from collections import Counter
colours = (
('Yasoob', 'Yellow'),
('Ali', 'Blue'),
('Arham', 'Green'),
('Ali', 'Black'),
('Yasoob', 'Red'),
('Ahmed', 'Silver'),
)
favs = Counter(name for name, colour in colours)
print(favs)
## 輸出:
## Counter({
## 'Yasoob': 2,
## 'Ali': 2,
## 'Arham': 1,
## 'Ahmed': 1
## })
我們也可以在利用它統計一個文件,例如:
with open('filename', 'rb') as f:
line_count = Counter(f)
print(line_count)
deque
deque提供了一個雙端隊列,你可以從頭/尾兩端添加或刪除元素。要想使用它,首先我們要從collections中導入deque模塊:
它的用法就像python的list,并且提供了類似的方法,例如:
from collections import deque
d = deque()
d.append('1')
d.append('2')
d.append('3')
print(len(d))
## 輸出: 3
print(d[0])
## 輸出: '1'
print(d[-1])
## 輸出: '3'
你可以從兩端取出(pop)數據:
d = deque(range(5))
print(len(d))
## 輸出: 5
d.popleft()
## 輸出: 0
d.pop()
## 輸出: 4
print(d)
## 輸出: deque([1, 2, 3])
對象自省
自省(introspection),在計算機編程領域里,是指在運行時來判斷一個對象的類型的能力。它是Python的強項之一。Python中所有一切都是一個對象,而且我們可以仔細勘察那些對象。Python還包含了許多內置函數和模塊來幫助我們。
- dir
返回一個列表,列出了一個對象所擁有的屬性和方法。這里是一個例子:
my_list = [1, 2, 3]
dir(my_list)
# Output: ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__',
# '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__',
# '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__',
# '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__',
# '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__',
# '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop',
# 'remove', 'reverse', 'sort']
上面的自省給了我們一個列表對象的所有方法的名字。當你沒法回憶起一個方法的名字,這會非常有幫助。如果我們運行dir()而不傳入參數,那么它會返回當前作用域的所有名字。
- type和id
print(type(''))
# Output: <type 'str'>
print(type([]))
# Output: <type 'list'>
print(type({}))
# Output: <type 'dict'>
print(type(dict))
# Output: <type 'type'>
print(type(3))
# Output: <type 'int'>
id()函數返回任意不同種類對象的唯一ID,舉個例子:
name = "Yasoob"
print(id(name))
# Output: 139972439030304
- inspect模塊
inspect模塊也提供了許多有用的函數,來獲取活躍對象的信息。比方說,你可以查看一個對象的成員,只需運行:
import inspect
print(inspect.getmembers(str))
# Output: [('__add__', <slot wrapper '__add__' of ... ...
10.0 列表推導室
推導式(又稱解析式)是Python的一種獨有特性,如果我被迫離開了它,我會非常想念。推導式是可以從一個數據序列構建另一個新的數據序列的結構體。 共有三種推導,在Python2和3中都有支持:
列表(list)推導式
字典(dict)推導式
集合(set)推導式
- 列表(list)推導式
列表推導式(又稱列表解析式)提供了一種簡明扼要的方法來創建列表。
它的結構是在一個中括號里包含一個表達式,然后是一個for語句,然后是0個或多個for或者if語句。那個表達式可以是任意的,意思是你可以在列表中放入任意類型的對象。返回結果將是一個新的列表,在這個以if和for語句為上下文的表達式運行完成之后產生。
規范
這里是另外一個簡明例子:variable = [out_exp for out_exp in input_list if out_exp == 2]
這將對快速生成列表非常有用。multiples = [i for i in range(30) if i % 3 is 0] print(multiples) # Output: [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
有些人甚至更喜歡使用它而不是filter函數。
列表推導式在有些情況下超贊,特別是當你需要使用for循環來生成一個新列表。舉個例 子,你通常會這樣做:
你可以使用列表推導式來簡化它,就像這樣:squared = [] for x in range(10): squared.append(x**2)
squared = [x**2 for x in range(10)]
- 字典推導式(dict comprehensions)
字典推導和列表推導的使用方法是類似的。這里有個我最近發現的例子:
在上面的例子中我們把同一個字母但不同大小寫的值合并起來了。mcase = {'a': 10, 'b': 34, 'A': 7, 'Z': 3} mcase_frequency = { k.lower(): mcase.get(k.lower(), 0) + mcase.get(k.upper(), 0) for k in mcase.keys() } # mcase_frequency == {'a': 17, 'z': 3, 'b': 34}
就我個人來說沒有大量使用字典推導式。你還可以快速對換一個字典的鍵和值:{v: k for k, v in some_dict.items()}
- 集合推導式(set comprehensions)
它們跟列表推導式也是類似的。 唯一的區別在于它們使用大括號{}。 舉個例子:
squared = {x**2 for x in [1, 1, 2]} print(squared) # Output: {1, 4}
- 集合推導式(set comprehensions)
11.0 異常
異常處理是一種藝術,一旦你掌握,會授予你無窮的力量。我將要向你展示我們能處理 異常的一些方式。
最基本的術語里我們知道了try/except從句。可能觸發異常產生的代碼會放到try語句塊里,而處理異常的代碼會在except語句塊里實現。這是一個簡單的例子:
try:
file = open('test.txt', 'rb')
except IOError as e:
print('An IOError occurred. {}'.format(e.args[-1]))
上面的例子里,我們僅僅在處理一個IOError的異常。大部分初學者還不知道的是,我們可以處理多個異常。
- 處理多個異常
我們可以使用三種方法來處理多個異常。
第一種方法需要把所有可能發生的異常放到一個元組里。像這樣:
try:
file = open('test.txt', 'rb')
except (IOError, EOFError) as e:
print("An error occurred. {}".format(e.args[-1]))
另外一種方式是對每個單獨的異常在單獨的except語句塊中處理。我們想要多少個except語句塊都可以。這里是個例子:
try:
file = open('test.txt', 'rb')
except EOFError as e:
print("An EOF error occurred.")
raise e
except IOError as e:
print("An error occurred.")
raise e
現在,最后一種方式會捕獲所有異常:
try:
file = open('test.txt', 'rb')
except Exception:
# 打印一些異常日志,如果你想要的話
raise
- finally從句
我們把我們的主程序代碼包裹進了try從句。然后我們把一些代碼包裹進一個except從句,它會在try從句中的代碼觸發異常時執行。
在下面的例子中,我們還會使用第三個從句,那就是finally從句。包裹到finally從句中的代碼不管異常是否觸發都將會被執行。這可以被用來在腳本執行之后做清理工作。這里是個簡單的例子:try: file = open('test.txt', 'rb') except IOError as e: print('An IOError occurred. {}'.format(e.args[-1])) finally: print("This would be printed whether or not an exception occurred!") # Output: An IOError occurred. No such file or directory # This would be printed whether or not an exception occurred!
- try/else從句
try: print('I am sure no exception is going to occur!') except Exception: print('exception') else: # 這里的代碼只會在try語句里沒有觸發異常時運行, # 但是這里的異常將 *不會* 被捕獲 print('This would only run if no exception occurs. And an error here ' 'would NOT be caught.') finally: print('This would be printed in every case.') # Output: I am sure no exception is going to occur! # This would only run if no exception occurs. # This would be printed in every case.
12.0 lambda表達式
lambda表達式是一行函數。
它們在其他語言中也被稱為匿名函數。如果你不想在程序中對一個函數使用兩次,你也許會想用lambda表達式,它們和普通的函數完全一樣。
原型:
lambda 參數:操作(參數)
例子:
add = lambda x, y: x + y
print(add(3, 5))
# Output: 8
這還有一些lambda表達式的應用案例,可以在一些特殊情況下使用:
列表排序:
a = [(1, 2), (4, 1), (9, 10), (13, -3)]
a.sort(key=lambda x: x[1])
print(a)
# Output: [(13, -3), (4, 1), (1, 2), (9, 10)]
列表并行排序:
data = zip(list1, list2)
data.sort()
list1, list2 = map(lambda t: list(t), zip(*data))
13.0 一行式
- 一行式
本章節,我將向大家展示一些一行式的Python命令,這些程序將對你非常有幫助。
簡易Web Server
你是否想過通過網絡快速共享文件?好消息,Python為你提供了這樣的功能。進入到你要共享文件的目錄下并在命令行中運行下面的代碼:
# Python 2
python -m SimpleHTTPServer
# Python 3
python -m http.server
漂亮的打印
你可以在Python REPL漂亮的打印出列表和字典。這里是相關的代碼:
from pprint import pprint
my_dict = {'name': 'Yasoob', 'age': 'undefined', 'personality': 'awesome'}
pprint(my_dict)
這種方法在字典上更為有效。此外,如果你想快速漂亮的從文件打印出json數據,那么你可以這么做:
cat file.json | python -m json.tool
腳本性能分析 這可能在定位你的腳本中的性能瓶頸時,會非常奏效:
python -m cProfile my_script.py
備注:cProfile是一個比profile更快的實現,因為它是用c寫的
CSV轉換為json
在命令行執行這條指令
python -c "import csv,json;print json.dumps(list(csv.reader(open('csv_file.csv'))))"
確保更換csv_file.csv為你想要轉換的csv文件
列表輾平
您可以通過使用itertools包中的itertools.chain.from_iterable輕松快速的輾平一個列表。下面是一個簡單的例子:
a_list = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain.from_iterable(a_list)))
# Output: [1, 2, 3, 4, 5, 6]
# or
print(list(itertools.chain(*a_list)))
# Output: [1, 2, 3, 4, 5, 6]
一行式的構造器
避免類初始化時大量重復的賦值語句
class A(object):
def __init__(self, a, b, c, d, e, f):
self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})
14.0 For-Else
for循環還有一個else從句,我們大多數人并不熟悉。這個else從句會在循環正常結束時執行。這意味著,循環沒有遇到任何break. 一旦你掌握了何時何地使用它,它真的會非常有用。我自己對它真是相見恨晚。
有個常見的構造是跑一個循環,并查找一個元素。如果這個元素被找到了,我們使用break來中斷這個循環。有兩個場景會讓循環停下來。 - 第一個是當一個元素被找到,break被觸發。 - 第二個場景是循環結束。
現在我們也許想知道其中哪一個,才是導致循環完成的原因。一個方法是先設置一個標記,然后在循環結束時打上標記。另一個是使用else從句。
這就是for/else循環的基本結構:
for item in container:
if search_something(item):
# Found it!
process(item)
break
else:
# Didn't find anything..
not_found_in_container()
14.0 使用C擴展
CPython還為開發者實現了一個有趣的特性,使用Python可以輕松調用C代碼
開發者有三種方法可以在自己的Python代碼中來調用C編寫的函數-ctypes,SWIG,Python/C API。每種方式也都有各自的利弊。
首先,我們要明確為什么要在Python中調用C?
常見原因如下: - 你要提升代碼的運行速度,而且你知道C要比Python快50倍以上 - C語言中有很多傳統類庫,而且有些正是你想要的,但你又不想用Python去重寫它們 - 想對從內存到文件接口這樣的底層資源進行訪問 - 不需要理由,就是想這樣做
14.1 CTypes
Python中的ctypes模塊可能是Python調用C方法中最簡單的一種。ctypes模塊提供了和C語言兼容的數據類型和函數來加載dll文件,因此在調用時不需對源文件做任何的修改。也正是如此奠定了這種方法的簡單性。
示例如下
實現兩數求和的C代碼,保存為add.c
//sample C file to add 2 numbers - int and floats
#include <stdio.h>
int add_int(int, int);
float add_float(float, float);
int add_int(int num1, int num2){
return num1 + num2;
}
float add_float(float num1, float num2){
return num1 + num2;
}
接下來將C文件編譯為.so文件(windows下為DLL)。下面操作會生成adder.so文件
#For Linux
$ gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c
#For Mac
$ gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c
現在在你的Python代碼中來調用它
from ctypes import *
#load the shared object file
adder = CDLL('./adder.so')
#Find sum of integers
res_int = adder.add_int(4,5)
print "Sum of 4 and 5 = " + str(res_int)
#Find sum of floats
a = c_float(5.5)
b = c_float(4.1)
add_float = adder.add_float
add_float.restype = c_float
print "Sum of 5.5 and 4.1 = ", str(add_float(a, b))
輸出如下:
Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 = 9.60000038147
在這個例子中,C文件是自解釋的,它包含兩個函數,分別實現了整形求和和浮點型求和。
在Python文件中,一開始先導入ctypes模塊,然后使用CDLL函數來加載我們創建的庫文件。這樣我們就可以通過變量adder來使用C類庫中的函數了。當adder.add_int()被調用時,內部將發起一個對C函數add_int的調用。ctypes接口允許我們在調用C函數時使用原生Python中默認的字符串型和整型。
而對于其他類似布爾型和浮點型這樣的類型,必須要使用正確的ctype類型才可以。如向adder.add_float()函數傳參時, 我們要先將Python中的十進制值轉化為c_float類型,然后才能傳送給C函數。這種方法雖然簡單,清晰,但是卻很受限。例如,并不能在C中對對象進行操作。
14.2 SWIG
SWIG是Simplified Wrapper and Interface Generator的縮寫。是Python中調用C代碼的另一種方法。在這個方法中,開發人員必須編寫一個額外的接口文件來作為SWIG(終端工具)的入口。
Python開發者一般不會采用這種方法,因為大多數情況它會帶來不必要的復雜。而當你有一個C/C++代碼庫需要被多種語言調用時,這將是個非常不錯的選擇。
示例如下(來自SWIG官網):
example.c文件中的C代碼包含了不同的變量和函數:
#include <time.h>
double My_variable = 3.0;
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
int my_mod(int x, int y) {
return (x%y);
}
char *get_time()
{
time_t ltime;
time(<ime);
return ctime(<ime);
}
編譯它:
unix % swig -python example.i
unix % gcc -c example.c example_wrap.c \
-I/usr/local/include/python2.1
unix % ld -shared example.o example_wrap.o -o _example.so
最后,Python的輸出:
>>> import example
>>> example.fact(5)
120
>>> example.my_mod(7,3)
1
>>> example.get_time()
'Sun Feb 11 23:01:07 1996'
>>>
我們可以看到,使用SWIG確實達到了同樣的效果,雖然下了更多的工夫,但如果你的目標是多語言還是很值得的。
14.3 Python/C API
Python/C API可能是被最廣泛使用的方法。它不僅簡單,而且可以在C代碼中操作你的Python對象。
這種方法需要以特定的方式來編寫C代碼以供Python去調用它。所有的Python對象都被表示為一種叫做PyObject的結構體,并且Python.h
頭文件中提供了各種操作它的函數。例如,如果PyObject表示為PyListType(列表類型)時,那么我們便可以使用PyList_Size()
函數來獲取該結構的長度,類似Python中的len(list)
函數。大部分對Python原生對象的基礎函數和操作在Python.h
頭文件中都能找到。
示例
編寫一個C擴展,添加所有元素到一個Python列表(所有元素都是數字)
來看一下我們要實現的效果,這里演示了用Python調用C擴展的代碼
#Though it looks like an ordinary python import, the addList module is implemented in C
import addList
l = [1,2,3,4,5]
print "Sum of List - " + str(l) + " = " + str(addList.add(l))
上面的代碼和普通的Python文件并沒有什么分別,導入并使用了另一個叫做addList的Python模塊。唯一差別就是這個模塊并不是用Python編寫的,而是C。
接下來我們看看如何用C編寫addList模塊,這可能看起來有點讓人難以接受,但是一旦你了解了這之中的各種組成,你就可以一往無前了。
//Python.h has all the required function definitions to manipulate the Python objects
#include <Python.h>
//This is the function that is called from your python code
static PyObject* addList_add(PyObject* self, PyObject* args){
PyObject * listObj;
//The input arguments come as a tuple, we parse the args to get the various variables
//In this case it's only one list variable, which will now be referenced by listObj
if (! PyArg_ParseTuple( args, "O", &listObj ))
return NULL;
//length of the list
long length = PyList_Size(listObj);
//iterate over all the elements
int i, sum =0;
for (i = 0; i < length; i++) {
//get an element out of the list - the element is also a python objects
PyObject* temp = PyList_GetItem(listObj, i);
//we know that object represents an integer - so convert it into C long
long elem = PyInt_AsLong(temp);
sum += elem;
}
//value returned back to python code - another python object
//build value here converts the C long to a python integer
return Py_BuildValue("i", sum);
}
//This is the docstring that corresponds to our 'add' function.
static char addList_docs[] =
"add( ): add all elements of the list\n";
/* This table contains the relavent info mapping -
<function-name in python module>, <actual-function>,
<type-of-args the function expects>, <docstring associated with the function>
*/
static PyMethodDef addList_funcs[] = {
{"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
{NULL, NULL, 0, NULL}
};
/*
addList is the module name, and this is the initialization block of the module.
<desired module name>, <the-info-table>, <module's-docstring>
*/
PyMODINIT_FUNC initaddList(void){
Py_InitModule3("addList", addList_funcs,
"Add all ze lists");
}
逐步解釋 - Python.h頭文件中包含了所有需要的類型(Python對象類型的表示)和函數定義(對Python對象的操作) - 接下來我們編寫將要在Python調用的函數, 函數傳統的命名方式由{模塊名}_{函數名}組成,所以我們將其命名為addList_add
- 然后填寫想在模塊內實現函數的相關信息表,每行一個函數,以空行作為結束 - 最后的模塊初始化塊簽名為PyMODINIT_FUNC init{模塊名}。
函數addList_add接受的參數類型為PyObject類型結構(同時也表示為元組類型,因為Python中萬物皆為對象,所以我們先用PyObject來定義)。傳入的參數則通過PyArg_ParseTuple()來解析。第一個參數是被解析的參數變量。第二個參數是一個字符串,告訴我們如何去解析元組中每一個元素。字符串的第n個字母正是代表著元組中第n個參數的類型。例如,"i"代表整形,"s"代表字符串類型, "O"則代表一個Python對象。接下來的參數都是你想要通過PyArg_ParseTuple()函數解析并保存的元素。這樣參數的數量和模塊中函數期待得到的參數數量就可以保持一致,并保證了位置的完整性。例如,我們想傳入一個字符串,一個整數和一個Python列表,可以這樣去寫
int n;
char *s;
PyObject* list;
PyArg_ParseTuple(args, "siO", &n, &s, &list);
在這種情況下,我們只需要提取一個列表對象,并將它存儲在listObj變量中。然后用列表對象中的PyList_Size()函數來獲取它的長度。就像Python中調用len(list)。
現在我們通過循環列表,使用PyList_GetItem(list, index)函數來獲取每個元素。這將返回一個PyObject*對象。既然Python對象也能表示PyIntType,我們只要使用PyInt_AsLong(PyObj *)函數便可獲得我們所需要的值。我們對每個元素都這樣處理,最后再得到它們的總和。
總和將被轉化為一個Python對象并通過Py_BuildValue()返回給Python代碼,這里的i表示我們要返回一個Python整形對象。
現在我們已經編寫完C模塊了。將下列代碼保存為setup.py
#build the modules
from distutils.core import setup, Extension
setup(name='addList', version='1.0', \
ext_modules=[Extension('addList', ['adder.c'])])
并且運行
python setup.py install
現在應該已經將我們的C文件編譯安裝到我們的Python模塊中了。
在一番辛苦后,讓我們來驗證下我們的模塊是否有效
#module that talks to the C code
import addList
l = [1,2,3,4,5]
print "Sum of List - " + str(l) + " = " + str(addList.add(l))
輸出結果如下
Sum of List - [1, 2, 3, 4, 5] = 15
如你所見,我們已經使用Python.h API成功開發出了我們第一個Python C擴展。這種方法看似復雜,但你一旦習慣,它將變的非常有效。
Python調用C代碼的另一種方式便是使用Cython讓Python編譯的更快。但是Cython和傳統的Python比起來可以將它理解為另一種語言,所以我們就不在這里過多描述了。
15 open函數
open 函數可以打開一個文件。超級簡單吧?大多數時候,我們看到它這樣被使用:
f = open('photo.jpg', 'r+')
jpgdata = f.read()
f.close()
有三個錯誤存在于上面的代碼中。
open的返回值是一個文件句柄,從操作系統托付給你的Python程序。一旦你處理完文件,你會想要歸還這個文件句柄,只有這樣你的程序不會超出一次能打開的文件句柄的數量上限。
顯式地調用close關閉了這個文件句柄,但前提是只有在read成功的情況下。如果有任意異常正好在f = open(...)之后產生,f.close()將不會被調用(取決于Python解釋器的做法,文件句柄可能還是會被歸還,但那是另外的話題了)。為了確保不管異常是否觸發,文件都能關閉,我們將其包裹成一個with語句:
with open('photo.jpg', 'r+') as f:
jpgdata = f.read()
open的第一個參數是文件名。第二個(mode 打開模式)決定了這個文件如何被打開。
如果你想讀取文件,傳入r
如果你想讀取并寫入文件,傳入r+
如果你想覆蓋寫入文件,傳入w
如果你想在文件末尾附加內容,傳入a
jpg圖像文件一般不是人寫的(而且其實不是人直接可讀的),因此你應該以二進制模式來打開它們,方法是在mode字符串后加一個b(你可以看看開頭的例子里,正確的方式應該是rb)。
如果你以文本模式打開一些東西(比如,加一個t,或者就用r/r+/w/a),你還必須知道要使用哪種編碼。對于計算機來說,所有的文件都是字節,而不是字符。
那你怎么找出正在讀的文件是用哪種編碼寫的呢?好吧,不幸的是,并沒有一個十分簡單的方式來檢測編碼。在不同的編碼中,同樣的字節可以表示不同,但同樣有效的字符。因此,你必須依賴一個元數據(比如,在HTTP頭信息里)來找出編碼。越來越多的是,文件格式將編碼定義成UTF-8。
有了這些基礎知識,我們來寫一個程序,讀取一個文件,檢測它是否是JPG(提示:這些文件頭部以字節FF D8開始),把對輸入文件的描述寫入一個文本文件。
import io
with open('photo.jpg', 'rb') as inf:
jpgdata = inf.read()
if jpgdata.startswith(b'\xff\xd8'):
text = u'This is a JPEG file (%d bytes long)\n'
else:
text = u'This is a random file (%d bytes long)\n'
with io.open('summary.txt', 'w', encoding='utf-8') as outf:
outf.write(text % len(jpgdata))
我敢肯定,現在你會正確地使用open啦!
16 協程
Python中的協程和生成器很相似但又稍有不同。主要區別在于: 生成器是數據的生產者 協程則是數據的消費者
首先我們先來回顧下生成器的創建過程。我們可以這樣去創建一個生成器:
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a+b
然后我們經常在for循環中這樣使用它:
for i in fib():
print i
這樣做不僅快而且不會給內存帶來壓力,因為我們所需要的值都是動態生成的而不是將他們存儲在一個列表中。更概括的說如果現在我們在上面的例子中使用yield
便可獲得了一個協程。協程會消費掉發送給它的值。Python實現的grep
就是個很好的例子:
def grep(pattern):
print("Searching for", pattern)
while True:
line = (yield)
if pattern in line:
print(line)
等等!yield
返回了什么?啊哈,我們已經把它變成了一個協程。它將不再包含任何初始值,相反要從外部傳值給它。我們可以通過send()
方法向它傳值。這有個例子:
search = grep('coroutine')
next(search)
#output: Searching for coroutine
search.send("I love you")
search.send("Don't you love me?")
search.send("I love coroutine instead!")
#output: I love coroutine instead!
發送的值會被yield
接收。我們為什么要運行next()
方法呢?這樣做正是為了啟動一個協程。就像協程中包含的生成器并不是立刻執行,而是通過next()
方法來響應send()
方法。因此,你必須通過next()
方法來執行yield
表達式。
我們可以通過調用close()
方法來關閉一個協程。像這樣:
search = grep('coroutine')
search.close()
更多協程相關知識的學習大家可以參考David Beazley的這份精彩演講。
17 函數緩存 (Function caching)
函數緩存允許我們將一個函數對于給定參數的返回值緩存起來。
當一個I/O密集的函數被頻繁使用相同的參數調用的時候,函數緩存可以節約時間。
在Python 3.2版本以前我們只有寫一個自定義的實現。在Python 3.2以后版本,有個lru_cache的裝飾器,允許我們將一個函數的返回值快速地緩存或取消緩存。
我們來實現一個斐波那契計算器,并使用lru_cache。
from functools import lru_cache
@lru_cache(maxsize=32)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
>>> print([fib(n) for n in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
那個maxsize參數是告訴lru_cache,最多緩存最近多少個返回值。
我們也可以輕松地對返回值清空緩存,通過這樣:
fib.cache_clear()
18 上下文管理器(Context managers)
上下文管理器允許你在有需要的時候,精確地分配和釋放資源。
使用上下文管理器最廣泛的案例就是with語句了。
想象下你有兩個需要結對執行的相關操作,然后還要在它們中間放置一段代碼。
上下文管理器就是專門讓你做這種事情的。舉個例子:
with open('some_file', 'w') as opened_file:
opened_file.write('Hola!')
上面這段代碼打開了一個文件,往里面寫入了一些數據,然后關閉該文件。如果在往文件寫數據時發生異常,它也會嘗試去關閉文件。上面那段代碼與這一段是等價的:
file = open('some_file', 'w')
try:
file.write('Hola!')
finally:
file.close()
當與第一個例子對比時,我們可以看到,通過使用with,許多樣板代碼(boilerplate code)被消掉了。 這就是with語句的主要優勢,它確保我們的文件會被關閉,而不用關注嵌套代碼如何退出。
上下文管理器的一個常見用例,是資源的加鎖和解鎖,以及關閉已打開的文件(就像我已經展示給你看的)。
讓我們看看如何來實現我們自己的上下文管理器。這會讓我們更完全地理解在這些場景背后都發生著什么。
18.1 基于類的實現
一個上下文管理器的類,最起碼要定義_enter_和_exit_方法。
讓我們來構造我們自己的開啟文件的上下文管理器,并學習下基礎知識。
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()
通過定義_enter_和_exit_方法,我們可以在with語句里使用它。我們來試試:
with File('demo.txt', 'w') as opened_file:
opened_file.write('Hola!')
我們的_exit_函數接受三個參數。這些參數對于每個上下文管理器類中的_exit_方法都是必須的。我們來談談在底層都發生了什么。
- with語句先暫存了File類的_exit_方法
- 然后它調用File類的_enter_方法
- _enter_方法打開文件并返回給with語句
- 打開的文件句柄被傳遞給opened_file參數
- 我們使用.write()來寫文件
- with語句調用之前暫存的_exit_方法
- _exit_方法關閉了文件