鍍金池/ 教程/ Python/ 函數(shù)
附錄
進程通信
操作系統(tǒng)
迭代器
模塊
描述符
裝飾器
第三部分 擴展庫
內(nèi)置類型
數(shù)據(jù)存儲
數(shù)據(jù)類型
基本環(huán)境
文件與目錄
異常
程序框架
數(shù)學運算
函數(shù)
元類
字符串
表達式

函數(shù)

當編譯器遇到 def,會生成創(chuàng)建函數(shù)對象指令。也就是說 def 是執(zhí)行指令,而不僅僅是個語法關(guān)鍵字??梢栽谌魏蔚胤絼討B(tài)創(chuàng)建函數(shù)對象。

一個完整的函數(shù)對象由函數(shù)和代碼兩部分組成。其中,PyCodeObject 包含了字節(jié)碼等執(zhí)行數(shù)據(jù),而 PyFunctionObject 則為其提供了狀態(tài)信息。

函數(shù)聲明:

def name([arg,... arg = value,... *arg, **kwarg]):
    suite

結(jié)構(gòu)定義:

typedef struct {
    PyObject_HEAD
    PyObject *func_code;   // PyCodeObject
    PyObject *func_globals;  // 所在模塊的全局名字空間
    PyObject *func_defaults;  // 參數(shù)默認值列表
    PyObject *func_closure;  // 閉包列表
    PyObject *func_doc;   // __doc__
    PyObject *func_name;   // __name__
    PyObject *func_dict;   // __dict__
    PyObject *func_weakreflist;  // 弱引用鏈表
    PyObject *func_module;  // 所在 Module
} PyFunctionObject;

創(chuàng)建

包括函數(shù)在內(nèi)的所有對象都是第一類對象,可作為其他函數(shù)的實參或返回值。

  • 在名字空間中,名字是唯一主鍵。因此函數(shù)在同一范圍內(nèi)不能 "重載 (overload)"。
  • 函數(shù)總是有返回值。就算沒有 return,默認也會返回 None。
  • 支持遞歸調(diào)用,但不進行尾遞歸優(yōu)化。最大深度 sys.getrecursionlimit()。
>>> def test(name):
...     if name == "a":
...         def a(): pass
...         return a
...     else:
...         def b(): pass
...         return b

>>> test("a").__name__
'a'

不同于用 def 定義復(fù)雜函數(shù),lambda 只能是有返回值的簡單的表達式。使用賦值語句會引發(fā)語法錯誤,可以考慮用函數(shù)代替。

>>> add = lambda x, y = 0: x + y

>>> add(1, 2)
3

>>> add(3)   # 默認參數(shù)
3

>>> map(lambda x: x % 2 and None or x, range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

參數(shù)

函數(shù)的傳參方式靈活多變,可按位置順序傳參,也可不關(guān)心順序用命名實參。

>>> def test(a, b):
...     print a, b

>>> test(1, "a")   # 位置參數(shù)
1 a

>>> test(b = "x", a = 100) # 命名參數(shù)
100 x

支持參數(shù)默認值。不過要小心,默認值對象在創(chuàng)建函數(shù)時生成,所有調(diào)用都使用同一對象。如果該默認值是可變類型,那么就如同 C 靜態(tài)局部變量。

>>> def test(x, ints = []):
... ints.append(x)
... return ints

>>> test(1)
[1]

>>> test(2)    # 保持了上次調(diào)用狀態(tài)。
[1, 2]

>>> test(1, [])   # 顯式提供實參,不使用默認值。
[1]

>>> test(3)    # 再次使用默認值。
[1, 2, 3]

默認參數(shù)后面不能有其他位置參數(shù),除非是變參。

>>> def test(a, b = 0, c): pass
SyntaxError: non-default argument follows default argument

>>> def test(a, b = 0, *args, **kwargs): pass

用 *args 收集 "多余" 的位置參數(shù),**kwargs 收集 "額外" 的命名參數(shù)。這兩個名字只是慣例,可自由命名。

>>> def test(a, b, *args, **kwargs):
...     print a, b
...     print args
...     print kwargs

>>> test(1, 2, "a", "b", "c", x = 100, y = 200)
1 2
('a', 'b', 'c')
{'y': 200, 'x': 100}

變參只能放在所有參數(shù)定義的尾部,且 **kwargs 必須是最后一個。

>>> def test(*args, **kwargs):   # 可以接收任意參數(shù)的函數(shù)。
...     print args
...     print kwargs

>>> test(1, "a", x = "x", y = "y")  # 位置參數(shù),命名參數(shù)。
(1, 'a')
{'y': 'y', 'x': 'x'}

>>> test(1)      # 僅傳位置參數(shù)。
(1,)
{}

>>> test(x = "x")     # 僅傳命名參數(shù)。
()
{'x': 'x'}

可 "展開" 序列類型和字典,將全部元素當做多個實參使用。如不展開的話,那僅是單個實參對象。

>>> def test(a, b, *args, **kwargs):
...     print a, b
...     print args
...     print kwargs

>>> test(*range(1, 5), **{"x": "Hello", "y": "World"})
1 2
(3, 4)
{'y': 'World', 'x': 'Hello'}

單個 "*" 展開序列類型,或者僅是字典的主鍵列表。"**" 展開字典鍵值對。但如果沒有變參收集,展開后多余的參數(shù)將引發(fā)異常。

>>> def test(a, b):
...     print a
...     print b

>>> d = dict(a = 1, b = 2)

>>> test(*d)    # 僅展開 keys(),test("a"、"b")。
a
b

>>> test(**d)    # 展開 items(),test(a = 1, b = 2)。
1
2

>>> d = dict(a = 1, b = 2, c = 3)

>>> test(*d)    # 因為沒有位置變參收集多余的 "c",導(dǎo)致出錯。
TypeError: test() takes exactly 2 arguments (3 given)

>>> test(**d)    # 因為沒有命名變參收集多余的 "c = 3",導(dǎo)致出錯。
TypeError: test() got an unexpected keyword argument 'c'

lambda 同樣支持默認值和變參,使用方法完全一致。

>>> test = lambda a, b = 0, *args, **kwargs:   \
...     sum([a, b] + list(args) + kwargs.values())

>>> test(1, *[2, 3, 4], **{"x": 5, "y": 6})
21

作用域

函數(shù)形參和內(nèi)部變量都存儲在 locals 名字空間中。

>>> def test(a, *args, **kwargs):
...     s = "Hello, World"
...     print locals()

>>> test(1, "a", "b", x = 10, y = "hi")
{
    'a': 1,
    'args': ('a', 'b'),
    'kwargs': {'y': 'hi', 'x': 10}
    's': 'Hello, World',
}

除非使用 global、nonlocal 特別聲明,否則在函數(shù)內(nèi)部使用賦值語句,總是在 locals 名字空間中新建一個對象關(guān)聯(lián)。注意:"賦值" 是指名字指向新的對象,而非通過名字改變對象狀態(tài)。

>>> x = 10

>>> hex(id(x))
'0x7fb8e04105e0'

>>> def test():
...     x = "hi"
...     print hex(id(x)), x

>>> test()    # 兩個 x 指向不同的對象。
0x10af2b490 hi

>>> x     # 外部變量沒有被修改。
10

如果僅僅是引用外部變量,那么按 LEGB 順序在不同作用域查找該名字。

名字查找順序: locals -> enclosing function -> globals -> __builtins__
  • locals: 函數(shù)內(nèi)部名字空間,包括局部變量和形參。
  • enclosing function: 外部嵌套函數(shù)的名字空間。
  • globals: 函數(shù)定義所在模塊的名字空間。
  • builtins: 內(nèi)置模塊的名字空間。 想想看,如果將對象引入 builtins 名字空間,那么就可以在任何模塊中直接訪問,如同內(nèi)置函數(shù)那樣。不過鑒于 builtins 的特殊性,這似乎不是個好主意。
>>> __builtins__.b = "builtins"

>>> g = "globals"

>>> def enclose():
...     e = "enclosing"
...     def test():
...         l = "locals"
...         print l
...         print e
...         print g
...         print b
...
...         return test

>>> t = enclose()

>>> t()
locals
enclosing
globals
builtins

通常內(nèi)置模塊 builtin 在本地名字空間的名字是 builtins (多了個 s 結(jié)尾)。但要記住這說法一點也不靠譜,某些時候它又會莫名其妙地指向 builtin.dict。如實在要操作該模塊,建議顯式 import builtin。

27.3. __builtin__ — Built-in objects
CPython implementation detail: Most modules have the name __builtins__ (note the 's') made available as partof their globals. The value of __builtins__ is normally either this module or the value of this modules’s __dict__attribute. Since this is an implementation detail, it may not be used by alternate implementations of Python.

現(xiàn)在,獲取外部空間的名字沒問題了,但如果想將外部名字關(guān)聯(lián)到一個新對象,就需要使用 global關(guān)鍵字,指明要修改的是 globals 名字空間。Python 3 還提供了 nonlocal 關(guān)鍵字,用來修改外部嵌套函數(shù)名字空間,可惜 2.7 沒有。

>>> x = 100

>>> hex(id(x))
0x7f9a9264a028

>>> def test():
...     global x, y  # 聲明 x, y 是 globals 名字空間中的。
...     x = 1000   # globals()["x"] = 1000
...     y = "Hello, World" # globals()["y"] = "..."。 新建名字。
...     print hex(id(x))

>>> test()    # 可以看到 test.x 引用的是外部變量 x。
0x7fdfba4abb30

>>> print x, hex(id(x))  # x 被修改。外部 x 指向新整數(shù)對象 1000。
1000 0x7fdfba4abb30

>>> x, y    # globals 名字空間中出現(xiàn)了 y。
(1000, 'Hello, World')

沒有 nonlocal 終歸有點不太方便,要實現(xiàn)類似功能稍微有點麻煩。

>>> from ctypes import pythonapi, py_object
>>> from sys import _getframe

>>> def nonlocal(**kwargs):
...     f = _getframe(2)
...     ns = f.f_locals
...     ns.update(kwargs)
...     pythonapi.PyFrame_LocalsToFast(py_object(f), 0)

>>> def enclose():
...     x = 10
...
...     def test():
...         nonlocal(x = 1000)
...
...     test()
...     print x

>>> enclose()
1000

這種實現(xiàn)通過 _getframe() 來獲取外部函數(shù)堆棧幀名字空間,存在一些限制。因為拿到是調(diào)用者,而不一定是函數(shù)創(chuàng)建者。

需要注意,名字作用域是在編譯時確定的。比如下面例子的結(jié)果,會和設(shè)想的有很大差異。究其原因,是編譯時并不存在 locals x 這個名字。

>>> def test():
...     locals()["x"] = 10
...     print x

>>> test()
NameError: global name 'x' is not defined

要解決這個問題,可動態(tài)訪問名字,或使用 exec 語句,解釋器會做動態(tài)化處理。

>>> def test():
...     exec ""    # 空語句。
...     locals()["x"] = 10
...     print x

>>> test()
10

>>> def test():
...     exec "x = 10"   # exec 默認使用當前名字空間。
...     print x

>>> test()
10

如果函數(shù)中包含 exec 語句,編譯器生成的名字指令會依照 LEGB 規(guī)則搜索。繼續(xù)看下面的例子。

>>> x = "abc"

>>> def test():
...     print x
...     exec "x = 10"
...     print x

>>> test()
abc
10

解釋器會將 locals 名字復(fù)制到 FAST 區(qū)域來優(yōu)化訪問速度,因此直接修改 locals 名字空間并不會影響該區(qū)域。解決方法還是用 exec。

>>> def test():
...     x = 10
...
...     locals()["x"] = 100 # 該操作不會影響 FAST 區(qū)域,只不過指向一個新對象。
...     print x   # 使用 LOAD_FAST 訪問 FAST 區(qū)域名字,依然是原對象。
...
...     exec "x = 100"  # 同時刷新 locals 和 FAST。
...     print x

>>> test()
10
100

另外,編譯期作用域不受執(zhí)行期條件影響。

>>> def test():
...     if False:
...         global x  # 盡管此語句永不執(zhí)行,但編譯器依然會將 x 當做 globals 名字。
...     x = 10
...     print globals()["x"] is x

>>> test()
True
>>> x
10

>>> def test():
...     if False:
...         x = 10   # 同理,x 是 locals 名字。后面出錯也就很正常了。
...     print x

>>> test()
UnboundLocalError: local variable 'x' referenced before assignment

其中細節(jié),可以用 dis 反編譯查看生成的字節(jié)指令。

閉包

閉包是指:當函數(shù)離開創(chuàng)建環(huán)境后,依然持有其上下文狀態(tài)。比如下面的 a 和 b,在離開 test 函數(shù)后,依然持有 test.x 對象。

>>> def test():
...     x = [1, 2]
...     print hex(id(x))
...
...     def a():
...         x.append(3)
...         print hex(id(x))
...
...     def b():
...         print hex(id(x)), x
...
...     return a, b

>>> a, b = test()
0x109b925a8     # test.x

>>> a()
0x109b925a8     # 指向 test.x

>>> b()
0x109b925a8 [1, 2, 3]

實現(xiàn)方式很簡單,以上例來解釋:

test 在創(chuàng)建 a 和 b 時,將它們所引用的外部對象 x 添加到 func_closure 列表中。因為 x 引用計數(shù)增加了,所以就算 test 堆棧幀沒有了,x 對象也不會被回收。

>>> a.func_closure
(<cell at 0x109e0aef8: list object at 0x109b925a8>,)

>>> b.func_closure
(<cell at 0x109e0aef8: list object at 0x109b925a8>,)

為什么用 function.func_closure,而不是堆棧幀的名字空間呢?那是因為 test 僅僅返回兩個函數(shù)對象,并沒有調(diào)用它們,自然不可能為它們創(chuàng)建堆棧幀。這樣一來,就導(dǎo)致每次返回的 a 和 b 都是新建對象,否則這個閉包狀態(tài)就被覆蓋了。

>>> def test(x):
...     def a():
...         print x
...
...     print hex(id(a))
...     return a

>>> a1 = test(100)    # 每次創(chuàng)建 a 都提供不同的參數(shù)。
0x109c700c8

>>> a2 = test("hi")   # 可以看到兩次返回的函數(shù)對象并不相同。
0x109c79f50

>>> a1()     # a1 的狀態(tài)沒有被 a2 破壞。
100

>>> a2()
hi

>>> a1.func_closure   # a1、a2 持有的閉包列表是不同的。
(<cell at 0x109e0cf30: int object at 0x7f9a92410ce0>,)

>>> a2.func_closure
(<cell at 0x109d3ead0: str object at 0x109614490>,)

>>> a1.func_code is a2.func_code # 這個很好理解,字節(jié)碼沒必要有多個。
True

通過 func_code,可以獲知閉包所引用的外部名字。

  • co_cellvars: 被內(nèi)部函數(shù)引用的名字列表。
  • co_freevars: 當前函數(shù)引用外部的名字列表。
>>> test.func_code.co_cellvars  # 被內(nèi)部函數(shù) a 引用的名字。
('x',)

>>> a.func_code.co_freevars  # a 引用外部函數(shù) test 中的名字。
('x',)

使用閉包,還需注意 "延遲獲取" 現(xiàn)象??聪旅娴睦樱?/p>

>>> def test():
...     for i in range(3):
...         def a():
...             print i
...     yield a

>>> a, b, c = test()

>>> a(), b(), c()
2
2
2

為啥輸出的都是 2 呢?

首先,test 只是返回函數(shù)對象,并沒有執(zhí)行。其次,test 完成 for 循環(huán)時,i 已經(jīng)等于 2,所以執(zhí)行 a、b、c 時,它們所持有 i 自然也就等于 2。

堆棧幀

Python 堆棧幀基本上就是對 x86 的模擬,用指針對應(yīng) BP、SP、IP 寄存器。堆棧幀成員包括函數(shù)執(zhí)行所需的名字空間、調(diào)用堆棧鏈表、異常狀態(tài)等。

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back; // 調(diào)用堆棧 (Call Stack) 鏈表
    PyCodeObject *f_code; // PyCodeObject
    PyObject *f_builtins; // builtins 名字空間
    PyObject *f_globals;  // globals 名字空間
    PyObject *f_locals;  // locals 名字空間
    PyObject **f_valuestack; // 和 f_stacktop 共同維護運行幀空間,相當于 BP 寄存器。
    PyObject **f_stacktop; // 運行棧頂,相當于 SP 寄存器的作用。
    PyObject *f_trace;  // Trace function

    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; // 記錄當前棧幀的異常信息

    PyThreadState *f_tstate; // 所在線程狀態(tài)
    int f_lasti;   // 上一條字節(jié)碼指令在 f_code 中的偏移量,類似 IP 寄存器。
    int f_lineno;   // 與當前字節(jié)碼指令對應(yīng)的源碼行號

    ... ...

    PyObject *f_localsplus[1];  // 動態(tài)申請的一段內(nèi)存,用來模擬 x86 堆棧幀所在內(nèi)存段。
} PyFrameObject;

可使用 sys._getframe(0) 或 inspect.currentframe() 獲取當前堆棧幀。其中 _getframe() 深度參數(shù)為 0 表示當前函數(shù),1 表示調(diào)用堆棧的上個函數(shù)。除用于調(diào)試外,還可利用堆棧幀做些有意思的事情。

權(quán)限管理

通過調(diào)用堆棧檢查函數(shù) Caller,以實現(xiàn)權(quán)限管理。

>>> def save():
...     f = _getframe(1)
...     if not f.f_code.co_name.endswith("_logic"): # 檢查 Caller 名字,限制調(diào)用者身份。
...         raise Exception("Error")   # 還可以檢查更多信息。
...     print "ok"

>>> def test(): save()
>>> def test_logic(): save()

>>> test()
Exception: Error

>>> test_logic()
ok

上下文

通過調(diào)用堆棧,我們可以隱式向整個執(zhí)行流程傳遞上下文對象。 inspect.stack 比 frame.f_back更方便一些。

>>> import inspect

>>> def get_context():
...     for f in inspect.stack():   # 循環(huán)調(diào)用堆棧列表。
...         context = f[0].f_locals.get("context") # 查看該堆棧幀名字空間中是否有目標。
...         if context: return context   # 找到了就返回,并終止查找循環(huán)。

>>> def controller():
...     context = "ContextObject"   # 將 context 添加到 locals 名字空間。
...     model()

>>> def model():
...     print get_context()    # 通過調(diào)用堆棧查找 context。

>>> controller()      # 測試通過。
ContextObject

sys._current_frames 返回所有線程的當前堆棧幀對象。

虛擬機會緩存 200 個堆棧幀復(fù)用對象,以獲得更好的執(zhí)行性能。整個程序跑下來,天知道要創(chuàng)建多少個這類對象。

包裝

用 functools.partial() 可以將函數(shù)包裝成更簡潔的版本。

>>> from functools import partial

>>> def test(a, b, c):
...     print a, b, c

>>> f = partial(test, b = 2, c = 3) # 為后續(xù)參數(shù)提供命名默認值。
>>> f(1)
1 2 3

>>> f = partial(test, 1, c = 3)  # 為前面的位置參數(shù)和后面的命名參數(shù)提供默認值。
>>> f(2)
1 2 3

partial 會按下面的規(guī)則合并參數(shù)。

def partial(func, *d_args, **d_kwargs):

    def wrap(*args, **kwargs):
        new_args = d_args + args  # 合并位置參數(shù),partial 提供的默認值優(yōu)先。
        new_kwargs = d_kwargs.copy()  # 合并命名參數(shù),partial 提供的會被覆蓋。
        new_kwargs.update(kwargs)

        return func(*new_args, **new_kwargs)
    return wrap

與函數(shù)相關(guān)內(nèi)容很多,涉及虛擬機底層實現(xiàn)。還要分清函數(shù)和對象方法的差別,后面會詳細說明。

上一篇:模塊下一篇:操作系統(tǒng)