JSer 快速入門 Python 之函數(shù)詳解

前一篇文章,用一天的時間,通過與 JavaScript 做對比的方式,快速領(lǐng)略了 Python 全貌。

梳理了那么多,若忽略細節(jié)差異,兩門語言只有兩個重要差異:

1、書寫風(fēng)格上大相同
2、功能覆蓋上,Python ~ ES6 + NodeJs

第 2 條,說的是 Python 的能力,不是入門時所要關(guān)注的。那么只有 寫法差異 這一條需要重點關(guān)注——若經(jīng)歷了第一天的快速入門,現(xiàn)在閉著眼睛還能回想 Python 的寫法差異,比如這樣:

#  一個簡單的 for 循環(huán)
for val in ['a', 'b', 'c', 'd']:
    print(val)

# 一個簡單的函數(shù)定義,包含了 if else 語句
def fn(n):
    if n >= 0:
        return n
    else:
        return -n

那么,恭喜,你可以對自己說:我已掌握 Python!

之前主要集中在概覽和基本的語法規(guī)范上,現(xiàn)在我們把目光專注在函數(shù)上,詳細看看 Python 函數(shù)有哪些不一樣的特性。

對于 JS 函數(shù)我們爛熟于心。JS 函數(shù)由函數(shù)名參數(shù)、返回值內(nèi)置成員作用域等部分構(gòu)成。

let a = 1;
function fn (x, ...y) {
    // arguments, this
    let inner = () => {
        a = 100;
    }
    inner();
    console.log(a);
    return undefined
}

Python 函數(shù)同樣由這些部分構(gòu)成,以下是一個簡單的表格將它們進行對比。

函數(shù)組成 JS Python 說明
語法規(guī)范 命名函數(shù)、匿名函數(shù) 和 JS 總體相似,但具體語法處處是差異 關(guān)鍵字+函數(shù)名+參數(shù)+返回值
內(nèi)置屬性 arguments、this 無內(nèi)置屬性 Python 沒有復(fù)雜的 this
作用域 全局作用域、局部作用域 和 JS 的意義一致,僅形式差異 無學(xué)習(xí)壓力

以下內(nèi)容就按照這些對比項依次梳理。(可能內(nèi)容和順序稍有變化)

函數(shù)的定義規(guī)范

Python 函數(shù)書寫形式上和 JS 有巨大差異(此次遷移學(xué)習(xí),適應(yīng) Python 代碼的書寫習(xí)慣一直是個重點),語法上差別也很多。

def py_fn (x):
    print(x)
    return x

聲明函數(shù)的關(guān)鍵字是 def,函數(shù)名駝峰下劃線都行,Python 在意大小寫的......這些沒啥好說。函數(shù)的參數(shù)、函數(shù)的調(diào)用及傳參、return 語句、匿名函數(shù)等等。

函數(shù)的參數(shù)

參數(shù)種類 JS Python 說明
位置參數(shù) 有序形參 有序形參 必須按照對應(yīng)的順序位置傳參
默認(rèn)參數(shù) 形參被賦初始值 形參被賦初始值 JS 和 Python 完全一致
關(guān)鍵字參數(shù) 無此類型 可通過形參名無序傳入 Python 傳參更靈活
rest 參數(shù) rest 部分只能是一個數(shù)組 rest 部分支持列表、元組、字典 Python 支持對象的 rest 參數(shù)

相比起來,JS 對于傳參顯得不夠靈活,比如我們往往會遇到需要傳遞多個參數(shù)的函數(shù),那么形參會列出一長串出來。
——很快會感覺形參過長,定位麻煩,而且再擴展則更難看。
——然后我們選擇把它合并在一個對象上 options,利于擴展更多參數(shù),但壞處是所有參數(shù)都被隱藏了,意味著語義不明。
——然后我們再做一次變形,突出關(guān)鍵參數(shù),其他參數(shù)打包放到 options。

// 參數(shù)展開
funtion init (name, age, city, job) {
    //
}
// 參數(shù)合并在一個對象上
funtion init (options) {
    //
}

// 突出關(guān)鍵字 name, job
funtion init (name, job, options) {
    //
}

ES6 引入的默認(rèn)參數(shù)、rest 參數(shù)很多程度增加了 JS 傳參的靈活性。

Python 參數(shù)相對更加靈活,其關(guān)鍵字參數(shù)是一個聰明的辦法,它允許用戶無視參數(shù)的位置,只要將實參賦值給形參傳入即可。另外,它不再有類似arguments的內(nèi)置對象去管理傳入的實參。

# 突出關(guān)鍵字 name, job
def init (name, job, **options):
    pass

init(job='writer', name='stone')

Python 的關(guān)鍵字參數(shù)是解除對參數(shù)順序的強綁定,JS 的 arguments 則是解除對參數(shù)名、實參個數(shù)的強綁定。兩種語言,從參數(shù)的設(shè)計上耐人尋味。

除了關(guān)鍵字參數(shù),其他形式的參數(shù)兩種語言完全一樣,在此就不再一 一展開了。

函數(shù)調(diào)用傳參

不過還有一項需要注意,就是 Python 函數(shù)調(diào)用時傳參個數(shù)必須和函數(shù)定義的形參個數(shù)一致,否則會報錯。這點,JS 則恰好不同,多傳不會報錯,少傳通過調(diào)整函數(shù)內(nèi)容也能避免報錯。

// 多傳參數(shù)
def f(x, y):
    print(x+y)
    
f(4, 5, 6) # TypeError: f() takes 2 positional arguments but 3 were given

// 少傳參數(shù)
f(4) # TypeError: f() missing 1 required positional argument: 'y'

函數(shù)傳參的值類型和引用類型

Python 也有值類型和引用類型,讓 JSer 欣慰的是它們恰和 JS 的值類型、引用類型表達完全相同的意思。當(dāng)然也有說是可變類型(mutable)與不可變類型(immutable),實際上都是一個意思。

在 Python 中,strings, tuplesnumbers 是不可修改的類型,而 list,dict 等則是可修改的類型。只要對不可變類型的變量進行了重新賦值,意味著是在內(nèi)存中新創(chuàng)建了一個變量,原來的變量不再被使用而可能被垃圾回收機制回收掉;而引用類型,是可以改變其內(nèi)部成員(元素)的,整體上變量還是指向那塊內(nèi)存區(qū)。

函數(shù)返回值

函數(shù)的定義關(guān)鍵字、函數(shù)名、參數(shù)都已經(jīng)依次梳理了,剩下的一項便是函數(shù)的 return 語句。

對比項 有 return 語句 無 return 語句
JS 返回 return 語句執(zhí)行后的值 返回undefined
Python 同上 返回 None

Python 暫不賦值時可以用空值占位,即用 None 表示,僅此而已。它沒有 JS 的 undefined(未定義、未賦值),倒是和 null的意思很接近。

另外,Python 可以一次返回多個值——但這只是寫法,并不是真正意義的返回多個值,因為用 , 逗號隔開的返回值,實際上是一個元組,只是看起來像返回了多個值——函數(shù)就是數(shù)學(xué)意義上的函數(shù),可以有多個輸入,但輸出只有一個。

def f(x, y):
    print(x, y)
    return x*x, x*y, y*y

z = f(1, 2)
print('z', z) #z (1, 2, 4)

與之有相似用法的是 JS 的解構(gòu)賦值:

function f(x, y) {
    console.log(x, y)
    return {x, y}
}
let z = f(1, 2);
console.log(z); // {x: 1, y: 2}

作用域

和 JS 非常相似,Python 同樣具有全局作用域和局部作用域。在說這個之前,咱們先比較幾個細節(jié)點。

Python 沒有塊級作用域

從 ES6 開始,JS 就有了塊級作用域,也就是用 {} + let 或者 {} + const開辟出來的一種局部作用域。

也許 Python 不想把事情搞得很復(fù)雜,也許因為比較恨 {}符號,總之它的作用域比較簡單,它沒有塊級作用域。它唯一能用來開辟作用域的就是函數(shù)。

Python 不存在變量提升

JS 的變量聲明和賦值是分兩步進行的,在同一層作用域中,JS 會預(yù)先掃描一遍 var、let、const 關(guān)鍵字聲明的變量,會對 ES5 時代 的 var 變量進行提升,而 ES6 時代的 letconst 變量不做提升。

Python 聲明變量則是一步完成的,也就是它必須是一個帶賦值 = 號的聲明語句,那樣很尷尬。一不帶關(guān)鍵字,二則一步完成,剪去了不少的邊邊角角,這恐怕也是 Python 的簡潔之道了。聲明和賦值是強綁定在一塊的,顯然難以做變量提升。

// 因為對 var 變量進行了提升,故報錯
function fn (x) {
    const y = 100;
    {
        var y = 200;  // Error: Duplicate declaration
    }
}

# 因為不存在變量提升,故報錯
def fn (x): 
    print(y) #UnboundLocalError: local variable 'y' referenced before assignment
    y = 5

全局作用域和局部作用域

聲明在函數(shù)外的擁有全局作用域;聲明在函數(shù)內(nèi)部的變量,擁有一個局部作用域。全局變量可以在整個程序范圍中被訪問到,而局部變量只能在函數(shù)內(nèi)部被訪問。這與 JS 一致,并無任何差異。

y = 100
def fn ():
    x = y + 1
    print(x)

變量的聲明的差異,則是剪掉了繁瑣的聲明關(guān)鍵字。

繼續(xù)來一個例子,對 Python 作用域和變量聲明強化下認(rèn)識。

x = 100
def fn ():
    x = x + 1
    print(x) #?

如果思維切換的太快,很可能你會認(rèn)為 x的打印結(jié)果為 101——理由是函數(shù)里頭的賦值語句右邊的 x 能夠訪問到函數(shù)之外的全局變量 x。很顯然,一時間還不能適應(yīng)無關(guān)鍵字的聲明……因為 Pyhton 聲明變量不帶關(guān)鍵字,容易把 x = x + 1 當(dāng)作是對全局變量的訪問和改寫。

那么,此處實際上是在函數(shù)體內(nèi)聲明一個同名局部變量,這個聲明已然切斷了訪問外部 x 的能力,再加上一條不存在變量提升,也就是編譯到這行時才去找 x,發(fā)現(xiàn)上邊無法找到被定義的 x,故報錯 local variable 'x' referenced before assignment。

咱再來看看 ES6 同樣的代碼的執(zhí)行情況:

let x = 100;
function fn (){
    let x = x + 1;
    console.log(x); // ReferenceError: x is not defined
}

執(zhí)行 fn 同樣會報錯。所以,應(yīng)該將 Python 和最先進的 JS 比較學(xué)習(xí),變量提升神馬的讓大部分非 JSer 朋友很困惑,所以它倆都不存在變量的提升。

另外需要細細體會的是編譯時預(yù)先掃描一下這個動作,因為上例改成 ES5 的 var 寫法則不會報錯!掃描,好比上課前老師對教室的學(xué)生掃視一下——掃視容易發(fā)現(xiàn)長得像的雙胞胎和長得出眾的美女——僅此而已,相當(dāng)于做一個預(yù)先的錯誤排查。

最后關(guān)于作用域還補述一個問題:如果想在局部作用域(函數(shù)體內(nèi))修改一個全局變量咋辦?

很簡單,JS 直接拿起來重新賦值就好了,因為賦值不帶關(guān)鍵字也不是聲明;Python 恰好相反,它聲明變量不帶關(guān)鍵字,那么在局部作用域中重新賦值豈不等于是新聲明了一個局部變量?

所以 Python 不得不特殊關(guān)照下這種情況,也就是引入關(guān)鍵字 globalnonlocal,前者表示該關(guān)鍵字之后的變量是全局變量,后者表示該關(guān)鍵字之后是本函數(shù)之外的、全局之下的局部變量。

x = 100
def fn ():
    global x
    x = 99
    def f ():
        nonlocal y
        y = 666
    f()
    print(y) # 666
fn()
print(x)  # 1

閉包

作用域除了上述的 “內(nèi)訪問外,外不能訪問內(nèi)”,閉包也是作用域訪問的一種形式,Python 同樣擁有閉包特性,且和 JS 的閉包是一個道理,在此不再贅述。

函數(shù)內(nèi)置屬性

JS 函數(shù)內(nèi)置了 arguments、this 這倆屬性來獲取函數(shù)調(diào)用的一些重要信息,Python 沒有提供這樣的內(nèi)置屬性,所以完全不必操心這方面的學(xué)習(xí)。

取而代之的是,Python 提供了一系列的內(nèi)置函數(shù),來獲取函數(shù)的相關(guān)信息。例如,用dir 函數(shù)讀取函數(shù)相關(guān)信息:

info = dir(f)
print(info)

"""
['_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__']
"""

這些信息較多,請讀者自行百度了解,此節(jié)不再詳述。

最后

好了,到此已經(jīng)將 Python 函數(shù)各個組成部分和 JS 逐個對比了,總之相似的部分居多,差異的部分只要自己的 Python 代碼跑起來,自然很容易發(fā)現(xiàn)和掌握。

最后不得不說,從 JS 轉(zhuǎn)學(xué) Python 非常容易,只要時刻記住:縮進、縮進、再縮進!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容