關於內存
理解變量在計算機內存中的表示也非常重要。當我們寫:
a = 'ABC'
時,Python解釋器幹了兩件事情:
-
在內存中創建了一個
'ABC'
的字符串; -
在內存中創建了一個名爲
a
的變量,並把它指向'ABC'
。
也可以把一個變量a
賦值給另一個變量b
,這個操作實際上是把變量b
指向變量a
所指向的數據,例如下面的代碼:
a = 'ABC'
b = a
a = 'XYZ'
print(b)
最後一行打印出變量b
的內容到底是'ABC'
呢還是'XYZ'
?如果從數學意義上理解,就會錯誤地得出b
和a
相同,也應該是'XYZ'
,但實際上b
的值是'ABC'
,讓我們一行一行地執行代碼,就可以看到到底發生了什麼事:
執行a = 'ABC'
,解釋器創建了字符串'ABC'
和變量a
,並把a
指向'ABC'
:
執行b = a
,解釋器創建了變量b
,並把b
指向a
指向的字符串'ABC'
:
執行a = 'XYZ'
,解釋器創建了字符串'XYZ',並把a
的指向改爲'XYZ'
,但b
並沒有更改:
所以,最後打印變量b
的結果自然是'ABC'
了。
編碼問題
總結一下現在計算機系統通用的字符編碼工作方式:
在計算機內存中,統一使用Unicode編碼,當需要保存到硬盤或者需要傳輸的時候,就轉換爲UTF-8編碼。
用記事本編輯的時候,從文件讀取的UTF-8字符被轉換爲Unicode字符到內存裏,編輯完成後,保存的時候再把Unicode轉換爲UTF-8保存到文件:
瀏覽網頁的時候,服務器會把動態生成的Unicode內容轉換爲UTF-8再傳輸到瀏覽器:
所以你看到很多網頁的源碼上會有類似<meta charset="UTF-8" />
的信息,表示該網頁正是用的UTF-8編碼。
格式化
你可能猜到了,%
運算符就是用來格式化字符串的。在字符串內部,%s
表示用字符串替換,%d
表示用整數替換,有幾個%?
佔位符,後面就跟幾個變量或者值,順序要對應好。如果只有一個%?
,括號可以省略。
常見的佔位符有:
%d | 整數 |
%f | 浮點數 |
%s | 字符串 |
%x | 十六進制整數 |
其中,格式化整數和浮點數還可以指定是否補0和整數與小數的位數:
>>> '%2d-%02d' % (3, 1)
' 3-01'
>>> '%.2f' % 3.1415926
'3.14'
如果你不太確定應該用什麼,%s
永遠起作用,它會把任何數據類型轉換爲字符串:
>>> 'Age: %s. Gender: %s' % (25, True)
'Age: 25. Gender: True'
有些時候,字符串裏面的%
是一個普通字符怎麼辦?這個時候就需要轉義,用%%
來表示一個%
:
>>> 'growth rate: %d %%' % 7
'growth rate: 7 %'
可變的tuple
tuple和list非常類似,但是tuple一旦初始化就不能修改,比如同樣是列出同學的名字:
>>> classmates = ('Michael', 'Bob', 'Tracy')
現在,classmates這個tuple不能變了,它也沒有append(),insert()這樣的方法。其他獲取元素的方法和list是一樣的,你可以正常地使用classmates[0]
,classmates[-1]
,但不能賦值成另外的元素。
不可變的tuple有什麼意義?因爲tuple不可變,所以代碼更安全。如果可能,能用tuple代替list就儘量用tuple。
tuple的陷阱:當你定義一個tuple時,在定義的時候,tuple的元素就必須被確定下來,比如:
>>> t = (1, 2)
>>> t
(1, 2)
如果要定義一個空的tuple,可以寫成()
:
>>> t = ()
>>> t
()
但是,要定義一個只有1個元素的tuple,如果你這麼定義:
>>> t = (1)
>>> t
1
定義的不是tuple,是1
這個數!這是因爲括號()
既可以表示tuple,又可以表示數學公式中的小括號,這就產生了歧義,因此,Python規定,這種情況下,按小括號進行計算,計算結果自然是1
。
所以,只有1個元素的tuple定義時必須加一個逗號,
,來消除歧義:
>>> t = (1,)
>>> t
(1,)
Python在顯示只有1個元素的tuple時,也會加一個逗號,
,以免你誤解成數學計算意義上的括號。
最後來看一個“可變的”tuple:
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])
這個tuple定義的時候有3個元素,分別是'a'
,'b'
和一個list。不是說tuple一旦定義後就不可變了嗎?怎麼後來又變了?
別急,我們先看看定義的時候tuple包含的3個元素:
當我們把list的元素'A'
和'B'
修改爲'X'
和'Y'
後,tuple變爲:
表面上看,tuple的元素確實變了,但其實變的不是tuple的元素,而是list的元素。tuple一開始指向的list並沒有改成別的list,所以,tuple所謂的“不變”是說,tuple的每個元素,指向永遠不變。即指向'a'
,就不能改成指向'b'
,指向一個list,就不能改成指向其他對象,但指向的這個list本身是可變的!
理解了“指向不變”後,要創建一個內容也不變的tuple怎麼做?那就必須保證tuple的每一個元素本身也不能變。
字典的特性
和list比較,dict有以下幾個特點:
- 查找和插入的速度極快,不會隨着key的增加而變慢;
- 需要佔用大量的內存,內存浪費多。
而list相反:
- 查找和插入的時間隨着元素的增加而增加;
- 佔用空間小,浪費內存很少。
所以,dict是用空間來換取時間的一種方法。
dict可以用在需要高速查找的很多地方,在Python代碼中幾乎無處不在,正確使用dict非常重要,需要牢記的第一條就是dict的key必須是不可變對象。
這是因爲dict根據key來計算value的存儲位置,如果每次計算相同的key得出的結果不同,那dict內部就完全混亂了。這個通過key計算位置的算法稱爲哈希算法(Hash)。
要保證hash的正確性,作爲key的對象就不能變。在Python中,字符串、整數等都是不可變的,因此,可以放心地作爲key。而list是可變的,就不能作爲key:
>>> key = [1, 2, 3]
>>> d[key] = 'a list'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
再議不可變對象
上面我們講了,str是不變對象,而list是可變對象。
對於可變對象,比如list,對list進行操作,list內部的內容是會變化的,比如:
>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']
而對於不可變對象,比如str,對str進行操作呢:
>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'
雖然字符串有個replace()
方法,也確實變出了'Abc'
,但變量a
最後仍是'abc'
,應該怎麼理解呢?
我們先把代碼改成下面這樣:
>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'
要始終牢記的是,a
是變量,而'abc'
纔是字符串對象!有些時候,我們經常說,對象a
的內容是'abc'
,但其實是指,a
本身是一個變量,它指向的對象的內容纔是'abc'
:
當我們調用a.replace('a', 'A')
時,實際上調用方法replace
是作用在字符串對象'abc'
上的,而這個方法雖然名字叫replace
,但卻沒有改變字符串'abc'
的內容。相反,replace
方法創建了一個新字符串'Abc'
並返回,如果我們用變量b
指向該新字符串,就容易理解了,變量a
仍指向原有的字符串'abc'
,但變量b
卻指向新字符串'Abc'
了:
所以,對於不變對象來說,調用對象自身的任意方法,也不會改變該對象自身的內容。相反,這些方法會創建新的對象並返回,這樣,就保證了不可變對象本身永遠是不可變的。
函數參數
1.位置參數
就是最普通的參數
2.默認參數
設置默認參數時,有幾點要注意:
一是必選參數在前,默認參數在後,否則Python的解釋器會報錯(思考一下爲什麼默認參數不能放在必選參數前面);
二是如何設置默認參數。
當函數有多個參數時,把變化大的參數放前面,變化小的參數放後面。變化小的參數就可以作爲默認參數。
使用默認參數有什麼好處?最大的好處是能降低調用函數的難度。
舉個例子,我們寫個一年級小學生註冊的函數,需要傳入name
和gender
兩個參數:
def enroll(name, gender):
print('name:', name)
print('gender:', gender)
這樣,調用enroll()
函數只需要傳入兩個參數:
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
如果要繼續傳入年齡、城市等信息怎麼辦?這樣會使得調用函數的複雜度大大增加。
我們可以把年齡和城市設爲默認參數:
def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
這樣,大多數學生註冊時不需要提供年齡和城市,只提供必須的兩個參數:
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
age: 6
city: Beijing
只有與默認參數不符的學生才需要提供額外的信息:
enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')
可見,默認參數降低了函數調用的難度,而一旦需要更復雜的調用時,又可以傳遞更多的參數來實現。無論是簡單調用還是複雜調用,函數只需要定義一個。
默認參數的坑
先定義一個函數,傳入一個list,添加一個END
再返回:
def add_end(L=[]):
L.append('END')
return L
當你正常調用時,結果似乎不錯:
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
當你使用默認參數調用時,一開始結果也是對的:
>>> add_end()
['END']
但是,再次調用add_end()
時,結果就不對了:
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
很多初學者很疑惑,默認參數是[]
,但是函數似乎每次都“記住了”上次添加了'END'
後的list。
原因解釋如下:
Python函數在定義的時候,默認參數L
的值就被計算出來了,即[]
,因爲默認參數L
也是一個變量,它指向對象[]
,每次調用該函數,如果改變了L
的內容,則下次調用時,默認參數的內容就變了,不再是函數定義時的[]
了。
所以,定義默認參數要牢記一點:默認參數必須指向不變對象!
要修改上面的例子,我們可以用None
這個不變對象來實現:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
現在,無論調用多少次,都不會有問題:
>>> add_end()
['END']
>>> add_end()
['END']
爲什麼要設計str
、None
這樣的不變對象呢?因爲不變對象一旦創建,對象內部的數據就不能修改,這樣就減少了由於修改數據導致的錯誤。此外,由於對象不變,多任務環境下同時讀取對象不需要加鎖,同時讀一點問題都沒有。我們在編寫程序時,如果可以設計一個不變對象,那就儘量設計成不變對象。
3.可變參數
*nums
表示把nums
這個list的所有元素作爲可變參數傳進去
4.關鍵字參數
可變參數允許你傳入0個或任意個參數,這些可變參數在函數調用時自動組裝爲一個tuple。而關鍵字參數允許你傳入0個或任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝爲一個dict。請看示例:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
函數person
除了必選參數name
和age
外,還接受關鍵字參數kw
。在調用該函數時,可以只傳入必選參數:
>>> person('Michael', 30)
name: Michael age: 30 other: {}
也可以傳入任意個數的關鍵字參數:
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
生成器
如果列表元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出後續的元素呢?這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱爲生成器:generator。
創建生成器(一)
要創建一個generator,有很多種方法。第一種方法很簡單,只要把一個列表生成式的[]
改成()
,就創建了一個generator:
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
創建L
和g
的區別僅在於最外層的[]
和()
,L
是一個list,而g
是一個generator。
如果要一個一個打印出來,可以通過next()
函數獲得generator的下一個返回值:
>>> next(g)
0
>>> next(g)
1
創建生成器(二)
也就是說,上面的函數和generator僅一步之遙。要把fib
函數變成generator,只需要把print(b)
改爲yield b
就可以了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
這就是定義generator的另一種方法。如果一個函數定義中包含yield
關鍵字,那麼這個函數就不再是一個普通函數,而是一個generator:
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
這裏,最難理解的就是generator和函數的執行流程不一樣。函數是順序執行,遇到return
語句或者最後一行函數語句就返回。而變成generator的函數,在每次調用next()
的時候執行,遇到yield
語句返回,再次執行時從上次返回的yield
語句處繼續執行。
迭代器
我們已經知道,可以直接作用於for
循環的數據類型有以下幾種:
一類是集合數據類型,如list
、tuple
、dict
、set
、str
等;
一類是generator
,包括生成器和帶yield
的generator function。
這些可以直接作用於for
循環的對象統稱爲可迭代對象:Iterable
。
可以使用isinstance()
判斷一個對象是否是Iterable
對象:
>>> from collections import Iterable
>>> isinstance([], Iterable)
True
可以被next()
函數調用並不斷返回下一個值的對象稱爲迭代器:Iterator
。
生成器都是Iterator
對象,但list
、dict
、str
雖然是Iterable
,卻不是Iterator
。
把list
、dict
、str
等Iterable
變成Iterator
可以使用iter()
函數:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
你可能會問,爲什麼list
、dict
、str
等數據類型不是Iterator
?
這是因爲Python的Iterator
對象表示的是一個數據流,Iterator對象可以被next()
函數調用並不斷返回下一個數據,直到沒有數據時拋出StopIteration
錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()
函數實現按需計算下一個數據,所以Iterator
的計算是惰性的,只有在需要返回下一個數據時它纔會計算。
Iterator
甚至可以表示一個無限大的數據流,例如全體自然數。而使用list是永遠不可能存儲全體自然數的。
小結
凡是可作用於for
循環的對象都是Iterable
類型;
凡是可作用於next()
函數的對象都是Iterator
類型,它們表示一個惰性計算的序列;
集合數據類型如list
、dict
、str
等是Iterable
但不是Iterator
,不過可以通過iter()
函數獲得一個Iterator
對象。
Python的for
循環本質上就是通過不斷調用next()
函數實現的,例如:
for x in [1, 2, 3, 4, 5]:
pass
實際上完全等價於:
# 首先獲得Iterator對象:
it = iter([1, 2, 3, 4, 5])
# 循環:
while True:
try:
# 獲得下一個值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循環
break
區別
next()
方法可以逐次地返回數據流中的每一項,當沒有更多數據可用時,next()
方法會拋出異常StopIteration。此時迭代器對象已經枯竭了,之後調用next()
方法都會拋出異常StopIteration。迭代器需要有一個__iter()__
方法用來返回迭代器本身。因此它也是一個可迭代的對象。__iter__()
和 next()
方法,生成器顯得特別簡潔,而且生成器也是高效的。除了創建和保存程序狀態的自動方法,當發生器終結時,還會自動拋出StopIteration
異常。一個帶有yield
的函數就是一個
生成器,它和普通函數不同,生成一個 generator 看起來像函數調用,但不會執行任何函數代碼,直到對其調用next()
(在
for 循環中會自動調用next()
)纔開始執行。雖然執行流程仍按函數的流程執行,但每執行到一個yield
語句就會中斷,並返回一個迭代值,下次執行時從yield
的下一個語句繼續執行。看起來就好像一個函數在正常執行的過程中被yield
中斷了數次,每次中斷都會通過yield
返回當前的迭代值(yield
暫停一個函數,next()
從其暫停處恢復其運行)。生成器也是一個迭代器,但是你只可以迭代他們一次,不能重複迭代,因爲它並沒有把所有值存儲在內存中,而是實時地生成值:
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
從結果上看用()
代替[]
效果是一樣的,但是,你不可能第二次執行for
i in mygenerator
(譯註:這裏作者所表達的意思是第二次執行達不到期望的效果)因爲生成器只能使用一次:首先計算出0,然後計算出1,最後計算出4。
Yield
爲了完全弄懂yield,你必須清楚的是:當函數被調用時,函數體中的代碼是不會運行的,函數僅僅是返回一個生成器對象。
for
第一次調用生成器對象時,代碼將會從函數的開始處運行直到遇到yield
爲止,然後返回此次循環的第一個值,接着循環地執行函數體,返回下一個值,直到沒有值返回爲止。
一旦函數運行再也沒有遇到yield時,生成器就被認爲是空的。這有可能是因爲循環終止,或者因爲沒有滿足任何if/else
。
函數式編程
函數式編程的一個特點就是,允許把函數本身作爲參數傳入另一個函數,還允許返回一個函數!
變量可以指向函數
函數名也是變量
面向對象編程
數據封裝、繼承和多態是面向對象的三大特點
假設我們要處理學生的成績表,爲了表示一個學生的成績,面向過程的程序可以用一個dict表示:
std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }
而處理學生成績可以通過函數實現,比如打印學生的成績:
def print_score(std):
print('%s: %s' % (std['name'], std['score']))
如果採用面向對象的程序設計思想,我們首選思考的不是程序的執行流程,而是Student
這種數據類型應該被視爲一個對象,這個對象擁有name
和score
這兩個屬性(Property)。如果要打印一個學生的成績,首先必須創建出這個學生對應的對象,然後,給對象發一個print_score
消息,讓對象自己把自己的數據打印出來。
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
給對象發消息實際上就是調用對象對應的關聯函數,我們稱之爲對象的方法(Method)。面向對象的程序寫出來就像這樣:
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
面向對象的設計思想是從自然界中來的,因爲在自然界中,類(Class)和實例(Instance)的概念是很自然的。Class是一種抽象概念,比如我們定義的Class——Student,是指學生這個概念,而實例(Instance)則是一個個具體的Student,比如,Bart Simpson和Lisa Simpson是兩個具體的Student。
所以,面向對象的設計思想是抽象出Class,根據Class創建Instance。
面向對象的抽象程度又比函數要高,因爲一個Class既包含數據,又包含操作數據的方法。
類與實例
面向對象最重要的概念就是類(Class)和實例(Instance),必須牢記類是抽象的模板,比如Student類,而實例是根據類創建出來的一個個具體的“對象”,每個對象都擁有相同的方法,但各自的數據可能不同。
仍以Student類爲例,在Python中,定義類是通過class
關鍵字:
class Student(object):
pass
class
後面緊接着是類名,即Student
,類名通常是大寫開頭的單詞,緊接着是(object)
,表示該類是從哪個類繼承下來的,繼承的概念我們後面再講,通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會繼承的類。
定義好了Student
類,就可以根據Student
類創建出Student
的實例,創建實例是通過類名+()實現的:
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
可以看到,變量bart
指向的就是一個Student
的實例,後面的0x10a67a590
是內存地址,每個object的地址都不一樣,而Student
本身則是一個類。
繼承和多態
當子類和父類都存在相同的run()
方法時,我們說,子類的run()
覆蓋了父類的run()
,在代碼運行的時候,總是會調用子類的run()
。這樣,我們就獲得了繼承的另一個好處:多態。
多態的好處
新增一個Animal
的子類,不必對run_twice()
做任何修改,實際上,任何依賴Animal
作爲參數的函數或者方法都可以不加修改地正常運行,原因就在於多態。
多態的好處就是,當我們需要傳入Dog
、Cat
、Tortoise
……時,我們只需要接收Animal
類型就可以了,因爲Dog
、Cat
、Tortoise
……都是Animal
類型,然後,按照Animal
類型進行操作即可。由於Animal
類型有run()
方法,因此,傳入的任意類型,只要是Animal
類或者子類,就會自動調用實際類型的run()
方法,這就是多態的意思:
對於一個變量,我們只需要知道它是Animal
類型,無需確切地知道它的子類型,就可以放心地調用run()
方法,而具體調用的run()
方法是作用在Animal
、Dog
、Cat
還是Tortoise
對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,不管細節,而當我們新增一種Animal
的子類時,只要確保run()
方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的“開閉”原則:
對擴展開放:允許新增Animal
子類;
對修改封閉:不需要修改依賴Animal
類型的run_twice()
等函數。
鴨子類型
對於靜態語言(例如Java)來說,如果需要傳入Animal
類型,則傳入的對象必須是Animal
類型或者它的子類,否則,將無法調用run()
方法。
對於Python這樣的動態語言來說,則不一定需要傳入Animal
類型。我們只需要保證傳入的對象有一個run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
這就是動態語言的“鴨子類型”,它並不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
Python的“file-like object“就是一種鴨子類型。對真正的文件對象,它有一個read()
方法,返回其內容。但是,許多對象,只要有read()
方法,都被視爲“file-like object“。許多函數接收的參數就是“file-like object“,你不一定要傳入真正的文件對象,完全可以傳入任何實現了read()
方法的對象。
類屬性和實例屬性
當我們定義了一個類屬性後,這個屬性雖然歸類所有,但類的所有實例都可以訪問到。
在編寫程序的時候,千萬不要把實例屬性和類屬性使用相同的名字,因爲相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性後,再使用相同的名稱,訪問到的將是類屬性。
__slots__
爲了達到限制的目的,Python允許在定義class的時候,定義一個特殊的__slots__
變量,來限制該class實例能添加的屬性:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定義允許綁定的屬性名稱
@property
Python內置的@property
裝飾器就是負責把一個方法變成屬性調用的:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
@property
的實現比較複雜,我們先考察如何使用。把一個getter方法變成屬性,只需要加上@property
就可以了,此時,@property
本身又創建了另一個裝飾器@score.setter
,負責把一個setter方法變成屬性賦值,於是,我們就擁有一個可控的屬性操作:
>>> s = Student()
>>> s.score = 60 # OK,實際轉化爲s.set_score(60)
>>> s.score # OK,實際轉化爲s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
注意到這個神奇的@property
,我們在對實例屬性操作的時候,就知道該屬性很可能不是直接暴露的,而是通過getter和setter方法來實現的。
__call__
一個對象實例可以有自己的屬性和方法,當我們調用實例方法時,我們用instance.method()
來調用。能不能直接在實例本身上調用呢?在Python中,答案是肯定的。
任何類,只需要定義一個__call__()
方法,就可以直接對實例進行調用。請看示例:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
調用方式如下:
>>> s = Student('Michael')
>>> s() # self參數不要傳入
My name is Michael.
__call__()
還可以定義參數。對實例進行直接調用就好比對一個函數進行調用一樣,所以你完全可以把對象看成函數,把函數看成對象,因爲這兩者之間本來就沒啥根本的區別。
線程和進程
對於操作系統來說,一個任務就是一個進程(Process),比如打開一個瀏覽器就是啓動一個瀏覽器進程,打開一個記事本就啓動了一個記事本進程,打開兩個記事本就啓動了兩個記事本進程,打開一個Word就啓動了一個Word進程。
有些進程還不止同時幹一件事,比如Word,它可以同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就需要同時運行多個“子任務”,我們把進程內的這些“子任務”稱爲線程(Thread)。
由於每個進程至少要幹一件事,所以,一個進程至少有一個線程。當然,像Word這種複雜的進程可以有多個線程,多個線程可以同時執行,多線程的執行方式和多進程是一樣的,也是由操作系統在多個線程之間快速切換,讓每個線程都短暫地交替運行,看起來就像同時執行一樣。當然,真正地同時執行多線程需要多核CPU纔可能實現。多任務的實現有3種方式:
- 多進程模式;
- 多線程模式;
- 多進程+多線程模式。
多線程和多進程最大的不同在於,多進程中,同一個變量,各自有一份拷貝存在於每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。
正則表達式
^
表示行的開頭,^\d
表示必須以數字開頭。
$
表示行的結束,\d$
表示必須以數字結束。
py
也可以匹配'python'
,但是加上^py$
就變成了整行匹配,就只能匹配'py'
了。
UDP連接
TCP是建立可靠連接,並且通信雙方都可以以流的形式發送數據。相對TCP,UDP則是面向無連接的協議。
使用UDP協議時,不需要建立連接,只需要知道對方的IP地址和端口號,就可以直接發數據包。但是,能不能到達就不知道了。
雖然用UDP傳輸數據不可靠,但它的優點是和TCP比,速度快,對於不要求可靠到達的數據,就可以使用UDP協議。