《流暢的Python》筆記。
本篇主要講述Python中函數的進階內容。包括函數和對象的關係,函數內省,Python中的函數式編程。
本片首先介紹函數和對象的關係;隨後介紹函數和可調用對象的關係,以及函數內省。函數內省這部分會涉及不少與IDE和框架相關的東西,若是平時並不寫框架,能夠略過此部分。最後介紹函數式編程的相關概念,以及與之相關的兩個重要模塊:operator模塊和functools模塊。python
首先補充「一等對象」的概念。「一等對象」通常定義以下:編程
從上述定義能夠看出,Python中的函數符合上述四點,因此在Python中函數也被視做一等對象。bash
「把函數視做一等對象」簡稱爲「一等函數」,但這並非指有一類函數是「一等函數」,在Python中全部函數都是一等函數!微信
爲了代表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
類的一個實例。將函數賦值給一個變量和將函數做爲參數傳遞給另外一個函數則體現了「一等對象」的特性。閉包
接收函數做爲參數,或者把函數做爲結果返回的函數叫作高階函數(higher-order function),上述的map
函數就是高階函數,還有咱們經常使用的sorted
函數也是。app
你們或多或少見過map
,filter
和reduce
三個函數,這三個就是高階函數,在過去很經常使用,但如今它們都有了替代品:框架
map
和filter
依然是內置函數,但因爲有了列表推導和生成器表達式,這兩個函數已不經常使用;reduce
已不是內置函數,它被放到了functools模塊中。它常被用於求和,但如今求和最好用內置的sum
函數。sum
和reduce
這樣的函數叫作歸約函數,它們的思想是將某個操做連續應用到一系列數據上,累計以前的結果,最後獲得一個值,即將一系列元素歸約成一個值。ssh
內置的歸約函數還有all
和any
:編程語言
all(iterable)
:若是iterable
中每一個值都爲真,則返回True
;all([])
返回True
;any(iterable)
:若是iterable
中有至少一個元素爲真,則返回True
;any([])
返回False
。lambda
關鍵字在Python表達式內建立匿名函數,但在Python中,匿名函數內不能賦值,也不能使用while
,try
等語句。但它和def
語句同樣,實際建立了函數對象。
若是使用lambda
表達式致使一段代碼難以理解,最好仍是將其轉換成用def
語句定義的函數。
函數其實一個可調用對象,它實現了__call__
方法。Python數據模型文檔列出了7種可調用對象:
def
語句或lambda
表達式建立;len
或time.strftime
;dict.get
;__new__
方法建立一個實例,而後運行__init__
方法初始化實例,最後把實例返回給調用方。由於Python沒有new
運算符,因此調用類至關於調用函數;__call__
方法,那麼它的實例能夠做爲函數調用;yield
關鍵字的函數或方法。調用生成器函數返回的是生成器對象。任何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__
方法的類是建立函數類對象的簡便方式。有時這些類必須在內部維護一些狀態,讓它在調用之間可用,好比裝飾器。裝飾器必須是函數,並且有時還要在屢次調用之間保存一些數據。
如下內容在編寫框架和IDE時用的比較多。
筆者以前偶有見到」內省「,但一直不明白」內省「這個詞到底是什麼意思。「自我檢討」?其實在編程中,這個詞的意思就是:讓代碼自動肯定某一段代碼能幹什麼。若是以函數舉例,就是函數A自動肯定函數B是什麼,包含哪些信息,能幹什麼。不過在講Python函數的內省以前,先來看看函數都有哪些屬性和方法。
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__']
與用戶定義的常規類同樣,函數使用__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" # 給方法賦予了一個屬性
從這裏開始就是函數內省的內容。在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
。不過,咱們並非第一個發現這種方式很不方便。已經有人爲咱們造好了輪子。
>>> 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'
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
屬性保存了參數的註解。
函數內省的內容到此結束。後面將介紹標準庫中爲函數式編程提供支持的經常使用包。
Python並非一個函數式編程語言,但經過operator和functools等包的支持,也能夠寫出函數式風格的代碼。
在函數式編程中,常常須要把算術運算符當作函數使用,好比非遞歸求階乘,實現以下:
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
。這兩個函數其實會自行構建函數。
如下代碼展現了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__
方法的類。
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)
從名字也可看出,它建立的函數會在對象上調用參數指定的方法(注意是方法,而不是函數)。
>>> 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
函數的做用相似。
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的過程叫作回調。
本篇首先介紹了函數,包括函數與對象的關係,高階函數和匿名函數,重點是函數就是對象;隨後介紹了函數和可調用對象的關係,以及函數的內省;最後,咱們介紹了關於函數式編程的概念以及與之相關的兩個重要模塊。
迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~