Python學習之路24-一等函數

《流暢的Python》筆記。
本篇主要講述Python中函數的進階內容。包括函數和對象的關係,函數內省,Python中的函數式編程。

1. 前言

本片首先介紹函數和對象的關係;隨後介紹函數和可調用對象的關係,以及函數內省。函數內省這部分會涉及不少與IDE和框架相關的東西,若是平時並不寫框架,能夠略過此部分。最後介紹函數式編程的相關概念,以及與之相關的兩個重要模塊:operator模塊和functools模塊。python

首先補充「一等對象」的概念。「一等對象」通常定義以下:編程

  • 在運行時建立;
  • 能賦值給變量或數據結構中的元素;
  • 能做爲參數傳給函數;
  • 能做爲函數的返回結果。

從上述定義能夠看出,Python中的函數符合上述四點,因此在Python中函數也被視做一等對象。bash

「把函數視做一等對象」簡稱爲「一等函數」,但這並非指有一類函數是「一等函數」,在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函數也是。app

你們或多或少見過mapfilterreduce三個函數,這三個就是高階函數,在過去很經常使用,但如今它們都有了替代品:框架

  • Python3中,mapfilter依然是內置函數,但因爲有了列表推導和生成器表達式,這兩個函數已不經常使用;
  • Python3中,reduce已不是內置函數,它被放到了functools模塊中。它常被用於求和,但如今求和最好用內置的sum函數。

sumreduce這樣的函數叫作歸約函數,它們的思想是將某個操做連續應用到一系列數據上,累計以前的結果,最後獲得一個值,即將一系列元素歸約成一個值。ssh

內置的歸約函數還有allany編程語言

  • all(iterable):若是iterable中每一個值都爲真,則返回Trueall([])返回True
  • any(iterable):若是iterable中有至少一個元素爲真,則返回Trueany([])返回False

2.3 匿名函數

lambda關鍵字在Python表達式內建立匿名函數,但在Python中,匿名函數內不能賦值,也不能使用whiletry等語句。但它和def語句同樣,實際建立了函數對象。

若是使用lambda表達式致使一段代碼難以理解,最好仍是將其轉換成用def語句定義的函數。

3. 可調用對象

函數其實一個可調用對象,它實現了__call__方法。Python數據模型文檔列出了7種可調用對象:

  • 用於定義的函數:使用def語句或lambda表達式建立;
  • 內置函數:使用C語言(CPython)實現的函數,如lentime.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_KEYWORDVAR_POSITIONAL(任意數量參數,以一個*號開頭的那種參數),VAR_KEYWORD(任意數量的關鍵字參數,以**開頭的那種參數),KEYWORD_ONLY(命名關鍵字參數)和POSITIONAL_ONLY(Python句法不支持該類型)
  • annotationreturn_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表達式:itemgetterattrgetter。這兩個函數其實會自行構建函數。

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()

attrgetteritemgetter做用相似,它建立的函數根據名稱提取對象的屬性。若是傳入多個屬性名,它也會返回屬性名對應的值構成的元組。這裏要展現的是,若是參數名中包含句點.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 ~

相關文章
相關標籤/搜索