[Python]函數與函數編程

1. 函數

使用def語句可定義函數:

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

函數體就是在調用函數時所執行的一系列語句。調用函數的方法是在函數名稱後面加上參數。參數的順序必須與函數定義匹配,否則會引發TypeError異常。可以爲函數的參數設置默認值,例如:

def split(line, delimiter=','):
    statements

如果給最後一個參數名加上星號"*",函數就可以接受任意數量的參數:

def fprintf(file, fmt, *args):
    file.write(fmt % args)
fprintf(out, "%d %s %f", 42, "hello world", 3.45)

在這個例子中,所有餘下的參數都作爲一個元組放入args變量。要把元組args當作參數傳遞給函數,可以在函數調用中使用*args語法。例如:

def printf(fmt, *args):
    fprintf(sys.stdout, fmt, *args)

提供函數參數還有一種方式,即顯示地命名每個參數併爲其指定一個值,這稱爲關鍵字參數,例如:

def foo(w, x, y, z):
    statements
foo(x=3, y=22, w='hello', z=[1, 2])

使用關鍵字參數時,參數的順序無關緊要。但除非提供了默認值,否則必須顯式地命名所有必需的函數參數。位置參數和關鍵字參數可以同時使用,前提是所有位置參數必須先出現,給所有非可選參數提供值,例如:

foo('hello', 3, z=[1, 2], y=22)

如果函數定義的最後一個參數以"**"開頭,可以把所有額外的關鍵字參數都放入一個字典中,並把這個字典傳遞給參數。例如:

def make_table(data, **params):
    fgcolor = params.pop("fgcolor", "black")
    bgcolor = params.pop("bgcolor", "white")
    width = params.pop("width", None)
    if params:
        raise TypeError("Unsupported configuration options %s" % list(params))
make_table(items, fgcolor="black", bgcolor="white", border=1, borderstyle="grooved", cellpoadding=10, width=400)

關鍵字參數和可變長度參數列表可以一起使用,只要"**"參數出現在最後即可,例如:

def spam(*args, **kwargs):
    statements

 

2. 參數傳遞與返回值

調用函數時,函數參數僅僅是引用傳入對象的名稱。參數傳遞的基本語義和其他編程語言中已知的方式不完全相同,如“按值傳遞”和“按引用傳遞”。比如傳遞不可變的值,參數看起來實際是按值傳遞的,如果傳遞的是可變對象(如列表或字典)給函數,然後再修改此可變對象,這些改動將反映在原始對象中。例如:

a = [1, 2, 3, 4, 5]
def square(items):
    for i, x in enumerate(items):
        items[i] = x * x
square(a) # a = [1, 4, 9, 16, 25]

return語句從函數返回一個值。如果沒有指定任何值或者省略return語句,就會返回None對象。如果返回值有多個,可以把它們放在一個元組中,例如:

def factor(a):
    d = 2
    while (d <= (a / 2)):
        if ((a / d) * d == a):
            return ((a / d), d)
        d = d + 1
    return (a, 1)

 

3. 作用域規則

每次執行一個函數時,就會創建新的局部命名空間。該命名空間代表一個局部環境,其中包含函數參數的名稱和在函數體內賦值的變量名稱。解析這些名稱時,解釋器將首先搜索局部命名空間。如果沒有找到匹配的名稱,它就會搜索全局命名空間。如果在全局命名空間中也找不到匹配值,最終會檢查內置命名空間。如果仍然找不到,就會引發NameError異常。
命名空間的特性之一是在函數中對全局變量的操作,例如:

a = 42
def foo():
    a = 13
foo() # a仍然是42

執行這段代碼時,儘量在函數foo中修改了變量a的值,但最終a仍然是42.在函數中對變量進行賦值時,這些變量始終綁定到該函數的局部命名空間中,因此函數體中的變量a引用的是一個包含值13的全新對象,而不是外部的變量。使用global語句可以改變這種行爲,例如:

a = 42
def foo():
    global a
    a  = 13
foo() # a的值已變13

Python支持嵌套的函數定義,例如:

def countdown(start):
    n = start
    def display():
        print('T-minus %d' % n)
    while n > 0:
        display()
        n -= 1

使用靜態作用域綁定嵌套函數中的變量,即解析名稱時首先檢查局部作用域,而後由內向外一層層檢查外部嵌套函數定義的作用域。如果找不到匹配,最後將搜索全局命名空間和內置命名空間。可以使用nonlocal語句綁定外部變量,例如:

def countdown(start):
    n = start
    def display():
        print('T-minus %d' % n)
    def decrement():
        nonlocal n
        n -= 1
    while n > 0:
        display()
        decrement()

nonlocal聲明不會把名稱綁定到任意函數中定義的局部變量,而是搜索當前調用棧中的下一層函數定義,即動態作用域。例如:

i = 0
def foo():
    i = i + 1 # UnboundLocalError異常

儘管有一個全局變量i,但它不會給局部變量i提供值。函數定義時就確定了變量是局部的還是全局的,而且在函數中不能突然改變它們的作用域。

 

4. 函數對象與閉包

函數在Python中是第一類對象。即可以把它們當作參數傳遞給其他函數,放在數據結構中,以及作爲函數的返回結果。例如:

def callf(func):
    return func()

把函數當作數據處理時,它將顯式地攜帶與定義該函數的周圍環境相關的信息。這將影響到函數中自由變量的綁定方式。例如:

# foo.py
x = 42
def callf(func):
    return func()

# main.py
import foo
x = 37
def helloworld():
    reutrn "x is %d" % x
foo.callf(helloworld) # x is 37

在上例中,即使foo.py中也定義了一個變量x,變際調用的是與helloworld()函數相同的環境中定義的值。將組成函數的語句和這些語句的執行環境打包在一起時,得到的對象稱爲閉包。事實上所有函數都擁有一個指向了定義該函數的全局命名空間的__globals__屬性。例如:

def page(url):
    def get():
        return urlopen(url).read()
    return get
python = page("http://www.python.org")
jython = page("http://www.jython.org")
pydata = python() # 獲取http://www.python.org
jydata = jython() # 獲取http://www.jython.org

 

5. 裝飾器

裝飾器是一個函數,其主要用途是包裝另一個函數或類。這種包裝的首要目的是透明地修改或增強被包裝對象的行爲。表示裝飾器的語法是特殊符號"@",例如:

@trace
def square(x):
    return x * x

上面的代碼可以簡化爲:

def square(x):
    return x * x
square = trace(square)

現在考慮trace的實現:

enable_tracing =  True
if enable_tracing:
    debug_log = open("debug.log", "w")

def trace(func):
    if enable_tracing:
        def callf(*args, **kwargs):
            debug_log.write("Calling %s: %s, %s\n" % (func.__name__, args, kwargs))
            r = func(*args, **kwargs)
            debug_log.write("%s returned %s\n" % (func.__name__, r))
            return r
        return callf
    else:
        return func

這段代碼中,trace()創建了寫有一些調試輸出的包裝器函數,然後調用了原始函數對象。因此如果調用square()函數,看到的將是包裝器中write()方法的輸出。
使用裝飾器時,它們必須出現在函數或類定義之前的單獨行上。可以同時使用多個裝飾器,例如:

@foo
@bar
@spam
def grok(x):
    pass\
grok = foo(bar(spam(grok)))

裝飾器也可以接受參數,例如:

@eventhandler('BUTTON')
def handle_button(msg):
    ...
@eventhandler('RESET')
def handle_reset(msg):
    ...

如果提供參數,裝飾器的語義如下所示:

def handle_button(msg):
    ...
temp = eventhandler('BUTTON')
handle_button = temp(handle_button)

對於類裝飾器,應該讓裝飾器函數始終返回類對象作爲結果。需要使用原始類定義的代碼可能要直接引用類成員。
 

6. 生成器與yield

函數使用yield關鍵字可以定義生成器對象。生成器是一個函數,它生成一個值的序列,以便在迭代中使用,例如:

def countdown(n):
    while n > 0:
        yield n
        n -=1
    return

如果調用該函數,其中的代碼不會開始執行,它會返回一個生成器對象,該對象在_next_()被調用,例如:

c = countdown(10)
c.__next__()

調用_next_()時,生成器函數將不斷執行語句,直到遇到yield語句爲止。通常不會在生成器上直接調用_next_()方法,而是在for語句、sum()或一些使用序列的其他操作中使用,例如:

for n in countdown(10):
    statements
a = sum(countdown(10))

生成器函數完成的標誌是返回或引發StopIteration異常,這標誌着迭代的結束。如果生成器沒有全部完成,並且不再使用,可以調用close()方法,雖然通常情況下可以不必調用,例如:

c = countdown(10)
c.__next__()
c.close()
c.__next__() # 拋出異常

在生成器函數內部,在yield語句上出現GeneratorExit異常時就會調用close()方法。可以選擇獲取這個異常,例如:

def countdown(n):
    try:
        while n > 0:
            yield n
            n -= 1
    except GeneratorExit:
        print("Only made it to %d" % n)

 

7. 協程與yield表達式

在函數內,yield語句還可以用作出現在賦值運算符右邊的表達式,例如:

def receiver():
    while True:
        n = (yield)
        print("Got %s" % n)

以這種方式使用yield語句的函數稱爲協程,它的執行是爲了響應發送給它的值。它的行爲也類似於生成器,例如:

r = receiver()
r.__next__()
r.send(1)
r.send(2)

在協程中需要首先調用_next_()這件事很容易被忘記,可以用一個自動完成該步驟的裝飾器來包裝協程,例如:

def coroutine(func):
    def start(*args, **kwargs):
        g = func(*args, **kwargs)
        g.next()
        return g
    return start

@coroutine
def receiver():
    while True:
        n = (yield)
        print("Got %s" % n)

r = receiver()
r.send("Hello World")

協程的運行一般是無限期的,除非它被顯式關閉或者自己退出。使用close()可以關閉輸入值的流,例如:

r.close()
r.send() # 拋出異常

關閉後如果繼續給協程發送值,就會引發StopIteration異常,close()操作將在協程內部引發GeneratorExit異常。
 

8. 列表包含

函數的常用操作是將函數應用給一個列表的所有項,並使用結果創建一個新列表。這種操作很常見,因此出現了叫做列表推導的運算符,例如:

nums = [1, 2, 3, 4, 5]
squares = [n * n for n in nums]

列表推導的一般語法如下:

[expression for item1 in iterable1 if condition1
                                        for item2 in iterable2 if condition2
                                        ...
                                        for itemN in iterableN if conditionN]

下面給出一些例子:

a = [-3, 5, 2, -10, 7, 8]
b = 'abc'
c = [2 * s for s in a] # c = [-6, 10, 4, -20, 14, 16]
d = [s for s in a if s >= 0] # d = [5, 2, 7, 8]
e= [(x, y) for x in a
                                for y in b
                                if x > 0] 
# e = [(5, 'a'), (5, 'b'), (5, 'c'),
                    (2, 'a'), (2, 'b'), (2, 'c'),
                    (7, 'a'), (7, 'b'), (7, 'c'),
                    (8, 'a'), (8, 'b'), (8, 'c')]
f = [(1, 2), (3, 4), (5, 6)]
g  = [math.sqrt(x * x + y * y) for x, y in f] # g = [2.23606797749979, 5.0, 7.810249675906654]

 

9. 生成器表達式

生成器表達式是一個對象,它執行的計算與列表包含相同,但會迭代地生成結果,語法與列表包含相同,除了用圓括號代替方括號,如下:

(expression for item1 in iterable1 if condition1
                                        for item2 in iterable2 if condition2
                                        ...
                                        for itemN in iterableN if conditionN)

生成器表達式實際上不創建列表或者立即對圓括號內的表達式求值,它創建一個通過迭代並按照需要生成值的生成器對象,例如:

a  = [1, 2, 3, 4]
b = (10 * i for i in a)
print(b.__next__())
print(b.__next__())

使用列表推導時,Python實際上創建了包含結果數據的列表。而使用生成器表達式時,Python創建的是隻知道如何按照需要生成數據的生成器。在某些應用中,可能影響性能和內存使用,例如:

f = open("data.txt")
lines = (t.strip() for t in f)
comments = (t for t in lines if t[0] == '#')
for c in comments:
    print(c)

生成器表達式不會創建序列形式的對象,不能對它進行索引。但是,使用內置的list()函數可以將生成器表達式轉換爲列表,例如:

clist = list(comments)

 

10. lambda運算符

使用lambda語句可以創建表達式形式的匿名函數:

lambda args: expression

args是以逗號分隔的參數列表,而expression是用到這些參數的表達式,例如:

a = lambda x, y: x + y
r = a(2, 3)

使用lambda語句定義的代碼必須是合法的表達式。lambda語句中不能出現多條語句和其他非表達式語句,比如for或while。
 

11. 文檔字符串

通常,函數的第一條語句會使用文檔字符串,用於描述函數的用途,例如:

def factorial(n):
    """Computes n factorial. For examples:
            >>> factorial(6)
            120
    """
    if n <= 1: return 1
    else: return n* factorial(n-1)

文檔字符串保存在函數的__doc__屬性中,IDE通常使用該函數提供交互式幫助。如果需要使用裝飾器,可能會破壞與文檔字符串相關的幫助功能,例如:

def wrap(func):
    call(*args, **kwargs):
        return func(*args, **kwargs)
    return call
@wrap
def factorial(n):
    """Computes n factorial."""

如果查目的地以上函數的幫助,可能會看到一個相當奇怪的內容,解決方法是編寫可以傳遞函數名稱和文檔字符串的裝飾器函數,例如:

def wrap(func):
    call(*args, **kwargs):
        return func(*args, **kwargs)
    call.__doc__ =  func.__doc__
    call.__name__ = func.__name__
    return call

因爲這是一個常見問題,所以functools模塊提供了函數wraps,用於自動複製這些屬性,例如:

from functools import wraps
def wrap(func):
    @wrap(func)
    call(*args, **kwargs):
        return func(*args, **kwargs)
    return call

 

12. 函數屬性

可以給函數添加任意屬性,例如:

def foo():
    statements
foo.secure = 1
foo.private = 1

函數屬性保存在函數的__dict__屬性中,__dic__屬性是一個字典。和文檔字符串一樣,也要注意混合使用函數屬性和裝飾器的問題。如果使用裝飾器包裝函數,實際上是由裝飾器函數而非原始函數來訪問屬性。
 

13. eval()、exec()和compile()函數

eval(str [, globals [, locals]])函數執行一個表達式字符串並返回結果,例如:

a = eval('3 * math.sin(3.5 + x) + 7.2')

相似地,exec(str [, globals [, locals]])函數執行一個包含任意Python代碼的字符串。例如:

a = [3, 5, 10, 13]
exec("for i in a: print(i)")

這兩個函數都會在調用者的命名空間中執行。eval()和exec()函數可以接受一個或兩個可選的映射對象,分別用作代碼執行的全局和局部命名空間,例如:

globals = {'x': 7, 'y': 10, 'birds': ['Parrot', 'Swallow', 'Albatross']}
locals = {}
a = eval("3 * x + 4 * y", globals, locals)
exec("fro b in birds: print(b)", globals, locals)

compile(str, filename, kind)函數將字符串編譯爲字節碼,其中str是包含要編譯代碼的字符串,而filename是定義該字符串的文件,kind參數指定了要編譯代碼的類型。single表示一條語句,exec代表一組語句,而eval代表一個表達式。例如:

s = "for i inrange(0, 10): print(i)"
c = compile(s, '', 'exec')
exec(c)
s2 = "3 * x + 4 * y"
c2 = compile(s2, '', 'eval')
result = eval(c2)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章