瞭解幾個Python高級特性

  1. 前言

  Python 非常靈活強大,跟它具有一些特性有關,如匿名函數、列表推導式、迭代器、裝飾器等。本文主要簡單介紹:

  切片

  迭代、可迭代對象、迭代器

  推導式(列表推導式、集合推導式、字典推導式)

  生成器和生成器表達式

  匿名函數

  裝飾器

  2. 切片

  切片(slice)在 Python 中非常強大,可以輕鬆對字符串、列表和元組進行切割,完成拷貝。注意切片是淺拷貝,關於淺拷貝和深拷貝留作以後討論。

  切片的語法是:obj[start: end: step]

  obj 是支持切片的對象,如:列表、字符串、元組等。

  start 是開始切的索引位置,索引是從 0 開始標記的。start 可以省略,默認值是 0.

  end 是切片結束的位置,實際上切不到 obj[end]。end 也可以省略,默認值是對象的長度。

  step 是切片的步長,也可以省略,默認值是 1。

  對字符串、列表、元組進行切片。

  >>> word = "Python"

  >>> word[:]

  'Python'

  >>> word[1:3]

  'yt'

  >>> word[::2]

  'Pto'

  >>> ls = [1, 2, 3, 4, 5, 6]

  >>> ls[::2]

  [1, 3, 5]

  >>> t = (1, 2, 3, 4, 5, 6)

  >>> t[2:6]

  (3, 4, 5, 6)

  切片也支持負數,使用 obj[::-1] 可以輕鬆實現翻轉,如把列表翻轉:

  >>> ls = [1, 2, 3, 4, 5, 6, 7]

  >>> ls[::-1]

  [7, 6, 5, 4, 3, 2, 1]

  obj[end] 是取不到的。

  >>> ls = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  >>> ls[1:9]

  [2, 3, 4, 5, 6, 7, 8, 9]

  可以看到 ls[1:9], ls[1],即 2 可以取到,ls[9],即 10 是取不到的。

  3. 迭代、可迭代對象、迭代器

  迭代(iteration):迭代是一種操作,可以理解爲遍歷,如用 for 循環遍歷列表或者元組。

  可迭代對象(iterable object):可以用 for 循環迭代的對象。如列表、元組、字符串、字典、集合等。

  到目前爲止,可以看到大多數容器對象都可以使用 for 語句:

  for element in [1, 2, 3]:

  print(element)

  for element in (1, 2, 3):

  print(element)

  for key in {'one':1, 'two':2}:

  print(key)

  for char in "123":

  print(char)

  for line in open("myfile.txt"):

  print(line, end='')

  我們還可以用 collections 模塊中的 Iterable 類型判斷一個對象是否是可迭代對象。

  >>> from collections import Iterable

  >>> isinstance([1, 2], Iterable)

  True

  >>> isinstance((1, 2), Iterable)

  True

  >>> isinstance('abc', Iterable)

  True

  迭代器(iterator):是遵循迭代器協議的可迭代對象就稱爲迭代器。迭代器協議機制是:for 語句會在容器對象上調用 iter()。 該函數返回一個定義了 __next__() 方法的迭代器對象,此方法將逐一訪問容器中的元素。 當元素用盡時,__next__() 將引發 StopIteration 異常來通知終止 for 循環。你可以使用 next() 內置函數來調用 __next__() 方法。

  通俗理解就是:能被 next()函數調用並不斷返回下一個值的對象成爲迭代器。

  迭代器的使用非常普遍並使得 Python 成爲一個統一的整體。下面這個例子顯示了迭代器的運作方式:

  >>> s = 'abc'

  >>> it = iter(s)

  >>> it

  >>> next(it)

  'a'

  >>> next(it)

  'b'

  >>> next(it)

  'c'

  >>> next(it)

  Traceback (most recent call last):

  File "", line 1, in

  next(it)

  StopIteration

  我們可以使用 isinstance()方法判斷一個對象是否是 Iterator 對象。

  >>> from collections import Iterator

  >>> ls = [1, 2, 3, 4]

  >>> isinstance(ls, Iterator)

  False

  >>> isinstance(iter(ls), Iterator)

  True

  >>> t = (1, 2, 3, 4, 5)

  >>> isinstance(t, Iterator)

  False

  >>> isinstance(iter(t), Iterator)

  True

  >>> s = 'abcde'

  >>> isinstance(s, Iterator)

  False

  >>> isinstance(iter(s), Iterator)

  True

  通過上面的例子我們可以看到,列表、元組、字符串等是可迭代對象,但是不是迭代器。可以使用 iter()函數,輕鬆把列表、元組、字符串等轉爲迭代器。

  爲什麼列表、元組、字符串等不是迭代器呢?

  因爲 Python 的 Iterator 對象表示的是一個數據流,迭代器可以被 next()函數不斷調用並返回下一個數據,直到沒有數據時拋出 StopIteration 錯誤。我們可以把這個數據流看成一個有序序列,但是我們卻不能提前知道序列的長度,只能不斷通過 next()函數實現按需計算下一個數據,因此 Iterator 的計算是惰性的,只有在需要返回下一個數據時它纔會計算。

  所以 Iterator 可以表示一個無限大的數據流,如全部整數,但是列表等容器由於內存空間限制,用於不可能存儲全體整數。

  小結:

  迭代(iteration):迭代是一種操作,用 for 循環遍歷。

  可迭代對象(iterable object):可以用 for 循環迭代的對象。

  迭代器(iterator):可以作用於 next()函數的可迭代對象,它們是一個惰性計算序列。

  可以用 iter()函數把 list、str、tuple、dict 轉爲迭代器。

  4. 推導式

  推導式(comprehension)是 Python 非常重要的一個特性,提供了更加簡單的創建列表、集合、字典的方式。其中列表推導式(list comprehensions)是用的最多的。

  4.1 列表推導式

  列表推導式是 Python 非常重要的一個特性之一。列表推導式提供了一個更簡單的創建列表的方法。

  常見的用法是把某種操作應用於序列或可迭代對象的每個元素上,然後使用其結果來創建列表,或者通過滿足某些特定條件元素來創建子序列。

  比如:假設我們想創建一個平方列表。

  通常我們是這麼做:

  >>> squares = []

  >>> for x in range(10):

  ... squares.append(x**2)

  ...

  >>> squares

  [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

  這樣做盡管可以達到目的,但是這裏創建(或被重寫)的名爲 x 的變量在 for 循環後仍然存在,可能存在副作用。我們可以通過以下方法計算平方列表的值而不會產生任何副作用。

  方法一:squares = list(map(lambda x: x**2, range(10)))

  方法一等價於:

  方法二:squares = [x**2 for x in range(10)]

  這裏的方法二就是列表推導式,我們可以看到列表推導式更加簡潔易讀。

  列表推導式的結構是:

  [ 表達式 for子句(必須有一個) 0 或多個 for 或者 if 子句]

  說明:由一對方括號([])所包含以下內容:一個表達式,後面跟一個 for 子句,然後是零個或多個 for 或 if 子句。根據後面的 for 等子句計算表達式的值,然後把所有計算的值存爲一個新列表。如:以下列表推導式會將兩個列表中不相等的元素組合起來變爲一個新的列表:

  [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]

  它等價於

  >>> combs = []

  >>> for x in [1,2,3]:

  ... for y in [3,1,4]:

  ... if x != y:

  ... combs.append((x, y))

  ...

  >>> combs

  [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

  可見,使用列表推導式非常簡潔。注意在上面兩個代碼片段中, for 和 if 的順序是相同的。如果表達式是一個元組(例如上面的 (x, y)),那麼就必須加上括號。

  列表推導式可以使用複雜的表達式和嵌套函數。如:

  >>> from math import pi

  >>> [str(round(pi, i)) for i in range(1, 6)]

  ['3.1', '3.14', '3.142', '3.1416', '3.14159']

  嵌套的列表推導式:列表推導式中的初始表達式可以是任何表達式,包括另一個列表推導式。

  考慮下面這個 3x4 的矩陣,它由 3 個長度爲 4 的列表組成

  >>> matrix = [

  ... [1, 2, 3, 4],

  ... [5, 6, 7, 8],

  ... [9, 10, 11, 12],

  ... ]

  下面的列表推導式將交換其行和列

  >>> [[row[i] for row in matrix] for i in range(4)]

  [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

  如上節所示,嵌套的列表推導式是基於跟隨其後的 for 進行求值的,所以這個例子等價於:

  >>> transposed = []

  >>> for i in range(4):

  ... transposed.append([row[i] for row in matrix])

  ...

  >>> transposed

  [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

  反過來說,也等價於

  >>> transposed = []

  >>> for i in range(4):

  ... # the following 3 lines implement the nested listcomp

  ... transposed_row = []

  ... for row in matrix:

  ... transposed_row.append(row[i])

  ... transposed.append(transposed_row)

  ...

  >>> transposed

  [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

  實際應用中,使用內置函數去組成複雜的流程語句是更好的選擇。 zip() 函數將會很好地處理這種情況

  >>> list(zip(*matrix)) # *matrix 是列表解包,會去除最外層的[]

  [(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

  列表推導式小結:

  列表推導式格式: [表達式(可嵌套列表推導式) for子句 0或多個 for 或 if 子句]

  列表推導式創建新列表更加簡潔易讀,沒有副作用。

  注意,雖然元組和列表類似,但是列表是可變的,元組是不可變的,所以元組沒有推導式。

  元組是不可變的,其序列通常包含不同種類的元素,並且通過解包或者索引來訪問。一個元素的元組的創建必須在一個元素後面添加一個逗號,如 t = (1, )

  列表是可變的,一般列表的元素都是同種類型的,並且通過迭代訪問。

  4.2 集合推導式

  集合是無序、確定、互異的。它通常用於成員測試和去重操作。此外還可以進行並集、交集、差集、補集等操作。

  集合也和列表一樣支持推導式,集合推導式(set comprehensions)如下:

  >>> {x for x in 'abracadabra' if x not in 'abc'}

  {'r', 'd'}

  即:把[] 變爲 {} 即可,其他和列表規則一樣。

  4.3 字典推導式

  字典也支持字典推導式(dict comprehensions),如:

  >>> {x: x**2 for x in (2, 4, 6)}

  {2: 4, 4: 16, 6: 36}

  >>> {x: y for x, y in zip("abcd", [1, 2, 3, 4])}

  {'a': 1, 'b': 2, 'c': 3, 'd': 4}

  即:把[] 變爲 {},同時表達式符合字典的鍵值對形式 key: value。其他規則同列表推導式。

  5. 生成器和生成器表達式

  5.1 生成器

  生成器(Generator):是一個用於創建迭代器的簡單而強大的工具。 它們的寫法類似標準的函數,但當它們要返回數據時會使用 yield 語句。 每次對生成器調用 next() 時,它會從上次離開位置恢復執行(它會記住上次執行語句時的所有數據值)。就是把函數中 return 關鍵字換成了 yield 關鍵字,這樣定義出來的就不是函數了,而是一個生成器,通常我們用 for 循環去迭代生成器,而不是用 next()函數一個一個調用,示例如下:

  def reverse(data):

  for index in range(len(data)-1, -1, -1):

  yield data[index]

  >>> for char in reverse('golf'):

  ... print(char)

  ...

  f

  l

  o

  g

  可以用生成器來完成的操作同樣可以用基於類的迭代器來完成。 但生成器的寫法更爲緊湊,因爲它會自動創建 __iter__() 和 __next__() 方法。

  另一個關鍵特性在於局部變量和執行狀態會在每次調用之間自動保存。

  除了會自動創建方法和保存程序狀態,當生成器終結時,它們還會自動引發 StopIteration。 這些特性結合在一起,使得創建迭代器能與編寫常規函數一樣容易。

  生成器比較難理解的一點在於生成器的執行流程和函數流程不一樣,函數是順序執行,遇到 return 語句,或者最後一行函數語句就返回。而變成生成器的函數時,在每次調用 next()函數的時候執行,遇到 yield 語句返回,再次執行時從上次返回的 yield 語句處繼續執行。

  舉個栗子:

  >>> def language():

  ... print("Step 1")

  ... yield "Python"

  ... print("Step 2")

  ... yield "Java"

  ... print("Step 3")

  ... yield "C"

  ...

  >>> lang = language()

  >>> next(lang)

  Step 1

  'Python'

  >>> next(lang)

  Step 2

  'Java'

  >>> next(lang)

  Step 3

  'C'

  >>> next(lang)

  Traceback (most recent call last):

  File "", line 1, in

  StopIteration

  調用 language 生成器時,首先要生成一個 generator 對象,然後用 next()函數不斷獲得下一個返回值。在執行的過程中,遇到 yield 就中斷,下次又繼續執行,執行 3 次 yield 後,沒有 yield 可以執行了,所以第 4 次調用 next(lang)就報錯了。

  5.2 生成器表達式

  生成器除了用類似於函數的定義方法外,還可以用類似於列表推導式的方式生成,所用語法類似列表推導式,就是把外層的方括號換成圓括號即可。這種表達式被設計用於生成器將立即被外層函數所使用的情況。

  生成器表達式相比完整的生成器更緊湊但較不靈活,相比等效的列表推導式則更爲節省內存。因爲生成器表達式是生成迭代器,迭代器是惰性計算,它存儲的是算法,需要多少就計算多少,而列表推導式是直接全部計算出來,放到內存。

  例如:

  >>> sum(i*i for i in range(10)) # sum of squares

  285

  >>> xvec = [10, 20, 30]

  >>> yvec = [7, 5, 3]

  >>> sum(x*y for x,y in zip(xvec, yvec)) # dot product

  260

  >>> unique_words = set(word for line in page for word in line.split())

  >>> valedictorian = max((student.gpa, student.name) for student in graduates)

  >>> data = 'golf'

  >>> list(data[i] for i in range(len(data)-1, -1, -1))

  ['f', 'l', 'o', 'g']

  6. 匿名函數

  當我們傳入函數時,有些時候不需要顯示的定義函數,直接傳入匿名函數( anonymous functions)更方便。

  匿名函數是通過 lambda 關鍵字來創建的。基本語法是:lambda 參數: 表達式

  其中參數可以是多個,用逗號分隔。如:lambda a, b: a+b,這個匿名函數返回兩個參數的和。再舉個栗子看看匿名函數的常用用法:

  >>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]

  >>> pairs.sort(key=lambda pair: pair[1])

  >>> pairs

  [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

  Lambda 函數可以在需要函數對象的任何地方使用。它們在語法上限於單個表達式。從語義上來說,它們只是正常函數定義的語法糖,不用寫 return,返回值就是表達式的結果。

  匿名函數的優勢在於沒有名字,不用擔心命名衝突。此外匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數。如:

  >>> s = lambda x, y: x + y # 計算兩數之和

  >>> s

  at 0x000002B457B3AF78>

  >>> s(2, 3)

  5

  >>> s(3, 7)

  此外,與嵌套函數定義一樣,lambda 函數可以引用所包含域的變量,把匿名函數作爲返回值返回,舉個官網栗子

  >>> def make_incrementor(n):

  ... return lambda x: x + n

  ...

  >>> f = make_incrementor(42)

  >>> f(0)

  42

  >>> f(1)

  43

  7. 裝飾器

  裝飾器(decorator)是一個非常有用的設計。它可以在代碼運行期間動態增加功能。本質上,裝飾器是一個返回函數的高階函數。

  比如:我們現在要定義一個能打印日誌的裝飾器,可以定義如下:

  def log(func):鄭州做人流多少錢 http://wap.zyfuke.com/

  def wrapper(*args, **kwargs):

  print(f"call {func.__name__}()")

  return func(*args, **kwargs)

  return wrapper

  這個 log 其實就是一個裝飾器,它接收一個函數作爲參數,並返回一個函數。我們藉助 Python 的 @ 語法,把裝飾器置於函數的定義處:

  def log(func):

  def wrapper(*args, **kwargs):

  print(f"call {func.__name__}()") # func.__name__獲取該函數的名字

  return func(*args, **kwargs)

  return wrapper

  @log

  def now():

  print("2020-05-05")

  if __name__ == '__main__':

  now()

  結果輸出:

  call now()

  2020-05-05

  把 @log 放到 now() 函數的定義處,相當於執行了語句:`now = log(now)。

  由於 log()是一個裝飾器,返回一個函數,所以,原來的 now()函數仍然存在,只是現在同名的 now 變量指向了新的函數,於是調用 now()將執行新函數,即在 log()函數中返回的 wrapper()函數。

  wrapper()函數的參數定義是(*args, **kwargs),因此 wrapper()可以接收任意參數的調用,在 wrapper()函數內,首先打印日誌,再緊接着調用原始函數。

  如果 decorator 本身需要傳入參數,那就需要編寫一個返回 decorator 的高階函數,寫出來會更復雜,比如,自定義 log 的文本:

  def log(text):

  def decorator(func):

  def wrapper(*args, **kwargs):

  print(f"{text} call {func.__name__}")

  return func(*args, **kwargs)

  return wrapper

  return decorator

  @log("開始")

  def now():

  print("2020-05-05")

  if __name__ == '__main__':

  now()

  結果輸出:

  開始 call now

  2020-05-05

  與 2 層嵌套相比,3 層嵌套效果如:now = log("開始")(now)

  說明:首先執行 log(“我們”),然後返回 decorator 函數,在調用返回的函數,參數是 now 函數,返回值最終是 wrapper 函數。

  這兩種 decorator 定義方式都沒有問題,不過還差最後一步,因爲函數也是對象,它有__name__等屬性,經過 decorator 裝飾之後的函數,它們的__name__已經從原來的 now 變成了,wrapper:

  def log(text):

  def decorator(func):

  def wrapper(*args, **kwargs):

  print(f"{text} call {func.__name__}")

  return func(*args, **kwargs)

  return wrapper

  return decorator

  @log("開始")

  def now():

  print("2020-05-05")

  if __name__ == '__main__':

  now()

  print(now.__name__)

  結果輸出:

  開始 call now

  2020-05-05

  wrapper

  這是因爲返回的 wrapper()函數名字就是 wrapper,所以需要把原始函數的__name__等屬性複製到 wrapper()函數中,否則,有些依賴函數簽名的代碼執行會出錯。Python 內置的 functools.wraps 就是幹這個事的,所以一個完整的 decorator 的寫法如下:

  import functools

  def log(func):

  @functools.wraps(func)

  def wrapper(*args, **kwargs):

  print(f"call {func.__name__}()") # func.__name__獲取該函數的名字

  return func(*args, **kwargs)

  return wrapper

  @log

  def now():

  print("2020-05-05")

  if __name__ == '__main__':

  now()

  print(now.__name__)

  結果輸出:

  call now()

  2020-05-05

  now

  帶參數的裝飾器:

  import functools

  def log(text):

  def decorator(func):

  @functools.wraps(func)

  def wrapper(*args, **kwargs):

  print(f"{text} call {func.__name__}")

  return func(*args, **kwargs)

  return wrapper

  return decorator

  @log("現在")

  def now():

  print("2020-05-05")

  if __name__ == '__main__':

  now()

  print(now.__name__)

  結果輸出:

  現在 call now

  2020-05-05

  now


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章