Python面試題目之Python函數默認參數陷阱


請看如下一段程序:

def extend_list(v, li=[]):
    li.append(v)
    return li

list1 = extend_list(10)
list2 = extend_list(123, [])
list3 = extend_list('a')

print(list1)
print(list2)
print(list3)

print(list1 is list3)

請先猜想打印的結果:

是不是這樣:

[10]
[123]
[a]
False

 

但是,實際的打印效果

 

 請看如下解釋:

<!-- lang: python -->
# 函數的定義相當於一次類型構造,默認值只在此時解析一次。
# 而函數調用時不會重新執行默認參數的構造。所以,如果使用了字典,列表這樣的可變類型。
# 而又要在函數體內修改它,可能會出現意想不到的效果.
def a(b=[]):
    b.append('hi')
    print b

In [11]: a()
['hi']
In [12]: a()
['hi', 'hi']
In [13]: a(['2'])
['2', 'hi']
In [14]: a()
['hi', 'hi', 'hi']
In [15]: a.func_defaults
Out[15]: (['hi', 'hi', 'hi'],)

# 解決方法:參數默認值使用None賦值
def(b = None):
    b = b or []
    pass

# 類屬性也有類似問題
class A(object):
    x = []
    def __init__(self, c): 
        self.x.append(c)    # 這里的x搜索到類級別的x了而非實例的,
                            # 因實例級別的x未事先定義
In [36]: a1, a2 = A(1), A(2)
In [37]: a1.x, a2.x
Out[37]: ([1, 2], [1, 2])

# 解決方法, 實例級別的屬性事先定義
class B(object):
    x = []
    def __init__(self, c): 
        self.x = []     # 此處實例屬性有x,所以先搜索到此
        self.x.append(c) 

In [38]: b1, b2 = B(1), B(2)
In [39]: b1.x, b2.x
Out[39]: ([1], [2])

 

 

python可變對象做默認參數陷阱

可變對象與不可變對象

python中,萬物皆對象。python中不存在所謂的傳值調用,一切傳遞的都是對象的引用,也可以認為是傳址。

python中,對象分為可變(mutable)和不可變(immutable)兩種類型。

元組(tuple)、數值型(number)、字符串(string)均為不可變對象,而字典型(dictionary)和列表型(list)的對象是可變對象。

對於可變對象來說,傳址是可以改變原對象的值的,對於不可變對象來說,傳址相當於多了一個指向該值(不可變)的指針

不可變對象

 

可變對象

 

 

函數默認參數陷阱

下面這一段程序

#! /usr/bin/env python
# -*- coding: utf-8 -*-

class demo_list:
    def __init__(self, l=[]):
        self.l = l

    def add(self, ele):
        self.l.append(ele)

def appender(ele):
    obj = demo_list()
    obj.add(ele)
    print obj.l

if __name__ == "__main__":
    for i in range(5):
        appender(i)

 

輸出結果是多少?

[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]

而不是想象的

[0]
[1]
[2]
[3]
[4]

而如果想達到第二種效果,只需將obj = demo_list() 改為obj = demo_list(l=[]) 即可

默認參數原理

官方文檔中的一句話:

Default values are computed once, then re-used.

默認值是被重復使用的

Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call.

所以當默認參數值是可變對象的時候,那么每次使用該默認參數的時候,其實更改的是同一個變量

當python執行def語句時,它會根據編譯好的函數體字節碼和命名空間等信息新建一個函數對象,並且會計算默認參數的值。函數的所有構成要素均可通過它的屬性來訪問,比如可以用funcname屬性來查看函數的名稱。所有默認參數值則存儲在函數對象的defaults_屬性中,它的值為一個列表,列表中每一個元素均為一個默認參數的值

其中默認參數相當於函數的一個屬性

Functions in Python are first-class objects, and not only a piece of code.

我們可以這樣解讀:函數也是對象,因此定義的時候就被執行,默認參數是函數的屬性,它的值可能會隨着函數被調用而改變。其他對象不都是如此嗎?

避免

使用可變參數作為默認值可能導致意料之外的行為。為了防止出現這種情況,最好使用None值,並且在后面加上檢查代碼

def __init__(self, l=None):
       if not l:
            self.l = []
       else:
            self.l = l

 

在這里將None用作占位符來控制參數l的默認值。不過,有時候參數值可能是任意對象(包括None),這時候就不能將None作為占位符。你可以定義一個object對象作為占位符,如下面例子:

sentinel = object()

def func(var=sentinel):
   if var is sentinel:
        pass
   else:
        print var

 

修飾器方法

Python cookbook中也提到了這個方法,為了避免對每一個函數中每一個可能為None的對象進行一個if not l的判斷,使用可更優雅的修飾器方法

import copy
def freshdefault(f):
    fdefaults = f.func_defaults
    def refresher(*args,**kwds):
        f.func_defaults = deepcopy(fdefaults)
        return f(*args,**kwds)
    return refresh

 

這段代碼也再次認證了默認參數是函數的一個屬性這一事實

擴展

python中函數的默認值只會被執行一次,(和靜態變量一樣,靜態變量初始化也是被執行一次)。Python可以通過函數的默認值來實現靜態變量的功能。

參考 

[1]陷阱!python參數默認值
[2]python tips - 注意函數參數的默認值
[3]Default Parameter Values in Python


免責聲明!

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



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