Python 一等函數


在 Python 中,函數是一等對象。編程語言理論家把“一等對象”定義為滿

足下述條件的程序實體:

  • 在運行時創建

  • 能賦值給變量或數據結構中的元素

  • 能作為參數傳給函數

  • 能作為函數的返回結果

把函數視作對象

  Python 函數是對象。這里我們創建了一個函數,然后調用它,讀取它的 __doc__ 屬性,並且確定函數對象本身是 function 類的實例。

 1 #創建一個函數,只有函數在調用的時候才會運行
 2 def factorial(n):
 3     '''returns n!'''
 4     return 1 if n < 2 else n * factorial(n-1)
 5 
 6 #factorial(42)是函數function的實例
 7 print(factorial(42))
 8 print(type(factorial))
 9 
10 #函數眾多屬性中的其中一個~
11 print(factorial.__doc__)

以上代碼執行的結果為:

1405006117752879898543142606244511569936384000000000
<class 'function'>
returns n!

  展示了函數對象的“一等”本性。我們可以把 factorial 函數賦值給變量 fact,然后通過變量名調用。我們還能把它作為參數傳給map 函數。map 函數返回一個可迭代對象,里面的元素是把第一個參數(一個函數)應用到第二個參數(一個可迭代對象,這里是range(11))中各個元素上得到的結果。

🌰 通過別的名稱使用函數,再把函數作為參數傳遞

 1 #創建一個函數,只有函數在調用的時候才會運行
 2 def factorial(n):
 3     '''returns n!'''
 4     return 1 if n < 2 else n * factorial(n-1)
 5 
 6 fact = factorial
 7 print(fact)
 8 
 9 map_fact = map(factorial, range(11))
10 print('map fact:', map_fact)
11 print(list(map_fact))

以上代碼執行的結果為:

<function factorial at 0x1007a2e18>
map fact: <map object at 0x101c452e8>
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

 

高階函數

  接受函數為參數,或者把函數作為結果返回的函數是高階函數(higherorderfunction)。map 函數就是一例。此外,內函數 sorted 也是:可選的 key 參數用於提供一個函數,它會應用到各個元素上進行排序。

舉個🌰 根據單詞的長度排序一個列表

>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted_fruits = sorted(fruits, key=len)
>>> print(sorted_fruits)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

根據反向拼寫給一個單詞列表排序

>>> def reverse(word):
...     return word[::-1]
... 
>>> reverse('testing')
'gnitset'
>>> reverse(fruits)
['banana', 'raspberry', 'cherry', 'apple', 'fig', 'strawberry']
>>> fruits
['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

 函數式語言通常會提供 map、filter 和 reduce 三個高階函數(有時使用不同的名稱)。在 Python 3 中,map 和 filter 還是內置函數,但是由於引入了列表推導和生成器表達式,它們變得沒那么重要了。列表推導或生成器表達式具有 map 和 filter 兩個函數的功能,而且更易於閱讀,如示例:

>>> list(map(lambda x:x*x, range(6)))
[0, 1, 4, 9, 16, 25]
>>> [x*x for x in range(6)]
[0, 1, 4, 9, 16, 25]
>>> list(map(lambda x:x*x, filter(lambda x:x>5, range(10))))
[36, 49, 64, 81]
>>> [x*x for x in range(10) if x >5]
[36, 49, 64, 81]

  使用 reduce 和 sum 計算 0~99 之和

>>> from functools import reduce
>>> reduce(lambda x,y: x+y, range(100))
4950
>>> sum(range(100))
4950

all 和 any 也是內置的歸約函數

all(iterable)

  如果 iterable 的每個元素都是真值,返回 True;all([]) 返回True

any(iterable)

  只要 iterable 中有元素是真值,就返回 True;any([]) 返回False

 

匿名函數

  lambda 關鍵字在 Python 表達式內創建匿名函數。然而,Python 簡單的句法限制了 lambda 函數的定義體只能使用純表達式。換句話說,lambda 函數的定義體中不能賦值,也不能使用 while和 try 等 Python 語句。

舉個🌰 使用 lambda 表達式反轉拼寫,然后依此給單詞列表排序

>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

如果使用 lambda 表達式導致一段代碼難以理解,Fredrik Lundh 建議像下面這樣重構

  1. 編寫注釋 ,說明lambda表達式的作用

  2. 研究一會兒注釋,並找出一個名稱來概括注釋

  3. 把lambda表達式轉成def語句,使用那個名稱來定義函數

  4. 刪除注釋

可調用對象

  除了用戶定義的函數,調用運算符(即 ())還可以應用到其他對象上。如果想判斷對象能否調用,可以使用內置的 callable() 函數。Python 數據模型文檔列出了 7 種可調用對象。

用戶定義的函數

  使用 def 語句或 lambda 表達式創建

內置函數

  使用 C 語言(CPython)實現的函數,如 len 或 time.strftime

內置方法

  使用 C 語言實現的方法,如 dict.get

方法 

  在類的定義體中定義的函數

  調用類時會運行類的 __new__ 方法創建一個實例,然后運行__init__ 方法,初始化實例,最后把實例返回給調用方。因為 Python沒有 new 運算符,所以調用類相當於調用函數。(通常,調用類會創建那個類的實例,不過覆蓋 __new__ 方法的話,也可能出現其他行為

類的實例

  如果類定義了 __call__ 方法,那么它的實例可以作為函數調用

生成器函數

  使用 yield 關鍵字的函數或方法。調用生成器函數返回的是生成器對象

舉個🌰 判斷對象是否可以被調用

>>> abs, str, 123
(<built-in function abs>, <class 'str'>, 123)
>>> [callable(s) for s in [abs, str, 123]]
[True, True, False]

 

用戶定義的可調用類型

  不僅 Python 函數是真正的對象,任何 Python 對象都可以表現得像函數。為此,只需實現實例方法 __call__。

 1 import random
 2 
 3 
 4 class BingoCage:
 5 
 6     def __init__(self, item):                   #接收一個可迭代的對象,轉成列表,以防止意外
 7         self._item = list(item)
 8         random.shuffle(self._item)
 9 
10     def pick(self):
11         try:
12             return self._item.pop()             #每調用一次會會從刪除一個值
13         except IndexError:                      #列表中沒有值會報索引錯誤
14             raise LookupError('pick from empty BingoCage')
15 
16     def __call__(self):
17         return self.pick()                      #支持函數作為實例調用
18 
19 
20 
21 bingo = BingoCage(range(3))
22 print(bingo.pick())               #普通的調用
23 print(bingo())                    #函數作為實例調用
24 print(callable(bingo))

以上代碼執行的結果為:

2
0
True

  實現 __call__ 方法的類是創建函數類對象的簡便方式,此時必須在內部維護一個狀態,讓它在調用之間可用,例如 BingoCage 中的剩余元素。裝飾器就是這樣。裝飾器必須是函數,而且有時要在多次調用之間“記住”某些事 [ 例如備忘(memoization),即緩存消耗大的計算結果,供后面使用 ]。

 

函數內省 

  除了 __doc__,函數對象還有很多屬性。使用 dir 函數可以探知factorial 具有下述屬性:

>>> def factorial():
...     pass
... 
>>> 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__']

  下面重點說明函數專有而用戶定義的一般對象沒有的屬性。計算兩個屬性集合的差集便能得到函數專有屬性列表

1 class C: pass               #聲明一個類
2 obj = C()                   #實例化類
3 
4 def func(): pass            #聲明一個函數對象
5 
6 print(sorted(set(dir(func)) - set(dir(obj))))   #打印函數和類實例化類對象中的差集

以上代碼執行得結果為:

['__annotations__', '__call__', '__closure__', '__code__', '__defaults__',

'__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

用戶定義的函數的屬性

名稱 類型 說明
__annotations__ dict 參數返回值和的注釋信息
__call__ method-wrapper 實現()運算符;即可調用對象協議
__closure__ tuple 函數閉包,即自有變量的綁定(通常是None)
__code__ code 編譯成字節碼的函數元數據和函數定義體
__defaults__ tuple 形式參數的默認值
__get__ method-wrapper 實現只讀描述符協議
__globals__ dict 函數所在模塊中的全局變量
__kwdefaults__ dict 僅限關鍵字形式參數的默認值
__name__ str 函數名稱
__qualname__ str 函數的限定名稱,如Random.choice

 

 

 

 

 

 

 

 從定位參數到僅限關鍵字參數 

  Python 最好的特性之一是提供了極為靈活的參數處理機制,而且 Python3 進一步提供了僅限關鍵字參數(keyword-only argument)。與之密切相關的是,調用函數時使用 * 和 **“展開”可迭代對象,映射到單個參數。

  tag 函數用於生成 HTML 標簽;使用名為 cls 的關鍵字參數傳入“class”屬性,這是一種變通方法,因為“ class”是 Python的關鍵字

 1 def tag(name, *content, cls=None, **attrs):
 2     if cls is not None:
 3         attrs['class'] = cls
 4     if attrs:
 5         attr_str = ''.join(' %s="%s"' % (attr, value)
 6                            for attr, value in sorted(attrs.items()))
 7     else:
 8         attr_str = ''
 9 
10     if content:
11         return '  '.join('<%s%s>%s</%s>' %
12                          (name, attr_str, c, name) for c in content)
13     else:
14         return '<%s%s />' % (name, attr_str)
15 
16 print(tag('br'))
17 
18 print('-'*20)
19 print('位置參數:\n', tag('p', 'hello'))
20 
21 print('-'*20)
22 print('位置參數,通過函數*接收到一個元祖:\n', tag('p', 'hello', 'world'))
23 
24 print('-'*20)
25 print('位置參數和關鍵字參數:\n', tag('p', 'hello', id=33))
26 
27 print('-'*20)
28 print('cls的關鍵字參數傳參數:\n', tag('p', 'hello', 'world', cls='sidebar'))
29 
30 print('-'*20)
31 print('兩個關鍵字參數,content傳遞給函數中的content,name傳遞給name:\n', tag(content='testing', name="img"))
32 
33 print('-'*20)
34 #在 my_tag 前面加上 **,字典中的所有元素作為單個參數傳入,同名鍵會綁定到對應的具名參數上,余下的則被 **attrs 捕獲
35 my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
36 print('傳遞一個字典進去,通過**拆分成關鍵詞參數:\n', my_tag)

以上代碼執行的結果為: 

<br />
--------------------
位置參數:
 <p>hello</p>
--------------------
位置參數,通過函數*接收到一個元祖:
 <p>hello</p>  <p>world</p>
--------------------
位置參數和關鍵字參數:
 <p id="33">hello</p>
--------------------
cls的關鍵字參數傳參數:
 <p class="sidebar">hello</p>  <p class="sidebar">world</p>
--------------------
兩個關鍵字參數,content傳遞給函數中的content,name傳遞給name:
 <img content="testing" />
--------------------
傳遞一個字典進去,通過**拆分成關鍵詞參數:
 {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}

注意:

  僅限關鍵字參數是 Python 3 新增的特性。在示例 ,cls 參數只能通過關鍵字參數指定,它一定不會捕獲未命名的定位參數。定義函數時若想指定僅限關鍵字參數,要把它們放到前面有 * 的參數后面。如果不想支持數量不定的定位參數,但是想支持僅限關鍵字參數,在簽名中放一個 *,如下所示:

>>> def f(a, *, b):
...     return a, b
... 
>>> f(1, b=2)
(1, 2)

注意,僅限關鍵字參數不一定要有默認值,可以像上例中 b 那樣,強制必須傳入實參

 

函數注解

  Python 3 提供了一種句法,用於為函數聲明中的參數和返回值附加元數據。

🌰 有注釋的clip函數

 1 def clip(text:str, max_len:'int > 0'=80) -> str:        #函數中給每個參數都設置了傳遞的參數的信息,包括函數最終的返回結果的類型
 2     """
 3     :param text:
 4     :param max_len:前面或者后面的第一個空格處截取文本
 5     :return:
 6     """
 7     end = None
 8     if len(text) > max_len:                            #獲取傳入字符串的長度並與傳遞的要查找空格最大長度做判斷
 9         space_before = text.rfind(' ', 0, max_len)     #從開頭查找到max_len的長度截止,如果能找到則返回空格在字符串中的位置,否則則返回-1
10         if space_before >= 0:
11             end = space_before                         #查找空格的索引位置等於找到最終函數的切片的最終位置
12         else:
13             space_after = text.rfind(' ', max_len)     #如果從開頭沒有找到空格,則從max_len的長度開始往后繼續查找
14             if space_after >= 0:
15                 end = space_after
16 
17     if end is None:
18         end = len(text)
19 
20     return text[:end].rsplit()                         #返回從查找截止到空格位置的所有字符串
21 
22 
23 s = 'testing testing'
24 result = clip(s, 10)
25 print(result)

  函數聲明中的各個參數可以在 : 之后增加注解表達式。如果參數有默認值,注解放在參數名和 = 號之間。如果想注解返回值,在 ) 和函數聲明末尾的 : 之間添加 -> 和一個表達式。那個表達式可以是任何類型。注解中最常用的類型是類(如 str 或 int)和字符串(如 'int >0')

 注解不會做任何處理,只是存儲在函數的 __annotations__ 屬性(一個字典)中:

>>> from clip_annot import clip
>>> clip.__annotations__
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}

  'return' 鍵保存的是返回值注解,即示例中函數聲明里以 -> 標記的部分。

  Python 對注解所做的唯一的事情是,把它們存儲在函數的__annotations__ 屬性里。僅此而已,Python 不做檢查、不做強制、不做驗證,什么操作都不做。換句話說,注解對 Python 解釋器沒有任何意義。注解只是元數據,可以供 IDE、框架和裝飾器等工具使用。標准庫中還沒有什么會用到這些元數據,唯有inspect.signature() 函數知道怎么提取注解,如示例所示。

 1 from inspect import signature
 2 
 3 
 4 def clip(text:str, max_len:'int > 0'=80) -> str:        #函數中給每個參數都設置了傳遞的參數的信息,包括函數最終的返回結果的類型
 5     """
 6     :param text:
 7     :param max_len:前面或者后面的第一個空格處截取文本
 8     :return:
 9     """
10     end = None
11     if len(text) > max_len:                            #獲取傳入字符串的長度並與傳遞的要查找空格最大長度做判斷
12         space_before = text.rfind(' ', 0, max_len)     #從開頭查找到max_len的長度截止,如果能找到則返回空格在字符串中的位置,否則則返回-1
13         if space_before >= 0:
14             end = space_before                         #查找空格的索引位置等於找到最終函數的切片的最終位置
15         else:
16             space_after = text.rfind(' ', max_len)     #如果從開頭沒有找到空格,則從max_len的長度開始往后繼續查找
17             if space_after >= 0:
18                 end = space_after
19 
20     if end is None:
21         end = len(text)
22 
23     return text[:end].rsplit()                         #返回從查找截止到空格位置的所有字符串
24 
25 sig = signature(clip)
26 print(sig.return_annotation)
27 
28 for param in sig.parameters.values():
29     note = repr(param.annotation).ljust(13)
30     print(note, ':', param.name, '=', param.default)

以上代碼執行的結果為:

<class 'str'>
<class 'str'> : text = <class 'inspect._empty'>
'int > 0'     : max_len = 80

  signature 函數返回一個 Signature 對象,它有一個return_annotation 屬性和一個 parameters 屬性,后者是一個字典,把參數名映射到 Parameter 對象上。每個 Parameter 對象自己也有 annotation 屬性。

 

支持函數式編程的包

operator模塊

  在函數式編程中,經常需要把算術運算符當作函數使用。例如,不使用遞歸計算階乘。求和可以使用 sum 函數,但是求積則沒有這樣的函數。我們可以使用 reduce 函數,但是需要一個函數計算序列中兩個元素之積。展示如何使用 lambda 表達式解決這個問題

>>> from functools import reduce
>>> reduce(lambda x,y:x*y, range(1,11))
3628800

  operator 模塊為多個算術運算符提供了對應的函數,從而避免編寫lambda x, y: x*y 這種平凡的匿名函數

>>> from operator import mul
>>> from functools import reduce
>>> reduce(mul, range(1, 11))
3628800

  operator 模塊中還有一類函數,能替代從序列中取出元素或讀取對象屬性的 lambda 表達式:因此,itemgetter 和 attrgetter 其實會自行構建函數。

🌰 演示使用itemgetter排序一個元祖列表

>>> from operator import itemgetter
>>> 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)),
...     ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
...     ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
...     ]
>>> for city in sorted(metro_data, key=itemgetter(1)):
...     print(city)
... 
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))

  上面代碼中的itemgetter(1)等同於lambda fields:fields[1]的效果,也就是每個元素中的第一個索引進行排序

 如果把多個參數傳給 itemgetter,它構建的函數會返回提取的值構成的元組:

>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
...     print(cc_name(city))
... 
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')

  attrgetter 與 itemgetter 作用類似,它創建的函數根據名稱提取對象的屬性。如果把多個屬性名傳給 attrgetter,它也會返回提取的值構成的元組。此外,如果參數名中包含 .(點號),attrgetter 會深入嵌套對象,獲取指定的屬性。這些行為如示例 5-24 所示。這個控制台會話不短,因為我們要構建一個嵌套結構,這樣才能展示attrgetter 如何處理包含點號的屬性名。

🌰  定義一個 namedtuple,名為 metro_data,演示使用 attrgetter 處理它

 1 from collections import namedtuple
 2 from operator import attrgetter
 3 
 4 
 5 LatLong = namedtuple('LatLong', 'lat long')
 6 Metropolis = namedtuple('Metropolis', 'name cc pop coord')
 7 metro_data = [
 8     ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
 9     ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
10     ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
11     ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
12     ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
13 ]
14 
15 metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long))
16                for name, cc, pop, (lat, long) in metro_data]
17 
18 print(metro_areas[0])
19 print(metro_areas[0].coord.lat)
20 
21 name_lat = attrgetter('name', 'coord.lat')          #等同於調用Metropolis中的name和coord字段。coord字段對應LatLong,然后在去LatLong中的lat
22 
23 for city in sorted(metro_areas, key=attrgetter('coord.lat')):
24     print(name_lat(city)) #等同於Metropolis中獲取每一個name的字段和coord.lat的字段

以上代碼執行的結果為:

Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))
35.689722
('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)

methodcaller 使用示例:

>>> from operator import methodcaller
>>> s = 'The time has come....'
>>> upcase = methodcaller('upper')
>>> upcase(s)
'THE TIME HAS COME....'
>>> hiphenate = methodcaller('replace', ' ', '-')
>>> hiphenate(s)
'The-time-has-come....'

使用functools.partial凍結參數

  functools.partial 這個高階函數用於部分應用一個函數。部分應用是指,基於一個函數創建一個新的可調用對象,把原函數的某些參數固定。使用這個函數可以把接受一個或多個參數的函數改編成需要回調的API,這樣參數更少。

🌰 使用 partial 把一個兩參數函數改編成需要單參數的可調用對象

from functools import partial
from operator import mul

triple = partial(mul, 3)
print(triple(7))
print(list(map(triple, range(1, 10))))

以上代碼執行的結果為:

21
[3, 6, 9, 12, 15, 18, 21, 24, 27]

在來個舉個簡單的 🌰

def sum(x, y):
    return x + y

s = partial(sum, 10)
print(s(20))

#使用lambda表達式實現
sum = lambda x,y:x+y
s1 = partial(sum, 30)
print(s1(50))

以上代碼執行的結果為:

30
80

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM