《流暢的Python》筆記。
本篇主要講述Python中函式的進階內容。包括函式和物件的關係,函式內省,Python中的函數語言程式設計。
1. 前言
本片首先介紹函式和物件的關係;隨後介紹函式和可呼叫物件的關係,以及函式內省。函式內省這部分會涉及很多與IDE和框架相關的東西,如果平時並不寫框架,可以略過此部分。最後介紹函數語言程式設計的相關概念,以及與之相關的兩個重要模組:operator模組和functools模組。
首先補充“一等物件”的概念。“一等物件”一般定義如下:
- 在執行時建立;
- 能賦值給變數或資料結構中的元素;
- 能作為引數傳給函式;
- 能作為函式的返回結果。
從上述定義可以看出,Python中的函式符合上述四點,所以在Python中函式也被視作一等物件。
“把函式視作一等物件”簡稱為“一等函式”,但這並不是指有一類函式是“一等函式”,在Python中所有函式都是一等函式!
2. 函式
2.1 函式是物件
為了表明Python中函式就是物件,我們可以使用type()
函式來判斷函式的型別,並且訪問函式的__doc__
屬性,同時我們還將函式賦值給一個變數,並且將函式作為引數傳入另一個函式:
def factorial(n):
"""return n!"""
return 1 if n < 2 else n * factorial(n - 1)
# 在Python控制檯中,help(factorial)也會訪問函式的__doc__屬性。
print(factorial.__doc__)
print(type(factorial))
# 把函式賦值給一個變數
fact = factorial
print(fact)
fact(5)
# 把函式傳遞給另一個函式
print(list(map(fact, range(11))))
# 結果:
return n!
<class 'function'>
<function factorial at 0x000002421033C2F0>
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
複製程式碼
從上述結果可以看出,__doc__
屬性儲存了函式的文件字串,而type()
的結果說明函式其實是function
類的一個例項。將函式賦值給一個變數和將函式作為引數傳遞給另一個函式則體現了“一等物件”的特性。
2.2 高階函式
接收函式作為引數,或者把函式作為結果返回的函式叫做高階函式(higher-order function),上述的map
函式就是高階函式,還有我們常用的sorted
函式也是。
大家或多或少見過map
,filter
和reduce
三個函式,這三個就是高階函式,在過去很常用,但現在它們都有了替代品:
- Python3中,
map
和filter
依然是內建函式,但由於有了列表推導和生成器表示式,這兩個函式已不常用; - Python3中,
reduce
已不是內建函式,它被放到了functools模組中。它常被用於求和,但現在求和最好用內建的sum
函式。
sum
和reduce
這樣的函式叫做歸約函式,它們的思想是將某個操作連續應用到一系列資料上,累計之前的結果,最後得到一個值,即將一系列元素歸約成一個值。
內建的歸約函式還有all
和any
:
all(iterable)
:如果iterable
中每個值都為真,則返回True
;all([])
返回True
;any(iterable)
:如果iterable
中有至少一個元素為真,則返回True
;any([])
返回False
。
2.3 匿名函式
lambda
關鍵字在Python表示式內建立匿名函式,但在Python中,匿名函式內不能賦值,也不能使用while
,try
等語句。但它和def
語句一樣,實際建立了函式物件。
如果使用lambda
表示式導致一段程式碼難以理解,最好還是將其轉換成用def
語句定義的函式。
3. 可呼叫物件
函式其實一個可呼叫物件,它實現了__call__
方法。Python資料模型文件列出了7種可呼叫物件:
- 用於定義的函式:使用
def
語句或lambda
表示式建立; - 內建函式:使用C語言(CPython)實現的函式,如
len
或time.strftime
; - 內建方法:使用C語言實現的方法,如
dict.get
; - 方法:在類的定義體中定義的函式;
- 類:呼叫類時(也就是例項化一個類時)會執行類的
__new__
方法建立一個例項,然後執行__init__
方法初始化例項,最後把例項返回給呼叫方。因為Python沒有new
運算子,所以呼叫類相當於呼叫函式; - 類的例項:如果類實現了
__call__
方法,那麼它的例項可以作為函式呼叫; - 生成器函式:使用
yield
關鍵字的函式或方法。呼叫生成器函式返回的是生成器物件。
3.1 使用者定義的可呼叫型別
任何Python物件都可以表現得像函式,只要實現__call__
方法。
class SayHello:
def sayhello(self):
print("Hello!")
def __call__(self):
self.sayhello()
say = SayHello()
say.sayhello()
say()
print(callable(say))
# 結果:
Hello!
Hello!
True
複製程式碼
實現__call__
方法的類是建立函式類物件的簡便方式。有時這些類必須在內部維護一些狀態,讓它在呼叫之間可用,比如裝飾器。裝飾器必須是函式,而且有時還要在多次呼叫之間儲存一些資料。
3.2 函式內省
以下內容在編寫框架和IDE時用的比較多。
筆者之前偶有見到”內省“,但一直不明白”內省“這個詞究竟是什麼意思。“自我反省”?其實在程式設計中,這個詞的意思就是:讓程式碼自動確定某一段程式碼能幹什麼。如果以函式舉例,就是函式A自動確定函式B是什麼,包含哪些資訊,能幹什麼。不過在講Python函式的內省之前,先來看看函式都有哪些屬性和方法。
3.2.1 函式的屬性和方法
dir
函式可以檢測一個引數所含有的屬性和方法。我們可以用該函式檢視一個函式所包含的屬性和方法:
>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__',
'__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__',
'__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__',
'__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__',
'__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__']
複製程式碼
其中大多數屬性是Python物件共有的。函式獨有的屬性如下:
>>> class C:pass
>>> obj = C()
>>> def func():pass
>>> sorted(set(dir(func)) - set(dir(obj)))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__',
'__globals__', '__kwdefaults__', '__name__', '__qualname__']
複製程式碼
3.2.2 __dict__屬性
與使用者定義的常規類一樣,函式使用__dict__
屬性儲存使用者賦予它的屬性。這相當於一種基本形式的註解。
這裡可能有人覺得彆扭:之前都是給變數或者物件賦予屬性,現在是給函式或者方法賦予屬性。不過正如前面說的,Python中函式就是物件。
一般來說,為函式賦予屬性不是個常見的做法,但Django框架就有這樣的行為:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = "Customer name" # 給方法賦予了一個屬性
複製程式碼
3.2.3 獲取關於引數的資訊
從這裡開始就是函式內省的內容。在HTTP為框架Bobo中有個使用函式內省的例子,它以裝飾器的形式展示:
import bobo
@bobo.query("/")
def hello(person):
return "Hello %s!" % person
複製程式碼
通過裝飾器bobo.query
,Bobo會內省hello
函式:Bobo會發現這個hello
函式需要一個名為person
的引數,然後它就會從請求中獲取這個引數,並將這個引數傳給hello
函式。
有了這個裝飾器,我們就不用自己處理請求物件來獲取person
引數,Bobo框架幫我們自動完成了。
那這究竟是怎麼實現的呢?Bobo怎麼知道我們寫的函式需要哪些引數?它又是怎麼知道引數有沒有預設值呢?
這裡用到了函式物件特有的一些屬性(如果不瞭解引數型別,可以閱讀筆者的“Python學習之路7”中的相關內容):
__defaults__
的值是一個元組,儲存著關鍵字引數的預設值和位置引數;__kwdefaults__
儲存著命名關鍵字引數的預設值;__code__
屬性儲存引數的名稱,它的值是一個code
物件引用,自身也有很多屬性。
下面通過一個例子說明這些屬性的用途:
def func(a, b=10):
"""This is just a test"""
c = 20
if a > 10:
d = 30
else:
e = 30
print(func.__defaults__)
print(func.__code__)
print(func.__code__.co_varnames)
print(func.__code__.co_argcount)
# 結果:
(10,)
<code object func at 0x0000021651851DB0, file "mytest.py", line 1>
('a', 'b', 'c', 'd', 'e')
2
複製程式碼
可以看出,這種資訊的組織方式並不方便:
- 引數名在
__code__.co_varnames
中,它同時還儲存了函式定義體中的區域性變數,因此,只有前__code__.co_argcount
個元素是引數名(不包含字首為*
何**
的的變長引數); - 如果想將引數名和預設值對應上,只能從後向前掃描
__default__
屬性,比如上例中關鍵字引數b
的預設值10
。
不過,我們並不是第一個發現這種方式很不方便。已經有人為我們造好了輪子。
使用inspect模組簡化上述操作
>>> from mytest import func
>>> from inspect import signature
>>> sig = signature(func) # 返回一個inspect.Signature物件(簽名物件)
>>> sig
<Signature (a, b=10)>
>>> str(sig)
'(a, b=10)'
>>> for name, param in sig.parameters.items():
... print(param.kind, ":", name, "=",param.default)
...
POSITIONAL_OR_KEYWORD : a = <class 'inspect._empty'> # 表示沒有預設值
POSITIONAL_OR_KEYWORD : b = 10
複製程式碼
inspect.Signature
物件有一個屬性parameters
,該屬性是個有序對映,把引數名和inspect.Parameter
物件對應起來。inspect.Parameter
也有自己的屬性,如:
name
:引數的名稱;default
:引數的預設值;kind
:引數的型別,有5種,POSITIONAL_OR_KEYWORD
,VAR_POSITIONAL
(任意數量引數,以一個*號開頭的那種引數),VAR_KEYWORD
(任意數量的關鍵字引數,以**開頭的那種引數),KEYWORD_ONLY
(命名關鍵字引數)和POSITIONAL_ONLY
(Python句法不支援該型別)annotation
和return_annotation
:引數和返回值的註解,後面會講到。
inspect.Signature
物件有個bind
方法,它可把任意個引數繫結到Singature
中的形參上,框架可使用這個方法在真正呼叫函式前驗證引數是否正確。比如你自己寫的框架中的某函式A自動獲取使用者輸入的引數,並根據這些引數呼叫函式B,但在呼叫B之前,你想檢測下這些引數是否符合函式B對形參的要求,此時你就有可能用到這個bind
方法,看能不能將這些引數繫結到函式B上,如果能,則可認為能夠根據這些引數呼叫函式B:
>>> from mytest import func
>>> from inspect import signature
>>> sig = signature(func)
>>> my_tag = {"a":10, "b":20}
>>> bound_args = sig.bind(**my_tag)
>>> bound_args
<BoundArguments (a=10, b=20)>
>>> for name, value in bound_args.arguments.items():
... print(name, "=", value)
a = 10
b = 20
>>> del my_tag["a"]
>>> bound_args = sig.bind(**my_tag)
Traceback (most recent call last):
TypeError: missing a required argument: 'a'
複製程式碼
3.2.4 函式註解
Python3提供了一種句法,用於為函式宣告中的引數和返回值附加後設資料。如下:
# 未加註解
def func(a, b=10):
return a + b
# 新增註解
def func(a: int, b: 'int > 0' = 10) -> int:
return a + b
複製程式碼
各個引數可以在冒號後面增加註解表示式,如果有預設值,註解放在冒號和等號之間。上述-> int
是對返回值新增註解的形式。
這些註解都存放在函式的__annotations__
屬性中,它是一個字典:
print(func.__annotations__)
# 結果
# 'return'表示返回值
{'a': <class 'int'>, 'b': 'int > 0', 'return': <class 'int'>}
複製程式碼
Python只是將註解儲存在函式的__annotations__
屬性中,除此之外,再無任何操作。換句話說,這些註解對Python直譯器來說沒有意義。而這些註解的真正用途是提供給IDE、框架和裝飾器等工具使用,比如Mypy靜態型別檢測工具,它就會根據你寫的這些註解來檢測傳入的引數的型別是否符合要求。
inspect
模組可以獲取這些註解。inspect.Signature
有個一個return_annotation
屬性,它儲存返回值的註解;inspect.Parameter
物件中的annotation
屬性儲存了引數的註解。
函式內省的內容到此結束。後面將介紹標準庫中為函數語言程式設計提供支援的常用包。
4. 函數語言程式設計
Python並不是一個函數語言程式設計語言,但通過operator和functools等包的支援,也可以寫出函式式風格的程式碼。
4.1 operator模組
在函數語言程式設計中,經常需要把算術運算子當做函式使用,比如非遞迴求階乘,實現如下:
from functools import reduce
def fact(n):
return reduce(lambda a, b: a * b, range(1, n + 1))
複製程式碼
operator模組為多個算術運算子提供了對應的函式。使用算術運算子函式可將上述程式碼改寫如下:
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n + 1))
複製程式碼
operator模組中還有一類函式,能替代從序列中取出元素或讀取物件屬性的lambda
表示式:itemgetter
和attrgetter
。這兩個函式其實會自行構建函式。
4.1.1 itemgetter()
以下程式碼展示了itemgetter
的常見用途:
from operator import itemgetter
test_data = [
("A", 1, "Alpha"),
("B", 3, "Beta"),
("C", 2, "Coco"),
]
# 相當於 lambda fields: fields[1]
for temp in sorted(test_data, key=itemgetter(1)):
print(temp)
# 傳入多個引數時,它構建的函式返回下標對應的值構成的元組
part_tuple = itemgetter(1, 0)
for temp in test_data:
print(part_tuple(temp))
# 結果:
('A', 1, 'Alpha')
('C', 2, 'Coco')
('B', 3, 'Beta')
(1, 'A')
(3, 'B')
(2, 'C')
複製程式碼
itemgetter
內部使用[]
運算子,因此它不僅支援序列,還支援對映和任何實現了__getitem__
方法的類。
4.1.2 attrgetter()
attrgetter
和itemgetter
作用類似,它建立的函式根據名稱提取物件的屬性。如果傳入多個屬性名,它也會返回屬性名對應的值構成的元組。這裡要展示的是,如果引數名中包含句點.
,attrgetter
會深入巢狀物件,獲取指定的屬性:
from collections import namedtuple
from operator import attrgetter
metro_data = [
("Tokyo", "JP", 36.933, (35.689722, 139.691667)),
("Delhi NCR", "IN", 21.935, (28.613889, 77.208889)),
("Mexico City", "MX", 20.142, (19.433333, -99.133333)),
]
LatLong = namedtuple("LatLong", "lat long")
Metropolis = namedtuple("Metropolis", "name, cc, pop, coord")
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for
name, cc, pop, (lat, long) in metro_data]
# 返回新的元組,獲取name屬性和巢狀的coord.lat屬性
name_lat = attrgetter("name", "coord.lat")
for city in sorted(metro_areas, key=attrgetter("coord.lat")): # 巢狀
print(name_lat(city))
# 結果:
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
複製程式碼
4.1.3 methodcaller()
從名字也可看出,它建立的函式會在物件上呼叫引數指定的方法(注意是方法,而不是函式)。
>>> from operator import methodcaller
>>> s = "The time has come"
>>> upcase = methodcaller("upper")
>>> upcase(s) # 相當於s.upper()
'THE TIME HAS COME'
>>> hiphenate = methodcaller("replace"," ","-")
>>> hiphenate(s) # 相當於s.replace(" ", "-")
'The-time-has-come'
複製程式碼
從hiphenate
這個例子可以看出,methodcaller
還可以凍結某些引數,即部分應用(partial application),這與functools.partial
函式的作用類似。
4.2 使用functools.partial凍結引數
functool模組提供了一系列高階函式,reduce
函式相信大家已經很熟悉了,本節主要介紹其中兩個很有用的函式partial
和它的變體partialmethod
。
functools.partial
用到了一個“閉包”的概念,這個概念的詳細內容下一篇再介紹。使用這個函式可以把接收一個或多個引數的函式改編成需要回撥的API,這樣引數更少。
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3)
>>> triple(7)
21
>>> list(map(triple, range(1,10))) # 這裡無法直接使用mul函式
[3, 6, 9, 12, 15, 18, 21, 24, 27]
>>> triple.func # 訪問原函式
<built-in function mul>
>>> triple.args # 訪問固定引數
(3,)
>>> triple.keywords # 訪問關鍵字引數
{}
複製程式碼
functools.partialmethod
函式的作用於partial
一樣,只不過partialmethod
用於方法,partial
用於函式。
補充:回撥函式(callback function)可以簡單理解為,當一個函式X被傳遞給函式A時,函式X就被稱為回撥函式,函式A呼叫函式X的過程叫做回撥。
5. 總結
本篇首先介紹了函式,包括函式與物件的關係,高階函式和匿名函式,重點是函式就是物件;隨後介紹了函式和可呼叫物件的關係,以及函式的內省;最後,我們介紹了關於函數語言程式設計的概念以及與之相關的兩個重要模組。
迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~