走近 Python (類比 JS)

牧云云發表於2019-02-16

本文首發在 個人部落格

Python 是一門運用很廣泛的語言,自動化指令碼、爬蟲,甚至在深度學習領域也都有 Python 的身影。作為一名前端開發者,也瞭解 ES6 中的很多特性借鑑自 Python (比如預設引數、解構賦值、Decorator等),同時本文會對 Python 的一些用法與 JS 進行類比。不管是提升自己的知識廣度,還是更好地迎接 AI 時代,Python 都是一門值得學習的語言。

資料型別

在 Python 中,最常用的能夠直接處理的資料型別有以下幾種:

  • 數字[整數(int)、浮點型(float)、長整型(long)、複數(complex)]
  • 字串(str)
  • 布林值(bool)
  • 空值(None)

除此之外,Python 還提供了列表[list]、字典[dict] 等多種資料型別,這在下文中會介紹。

型別轉換與型別判斷

與 JS 十分類似,python 也能實現不同資料型別間的強制與隱式轉換,例子如下:

強制型別轉換:

int(`3`) # 3
str(3.14) # `3.14`
float(`3.14`) # 3.14
# 區別於 JS 只有 Number 一種型別,Python 中數字中的不同型別也能相互強制轉換
float(3) # 3.0
bool(3) # True
bool(0) # False

隱式型別轉換:

1 + 1.0 # 2.0
1 + False # 1
1.0 + True # 2.0
# 區別於 JS 的 String + Number = String, py 中 str + int 會報錯
1 + `1` # TypeError: cannot concatenate `str` and `int` objects

此外寫程式碼的時候經常會需要判斷值的型別,可以 使用 python 提供的 type() 函式獲取變數的型別,或者使用 isinstance(x, type) 來判斷 x 是否屬於相應的 type 型別。

type(1.3) == float # True
isinstance(`a`, str) # True
isinstance(1.3, int) # False
isinstance(True, bool) # True
isinstance([], list) # True
isinstance({}, dict) # True

有序集合型別

集合是指包含一組元素的資料結構,有序集合即集合裡面的元素是是按照順序排列的,Python 中的有序集合大概有以下幾類:list, tuple, str, unicode。

list 型別

Python 中 List 型別類似於 JS 中的 Array,

L = [1, 2, 3]
print L[-1] # `3`

L.append(4) # 末尾新增元素
print L # [1, 2, 3, 4]

L.insert(0, `hi`) # 指定索引位置新增元素
print L # [`hi`, 1, 2, 3, 4]

L.pop() # 末尾移除元素 L.pop(2) ?????? 2 ???
print L # [`hi`, 1, 2, 3]

tuple 型別

tuple 型別是另一種有序的列表,中文翻譯為“ 元組 ”。tuple 和 list 非常類似,但是,tuple 一旦建立完畢,就不能修改了。

t = (1, 2, 3)
print t[0] # 1
t[0] = 11 # TypeError: `tuple` object does not support item assignment

t = (1)
print t # 1  t 的結果是整數 1

t = (1,) # 為了避免出現如上有歧義的單元素 tuple,所以 Python 規定,單元素 tuple 要多加一個逗號“,”
print t # (1,)

無序集合型別

dict 型別

Python 中的 dict 型別類似於 JS 中的 {} (最大的不同是它是沒有順序的), 它有如下特點:

  • 查詢速度快 (無論 dict 有 10 個元素還是 10 萬個元素,查詢速度都一樣)
  • 佔用記憶體大 (與 list 型別相反)
  • dict 中的 key 不能重複
  • dict 中儲存的 key-value 序對是沒有順序的
d = {
    `a`: 1,
    `b`: 2,
    `c`: 3
}

print d # {`a`: 1, `c`: 3, `b`: 2}  可以看出列印出的序對沒有按正常的順序打出

# 遍歷 dict
for key,value in d.items():
    print(`%s: %s` % (key,value))
# a: 1
# c: 3
# b: 2

set 型別

有的時候,我們只想要 dict 的 key,不關心 key 對應的 value,而且要保證這個集合的元素不會重複,這時,set 型別就派上用場了。set 型別有如下特點:

  • set 儲存的元素和 dict 的 key 類似,必須是不變物件
  • set 儲存的元素也是沒有順序的
s = set([`A`, `B`, `C`, `C`])
print s # set([`A`, `C`, `B`])

s.add(`D`)
print s # set([`A`, `C`, `B`, `D`])

s.remove(`D`)
print s # set([`A`, `C`, `B`])

Python 中的迭代

在介紹完 Python 中的有序集合和無序集合型別後,必然存在遍歷集合的 for 迴圈。但是和其它語言的標準 for 迴圈不同,Python 中的所有迭代是通過 for … in 來完成的。以下給出一些常用的迭代 demos:

索引迭代:

L = [`apple`, `banana`, `orange`]
for index, name in enumerate(L):  # enumerate() 函式把 [`apple`, `banana`, `orange`] 變成了類似 [(0, `apple), (1, `banana`), (2, `orange`)] 的形式
    print index, `-`, name

# 0 - apple
# 1 - banana
# 2 - orange

迭代 dict 的 value:

d = { `apple`: 6, `banana`: 8, `orange`: 5 }
print d.values() # [6, 8, 5]
for v in d.values()
    print v
# 6
# 8
# 5

迭代 dict 的 key 和 value:

d = { `apple`: 6, `banana`: 8, `orange`: 5 }
for key, value in d.items()
    print key, `:`, value
# apple : 6
# banana: 8
# orange: 5

切片操作符

Python 提供的切片操作符類似於 JS 提供的原生函式 slice()。有了切片操作符,大大簡化了一些原來得用迴圈的操作。

L = [`apple`, `banana`, `orange`, `pear`]
L[0:2] # [`apple`, `banana`] 取前 2 個元素
L[:2] # [`apple`, `banana`] 如果第一個索引是 0,可以省略
L[:] # [`apple`, `banana`, `orange`, `pear`] 只用一個 : ,表示從頭到尾
L[::2] # [`apple`, `orange`] 第三個參數列示每 N 個取一個,這裡表示從頭開始,每 2 個元素取出一個來

列表生成器

如果要生成 [1×1, 2×2, 3×3, …, 10×10] 怎麼做?方法一是迴圈:

L = []
for x in range(1, 11):
    L.append(x * x)

但是迴圈太繁瑣,而列表生成式則可以用一行語句代替迴圈生成上面的 list:

# 把要生成的元素 x * x 放到前面,後面跟 for 迴圈,就可以把 list 建立出來
[x * x for x in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

列表生成式的 for 迴圈後面還可以加上 if 判斷(類似於 JS 中的 filter() 函式),示例如下:

[x * x for x in range(1, 11) if x % 2 == 0]
# [4, 16, 36, 64, 100]

for 迴圈可以巢狀,因此,在列表生成式中,也可以用多層 for 迴圈來生成列表。

[m + n for m in `ABC` for n in `123`]
# [`A1`, `A2`, `A3`, `B1`, `B2`, `B3`, `C1`, `C2`, `C3`]

Python 函式

預設引數

JS 中 ES6 的 預設引數正是借鑑於 Python,用法如下:

def greet(name=`World`):
    print `Hello, ` + name + `.`

greet() # Hello, World.
greet(`Python`) # Hello, Python.

可變引數

類似於 JS 函式中自動識別傳入引數的個數,Python 也提供了定義可變引數,即在可變引數的名字前面帶上個 * 號。

def fn(*args):
    print args

fn()  # ()
fn(`a`) # (`a`,)
fn(`a`, `b`) # (`a`, `b`)

Python 直譯器會把傳入的一組引數組裝成一個 tuple 傳遞給可變引數,因此,在函式內部,直接把變數 args 看成一個 tuple 就好了。

常用高階函式

Python 中常用的函式 (map、reduce、filter) 的作用和 JS 中一致,只是用法稍微不同。

  • map 函式: 接收一個函式 f 和一個 list,並通過把函式 f 依次作用在 list 的每個元素上,得到一個新的 list 並返回。
def f(x):
    return x * x
print map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) # [1, 4, 9, 16, 25, 36, 49, 64, 81]
  • reduce 函式: 接收一個函式 f 和一個 list(可以接受第三個值作為初始值),reduce() 對 list 的每個元素反覆呼叫函式 f,並返回最終結果值。
def f(x, y):
    return x * y

reduce(f, [1, 3, 5]) # 15
  • filter 函式: 接收一個函式 f 和一個list,這個函式 f 的作用是對每個元素進行判斷,返回 True或 False,filter() 根據判斷結果自動過濾掉不符合條件的元素,返回由符合條件元素組成的新 list。
def is_odd(x):
    return x % 2 == 1

filter(is_odd, [1, 4, 6, 7, 9, 12, 17]) # [1, 7, 9, 17]

匿名函式

和 JS 的匿名函式不同的地方是,Python 的匿名函式中只能有一個表示式,且不能寫 return。拿 map() 函式為例:

map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]) # [1, 4, 9, 16, 25, 36, 49, 64, 81]

關鍵詞 lambda 表示匿名函式,冒號前面的 x 表示函式引數,可以看出匿名函式 lambda x: x* x 實際上就是:

def f(x):
    return x * x

閉包

之前寫過一些關於 JS 閉包的文章,比如 深入淺出JavaScript之閉包(Closure)、以及 讀書筆記-你不知道的 JavaScript (上),Python 中閉包的定義和 JS 中的是一致的即:內層函式引用了外層函式的變數,然後返回內層函式。下面來看下 Py 中閉包之 for 迴圈經典問題:

# 希望一次返回3個函式,分別計算1x1,2x2,3x3:
def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i * i
        fs.append(f)
    return fs

f1, f2, f3 = count() # 這種寫法相當於 ES6 中的解構賦值
print f1(), f2(), f3() # 9 9 9

老問題了,f1(), f2(), f3() 結果不應該是 1, 4, 9 嗎,實際結果為什麼都是 9 呢?

原因就是當 count() 函式返回了 3 個函式時,這 3 個函式所引用的變數 i 的值已經變成了 3。由於 f1、f2、f3 並沒有被呼叫,所以,此時他們並未計算 i*i,當 f1 被呼叫時,i 已經變為 3 了。

要正確使用閉包,就要確保引用的區域性變數在函式返回後不能變。程式碼修改如下:

方法一: 可以理解為建立了一個封閉的作用域,i 的 值傳給 j 之後,就和 i 沒任何關係了。每次迴圈形成的閉包都存進了記憶體中。

def count():
    fs = []
    for i in range(1, 4):
        def f(j):
            def g(): # 方法一
                return j * j
            return g
        r = f(i)
        fs.append(r)
    return fs

f1, f2, f3 = count()
print f1(), f2(), f3() # 1 4 9

方法二:思路比較巧妙,用到了預設引數 j 在函式定義時可以獲取到 i 的值,雖然沒有用到閉包,但是和方法一有異曲同工之處。

def count():
    fs = []
    for i in range(1, 4):
        def f(j = i): # 方法二
            return j * j
        fs.append(f)
    return fs

f1, f2, f3 = count()
print f1(), f2(), f3() # 1 4 9

decorator 裝飾器

ES6 的語法中的 decorator 正是借鑑了 Python 的 decorator。decorator 本質上就是一個高階函式,它接收一個函式作為引數,然後返回一個新函式

那裝飾器的作用在哪呢?先上一段日常專案中用 ts 寫的閘道器程式碼:

@Post(`/rider/detail`)  // URL 路由
@log()                   // 列印日誌
  @ResponseBody
  public async getRiderBasicInfo(
    @RequestBody(`riderId`) riderId: number,
    @RequestBody(`cityId`) cityId: number,
  ) {
    const result = await this.riderManager.findDetail(cityId, riderId)
    return result
  }

可以看出使用裝飾器可以極大地簡化程式碼,避免每個函式(比如日誌、路由、效能檢測)編寫重複性程式碼。

回到 Python 上,Python 提供的 @ 語法來使用 decorator,@ 等價於 f = decorate(f)。下面來看看 @log() 在 Python 中的實現:

# 我們想把呼叫的函式名字給列印出來
@log()
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)

# 來看看 @log() 的定義
def log():
    def log_decorator(f):
        def fn(x):
            print `呼叫了函式` + f.__name__ + `()`
            return f(x)
        return fn
    return log_decorator

# 結果
# 呼叫了函式 factorial()
# 3628800

class

物件導向程式設計

物件導向程式設計是一種程式設計正規化,基本思想是:用類定義抽象型別,然後根據類的定義建立出例項。在掌握其它語言的基礎上,還是比較容易理解這塊知識點的,比如從下面兩種寫法可以看出不同語言的語言特性間竟然有如此多的共性。

es6: (附:本文的主題是 python,所以只是初略展示下 js 中類的定義以及例項的建立,為了說明寫法的相似性)

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
}

const child1 = new Person(`Xiao Ming`, 10)

Python: (核心要點寫在註釋中)

# 定義一個 Person 類:根據 Person 類就可以造成很多 child 例項
class Person(object):
    address = `Earth` # 類屬性 (例項公有)
    def __init__(self, name, age): # 建立例項時,__init__()方法被自動呼叫
        self.name = name
        self.age = age
    def get_age(self): # 定義例項方法,它的第一個引數永遠是 self,指向呼叫該方法的例項本身,其他引數和普通函式是一樣的
        return self.age

child1 = Person(`Xiao Ming`, 10)
child2 = Person(`Xiao Hong`, 9)

print child1.name # `Xiao Ming`
print child2.get_age() # 9
print child1.address # `Earth`
print child2.address # `Earth`

繼承

child 屬於 Student 類,Student 類屬於 People 類,這就引出了繼承: 即獲得了父類的方法屬性後又能新增自己的方法屬性。

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Student(Person):
    def __init__(self, name, age, grade):
        super(Student, self).__init__(name, age) # 這裡也能寫出 Person.__init__(self, name, age)
        self.grade = grade

s = Student(`Xiao Ming`, 10, 90)
print s.name # `Xiao Ming`
print s.grade # 90

可以看到子類在父類的基礎上又增加了 grade 屬性。我們可以再來看看 s 的型別。

isinstance(s, Person)
isinstance(s, Student)

可以看出,Python 中在一條繼承鏈上,一個例項可以看成它本身的型別,也可以看成它父類的型別。

相關文章