《Python核心編程》筆記(三)


今次的內容主要為format方法、函數、魔法參數、偏函數應用、面向對象編程、迭代器、新式類的高級特性和compile()等。原本預定要將生成器(yield)這部分的內容一併寫入,不過因為這部分內容難度較大,我覺得可能單獨寫成一篇文章比較好。另外,在今次的博文中,面向對象編程中的關於python的多重繼承機制,以及用戶如何自定義迭代器這兩個部分我會重點談一下。

今次博文寫完之後,基本上我的關於《Python核心編程》的筆記就到此結束了,之後關於python的技術心得文章應該會以專題的形式來進行撰寫,即一篇博文重點談一個問題,這樣檢索起來也比較方便。當然了,如果日後再遇到一些值得記錄下來的比較雜亂的小知識,我也會酌情考慮是否要將其作為《Python核心編程》筆記(四)來進行撰寫,畢竟python一些比較細節的語言特性還是值得我們注意的。

注:本書為第二版,其中python的代碼主要為2.x版本,使用python3.x的朋友請留意。


format方法

format方法主要用於構造字符串,個人認為這個方法在需要格式化輸出一些信息的時候用的比較多。這部分內容並不難理解,直接上代碼即可明白。

代碼示例:

1
2
3
4
age = 25
name = 'Swaroop'
print('{0} is {1} years old'.format(name,age))
print('Why is {0} playing with that python?'.format(name))

輸出:

1
2
Swaroop is 25 years old
Why is Swaroop playing with that python?

你可以注意到變量值到字符串的轉變是由format方法自動完成的,這就方便了我們輸出字符串而不用去糾結那些%s,%d之類的東西。大括號中的數字表示format方法中的參數的位置,0表示第一個參數,1表示第二個參數,以此類推,然後你在前面的字符串中指定是要輸出哪個變量的值即可。當然了,你也可以將大括號中的數字省略,這樣就意味著format會按照其參數的順序來為字符串中的大括號進行依次賦值的工作。

format方法的威力還不只如此,請閱讀下面的例子:

代碼示例:

1
2
3
4
5
6
>>> '{0:.9}'.format(1/3)
0.333333333
>>> '{0:_^11}'.format('hello')
'___hello___' #用下劃線將字符串'hello'填充至11個字符。
>>> '{name} wrote {book}'.format(name='Swaroop', book='A Byte of Python')
'Swaroop wrote A Byte of Python'

函數

python的函數”看上去”可以返回多個對象,如:

代碼示例:

1
2
def bar():
return 'abc', [42,'python'], "Guido"

這麼寫是合法的,不過事實上它返回的是一個元組,而並不是真正的多個對象。這裡只是省去了小括號而已。

函數屬性

函數是可以擁有屬性的:

代碼示例:

1
2
3
4
def bar():
pass
bar.__doc__ == 'fuck'
bar.version = 0.1

不過我們不能在函數定義的內部訪問這些屬性,因為在函數中沒有self這種東西,所以不能訪問的原因是因為函數體尚未創建。


魔法參數

這個特性是我目前最喜歡的一個python的語言特性,非常優雅,非常美妙,令人愛不釋手。這個特性主要用於需要定義能夠獲取任意數量個參數的函數的場合。舉例如下:

代碼示例:

1
2
3
4
5
6
7
8
9
def total(initial=5, *numbers, **keywords):
count = initial
for number in numbers:
count += number
for key in keywords:
count += keywords[key]
return count
  
print(total(10, 1, 2, 3, vegetables=50, fruits=100))

輸出的結果為166,不過166是怎麼計算出來的呢?請聽我娓娓道來:

當我們定義了一個帶一個星號的參數的時候,比如說這裡的numbers,就表示從該參數所在的位置開始到最後所有的除去關鍵字參數以外的參數會全部被收集到一個叫做numbers的列表中。在本例中,initial的值由默認的5變為了10,然後這之後的1,2,3全部被放進了名為numbers的列表中。同樣,當我們定義了一個帶兩個星號的參數的時候,比如說這裡的keywords,則表示從該參數所在的位置開始到最後的所有的關鍵字參數都會被收集到名為keywords的字典中。那麼在本例中,vegetables=50, fruits=100就被放進了這個字典中,其中vegetables和fruits為該字典中的兩個鍵,50和100為分別為它們的值。

需要特別注意的是,有默認值的參數必須處在沒有默認值參數的後面,否則解釋器會報錯。

代碼示例:

1
2
3
4
5
6
7
8
9
>>> def func(b=5,a):
... pass
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>> def func(a,b=5):
... pass
...
>>>

Keyword-only 參數

Keyword-only參數在我的理解中就是你在調用函數的時候,必須實名給出其值的參數。舉例如下:

代碼示例:

1
2
3
4
5
6
7
8
9
10
def total(initial=5, *numbers, vegetables):
count = initial
for number in numbers:
count += number
count += vegetables
return count

print(total(10, 1, 2, 3, vegetables=50))
print(total(10, 1, 2, 3,))
# Raises error because we have not supplied a default argument value for 'vegetables'

輸出:

1
2
3
4
5
6
$ python keyword_only.py
66
Traceback (most recent call last):
File "test.py", line 12, in <module>
print(total(10, 1, 2, 3))
TypeError: total() needs keyword-only argument vegetables

在帶星號的參數後面聲明參數,解釋器會認為其為Keyword-only參數。那麼如果該參數沒有默認值,而且你在調用的時候也沒有給他賦值,那麼解釋器就會報錯。如果你想使用Keyword-only參數,但又不需要帶有星號的參數,可以簡單地用一個星號來代替帶星號的參數,如:

1
2
def total(initial=5, *, vegetables):
pass

需要特別注意的是,哪怕你在調用的時候,參數的數目已經與函數的定義的參數數目相同,如果你沒有指明Keyword-only參數和其值,那麼一樣會報錯,也就是說:以上面那個函數為例,其參數數目為兩個,如果你這樣調用total(1,2)還是會報錯,解釋器會提醒你你給多了參數,而不會將這個2認為是Keyword-only參數的值。

調用帶有可變長參數對象的函數

這同樣是一個需要留意的特性,請注意如下代碼:

代碼示例:

1
2
3
4
5
aTuple = (6,7,8)
aDict = {'z':9}
def newfoo(arg1, arg2, *nkw, **kw):
pass #這裡略去不寫,該函數的功能就是簡單地將參數值按照不同類別來分別輸出。
newfoo(1, 2, 3, x = 4, y = 5, *aTuple, **aDict)

我們按照上述方法來調用該函數,其結果為:arg1 = 1,arg2 = 2,nkw列表中的值分別為:3,6,7,8,而kw字典中的鍵值對分別為:{‘z’:9, ‘x’:4, ‘y’:5}(這裡我感覺不需要在意列表和字典中數據存放的次序)


偏函數應用(Partial Function Application PFA)

這裡所指的是一類比較特殊的函數,即它們可以將帶有任意數量參數的函數轉化成另一些帶有剩餘參數的函數對象。某種意義上這和使用默認參數的情形相同,但在PFA的例子中,參數不需要調用函數默認值,而只需要明確要調用的集合。這樣一來對於一個函數來說,你可以擁有針對它的諸多偏函數調用,每個都能用不同的參數傳遞給原函數,然後進行調用。

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from operator import add, mul
>>> from functools import partial
>>> add1 = partial(add, 1) # add1(x) == add(1, x)
>>> mul100 = partial(mul, 100) # mul100(x) == mul(100, x)
>>>
>>> add1(10)
11
>>> add1(1)
2
>>> mul100(10)
1000
>>> mul100(500)
50000

add1函數的功能是對傳進來的參數的值執行加一操作,然後返回;mul100則是對其進行乘以100的操作,然後返回。你可以看到我們並不需要去實現這兩個函數,我們調用的是已經存在的函數,然而我們卻可以做到調用簡化,這就是PFA的威力。這種威力在我們需要調用那些帶有許多參數的函數的時候體現得最為明顯。這裡再舉一例:

代碼示例:

1
2
3
4
>>> baseTwo = partial(int, base=2)
>>> baseTwo.__doc__ = 'Convert base 2 string to an int.'
>>> baseTwo('10010')
18

以後我們想把二進制字符串轉換為十進制數字的時候,就可以簡單調用baseTwo函數了,而不需要每次都很繁瑣地調用int(‘10010’, 2)。不過你需要注意,如果你創建了不帶關鍵字參數的函數,比如baseTwo = partial(int, 2),這可能會讓參數以錯誤的順序傳入int(),因為固定參數總是放在運行時刻參數的左邊,即baseTwo == int(2, x),這時便會出現異常。


一個有趣的特性

代碼示例:

1
2
fuck = {'a':1, 'b':2}['a']
print(fuck) #輸出為1
1
2
3
4
5
6
def a(fuck):
def b():print('b')
def c():print('c')
suck = {'funcb':b, 'funcc':c}[fuck]
suck()
a('funcb') #輸出b

面向對象編程

面向對象編程大家應該都已經比較熟悉了,我這裡就著重只談一些我認為比較重要的地方。

  1. 如果需要,每個子類最好定義它自己的構造器,不然基類的構造器會被調用。然而若子類重寫了基類的構造器,則基類的構造器將不會被自動調用,而需要你手動去調用。

  2. 解構器–del–只有在當前類實例的所有引用都被清除掉之後才會被調用。注意:重寫–del–之前要記得調用父類的–del–,另外,調用del x不表示調用了x.–del–,而只是減少了x的引用計數。

  3. 要想更新類屬性,只能通過類名來訪問該屬性。若通過實例來訪問,會相當於創建了一個該實例的屬性,而不是修改類屬性。當然了,這裡針對的是那些不可變的類屬性,若為可變類屬性,則通過實例來訪問的話,會直接修改該類屬性。

  4. 若想在子類中調用父類的方法,推薦採用super方法。

代碼示例:

1
2
3
4
5
6
7
8
9
class P:
def foo(self):
print('Hi,I'm P-foo())
class C(P):
def foo(self):
super(C,self).foo()
print('Hi,I'm C-foo())
c = C()
c.foo()

輸出:

1
2
Hi,I'm P-foo()
Hi,I'm C-foo()

關於super(),其實這一塊的水挺深的,我這裡只是簡單地做一個介紹,詳細地可以看這裡:關於python中的super()用法研究

多重繼承

說道多重繼承,一個顯而易見的問題就是當子類調用其父類中的某個方法的時候,(假設其父類都有該方法)python解釋器是按照何種方式在繼承樹上進行遍歷的。在python2.x的版本中,類分為經典類(不繼承自object)和新式類(繼承自object)。不過在python3.x中,這兩者已經沒有區別,都是新式類。按照《核心編程》中的說法,經典類是採用深搜的方法來遍歷繼承樹的,而新式類則是採用廣搜的方法。這裡我們只舉新式類的例子:

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class P1: #(object):                     # 父类1
def foo(self):
print 'called P1-foo()'

class P2: #(object): # 父类2
def foo(self):
print 'called P2-foo()'
def bar(self):
print 'called P2-bar()'

class C1(P1, P2): #子类1,从P1,P2 派生
pass

class C2(P1, P2): #子类2,从P1,P2 派生
def bar(self):
print 'called C2-bar()'

class GC(C1, C2): #定义子孙类
pass #从C1,C2 派生

繼承樹示意圖如下:

然後我們來執行如下代碼:

1
2
3
4
5
>>> gc = GC()
>>> gc.foo() # GC ==> C1 ==> C2 ==> P1
called P1-foo()
>>> gc.bar() # GC ==> C1 ==> C2
called C2-bar()

我們可以清楚地看到,這裡採用的搜索算法為廣搜算法,即優先搜索在繼承樹中離自己最近的父類。


迭代器

什麼叫迭代器?我們定義了一個–iter–()方法的類就被叫做迭代器,這意味著我們可以自己創建自己的迭代器。我們先來看一個簡單的代碼:

代碼示例:

(randSeq.py)

1
2
3
4
5
6
7
8
9
10
11
from random import choice

class RandSeq(object):
def __init__(self, seq):
self.data = seq

def __iter__(self):
return self

def __next__(self):
return choice(self.data)
1
2
3
4
5
6
7
8
9
10
11
12
>>> from randseq import RandSeq
>>> for eachItem in RandSeq(
... ('rock', 'paper', 'scissors')):
... print (eachItem)
...
scissors
scissors
rock
paper
paper
scissors
:

這裡我們創建了一個隨機序列迭代器,它接受一個序列,然後隨機地返回其中的元素,需要注意的是這是一個無窮迭代,因為我們無損地讀取了一個序列,所以它是不會越界的。我們可以看到–iter–()僅返回self,這就是如何將一個對象聲明為迭代器的方式,最後,調用–next–方法來得到迭代器中連續的值。

我們在來看一個例子:

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyIterator(object):
def __init__(self,step):
self.step=step

def __next__(self):
if self.step==0:
raise StopIteration
self.step-=1
return self.step

def __iter__(self):
return self

for i in MyIterator(6):
print(i)

輸出:

1
2
3
4
5
6
7
8
9
>>> ================================ RESTART ================================
>>>
5
4
3
2
1
0
>>>

令這個迭代停止下來的方法就是捕獲StopIteration異常。


新式類的高級屬性

簡單的模塊級私有化只需要在屬性或函數名前加一個單下劃線字符即可,這樣就可以防止它們被from mymodule import *這樣的語句加載。

–slots–是一個類變量,由一序列型對象組成,由所有合法標識構成的實例屬性的集合來表示。它可以是一個列表、元組或可迭代對象,也可以是標識實例能擁有的唯一的屬性的簡單字符串。任何試圖創建一個其名不在–slots–中的名字的實例屬性都將導致AttributeError異常。

代碼示例:

1
2
3
4
5
6
7
8
class SlottedClass(object):
__slots__ = ('foo', 'bar')
>>> c = SlottedClass()
>>>
>>> c.foo = 42
>>> c.xxx = "don't think so" Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'SlottedClass' object has no attribute 'xxx'

這種特性的主要目的是節約內存。其副作用是某種類型的”安全”,它能防止用戶隨心所欲地動態增加實例屬性。帶__slots__屬性的類定義不會存在__dict__了(除非你在__slots__中增加’dict‘元素)。

python給類提供了名為–call–的特殊方法,該方法允許程序員創建可調用的對象(實例)。

代碼示例:

1
2
3
4
5
class C(object):
def __call__(self,*args):
print('args:\n',args)
c = C()
c(3) #輸出(3, )

callable()為一個布爾函數,用於判斷一個對象是否是可調用的。可調用返回True,否則返回False。


compile()

compile()允許程序員在運行時刻迅速生成代碼對象,然後就可以用exec()或者內建函數eval()來執行這些對象或者對它們進行求值了。一個很重要的地方是:exec()和eval()都可以執行字符串格式的python代碼。但是exec()更加強大并更具有技巧性。eval()函數只能執行單獨一條表達式,但是exec()能够執行多條語句,導入(import),函數聲明,甚至整個Python程序的字符串表示也可以。

compile函數的三個參數都是必需的,第一個參數代表要編譯的python代碼(字符串),第二個參數代表存放代碼對象的文件的名字(字符串)。雖為必需,但通常被置為空字符串。第三個參數也是一個字符串,用來表明代碼對象的類型,它有以下三種可能取值:

  • ‘eval’ 可求值表達式,和eval()一起使用
  • ‘single’ 單一可執行語句,和exec()一起使用
  • ‘exec’ 可執行語句組,和exec()一起使用

下面分別給出這三種情況的例子:

可求值表達式

1
2
3
>>> eval_code = compile('100 + 200', '', 'eval')
>>> eval(eval_code)
300

單一可執行語句

1
2
3
>>> single_code = compile('print("hello world!")', '', 'single')
>>> exec(single_code)
hello world!

可執行語句組

1
2
3
4
5
6
7
8
9
10
11
>>> exec_code = compile("""req = int(input('Count how many numbers?'))
for eachNum in range(req):
print(eachNum)""", '', 'exec')
>>> exec(exec_code)
Count how many numbers?6
0
1
2
3
4
5

需要注意的小知識

在python3中,對於input(),用戶輸入什麼它就返回什麼,即只是簡單地將其作為字符串返回;而對於eval(input()),則會將用戶輸入的東西作為表達式進行求值,然後返回其結果。

代碼示例:

1
2
3
4
5
6
7
8
>>> a = eval(input())
1+2
>>> print(a)
3
>>> a = input()
1+2
>>> print(a)
1+2

《Python核心編程》筆記(三)

https://justuno.github.io/2014/03/21/python-course-3/

作者

Justuno

发布于

2014-03-21

更新于

2016-02-09

许可协议

评论