python基礎回顧

關於內存

理解變量在計算機內存中的表示也非常重要。當我們寫:

a = 'ABC'

時,Python解釋器幹了兩件事情:

  1. 在內存中創建了一個'ABC'的字符串;

  2. 在內存中創建了一個名爲a的變量,並把它指向'ABC'

也可以把一個變量a賦值給另一個變量b,這個操作實際上是把變量b指向變量a所指向的數據,例如下面的代碼:

a = 'ABC'
b = a
a = 'XYZ'
print(b)

最後一行打印出變量b的內容到底是'ABC'呢還是'XYZ'?如果從數學意義上理解,就會錯誤地得出ba相同,也應該是'XYZ',但實際上b的值是'ABC',讓我們一行一行地執行代碼,就可以看到到底發生了什麼事:

執行a = 'ABC',解釋器創建了字符串'ABC'和變量a,並把a指向'ABC'

py-var-code-1

執行b = a,解釋器創建了變量b,並把b指向a指向的字符串'ABC'

py-var-code-2

執行a = 'XYZ',解釋器創建了字符串'XYZ',並把a的指向改爲'XYZ',但b並沒有更改:

py-var-code-3

所以,最後打印變量b的結果自然是'ABC'了。


編碼問題

總結一下現在計算機系統通用的字符編碼工作方式:

在計算機內存中,統一使用Unicode編碼,當需要保存到硬盤或者需要傳輸的時候,就轉換爲UTF-8編碼。

用記事本編輯的時候,從文件讀取的UTF-8字符被轉換爲Unicode字符到內存裏,編輯完成後,保存的時候再把Unicode轉換爲UTF-8保存到文件:

rw-file-utf-8

瀏覽網頁的時候,服務器會把動態生成的Unicode內容轉換爲UTF-8再傳輸到瀏覽器:

web-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個元素:

tuple-0

當我們把list的元素'A''B'修改爲'X''Y'後,tuple變爲:

tuple-1

表面上看,tuple的元素確實變了,但其實變的不是tuple的元素,而是list的元素。tuple一開始指向的list並沒有改成別的list,所以,tuple所謂的“不變”是說,tuple的每個元素,指向永遠不變。即指向'a',就不能改成指向'b',指向一個list,就不能改成指向其他對象,但指向的這個list本身是可變的!

理解了“指向不變”後,要創建一個內容也不變的tuple怎麼做?那就必須保證tuple的每一個元素本身也不能變。

字典的特性

和list比較,dict有以下幾個特點:

  1. 查找和插入的速度極快,不會隨着key的增加而變慢;
  2. 需要佔用大量的內存,內存浪費多。

而list相反:

  1. 查找和插入的時間隨着元素的增加而增加;
  2. 佔用空間小,浪費內存很少。

所以,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-to-str

當我們調用a.replace('a', 'A')時,實際上調用方法replace是作用在字符串對象'abc'上的,而這個方法雖然名字叫replace,但卻沒有改變字符串'abc'的內容。相反,replace方法創建了一個新字符串'Abc'並返回,如果我們用變量b指向該新字符串,就容易理解了,變量a仍指向原有的字符串'abc',但變量b卻指向新字符串'Abc'了:

a-b-to-2-strs

所以,對於不變對象來說,調用對象自身的任意方法,也不會改變該對象自身的內容。相反,這些方法會創建新的對象並返回,這樣,就保證了不可變對象本身永遠是不可變的。


函數參數

1.位置參數

就是最普通的參數

2.默認參數

設置默認參數時,有幾點要注意:

一是必選參數在前,默認參數在後,否則Python的解釋器會報錯(思考一下爲什麼默認參數不能放在必選參數前面);

二是如何設置默認參數。

當函數有多個參數時,把變化大的參數放前面,變化小的參數放後面。變化小的參數就可以作爲默認參數。

使用默認參數有什麼好處?最大的好處是能降低調用函數的難度。

舉個例子,我們寫個一年級小學生註冊的函數,需要傳入namegender兩個參數:

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

爲什麼要設計strNone這樣的不變對象呢?因爲不變對象一旦創建,對象內部的數據就不能修改,這樣就減少了由於修改數據導致的錯誤。此外,由於對象不變,多任務環境下同時讀取對象不需要加鎖,同時讀一點問題都沒有。我們在編寫程序時,如果可以設計一個不變對象,那就儘量設計成不變對象。

3.可變參數

*nums表示把nums這個list的所有元素作爲可變參數傳進去

4.關鍵字參數

可變參數允許你傳入0個或任意個參數,這些可變參數在函數調用時自動組裝爲一個tuple。而關鍵字參數允許你傳入0個或任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝爲一個dict。請看示例:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

函數person除了必選參數nameage外,還接受關鍵字參數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>

創建Lg的區別僅在於最外層的[]()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循環的數據類型有以下幾種:

一類是集合數據類型,如listtupledictsetstr等;

一類是generator,包括生成器和帶yield的generator function。

這些可以直接作用於for循環的對象統稱爲可迭代對象:Iterable

可以使用isinstance()判斷一個對象是否是Iterable對象:

>>> from collections import Iterable
>>> isinstance([], Iterable)
True

可以被next()函數調用並不斷返回下一個值的對象稱爲迭代器:Iterator

生成器都是Iterator對象,但listdictstr雖然是Iterable,卻不是Iterator

listdictstrIterable變成Iterator可以使用iter()函數:

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

你可能會問,爲什麼listdictstr等數據類型不是Iterator

這是因爲Python的Iterator對象表示的是一個數據流,Iterator對象可以被next()函數調用並不斷返回下一個數據,直到沒有數據時拋出StopIteration錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()函數實現按需計算下一個數據,所以Iterator的計算是惰性的,只有在需要返回下一個數據時它纔會計算。

Iterator甚至可以表示一個無限大的數據流,例如全體自然數。而使用list是永遠不可能存儲全體自然數的。

小結

凡是可作用於for循環的對象都是Iterable類型;

凡是可作用於next()函數的對象都是Iterator類型,它們表示一個惰性計算的序列;

集合數據類型如listdictstr等是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是關鍵字,它類似於return,只是函數會返回一個生成器。

爲了完全弄懂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這種數據類型應該被視爲一個對象,這個對象擁有namescore這兩個屬性(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作爲參數的函數或者方法都可以不加修改地正常運行,原因就在於多態。

多態的好處就是,當我們需要傳入DogCatTortoise……時,我們只需要接收Animal類型就可以了,因爲DogCatTortoise……都是Animal類型,然後,按照Animal類型進行操作即可。由於Animal類型有run()方法,因此,傳入的任意類型,只要是Animal類或者子類,就會自動調用實際類型的run()方法,這就是多態的意思:

對於一個變量,我們只需要知道它是Animal類型,無需確切地知道它的子類型,就可以放心地調用run()方法,而具體調用的run()方法是作用在AnimalDogCat還是Tortoise對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,不管細節,而當我們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的“開閉”原則:

對擴展開放:允許新增Animal子類;

對修改封閉:不需要修改依賴Animal類型的run_twice()等函數。

繼承還可以一級一級地繼承下來,就好比從爺爺到爸爸、再到兒子這樣的關係。而任何類,最終都可以追溯到根類object,這些繼承關係看上去就像一顆倒着的樹。


鴨子類型

對於靜態語言(例如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協議。



發佈了35 篇原創文章 · 獲贊 16 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章