Python2壽命只剩一個月啦!還不快趕緊學起Python3酷炫到爆的新特性!
- 2019 年 11 月 15 日
- 筆記
文章資訊
作者: Lateautumn4lin 來源:雲爬蟲技術研究筆記
AKA 逆向小學生
Python3.8已經發布了將近一個月了,距離Python3.0第一個版本發布也將超過10年了。相信很多人還是依舊在使用Python2.7版本,想要遷移到最新版本卻不知道怎麼能夠快速掌握其中最Amazing的方法。下面這篇文章,我會給大家推薦3.0版本依賴最最新潮的函數和語法,讓你們能夠在Review程式碼時候「脫穎而出」!
前言
首先我們先來講幾個時間點:
- Python2.7正式停止維護時間 2020年1月1日,距今還有1個多月

- Python3.8正式開始發布時間 2019年10月14日,距今將近1個多月

從這兩個數字我們可以看出,Python3這個大版本已經發展很長的時間了,而距離Python2.7的結束也越來越近了。在距離Python2.7停止維護的一年內,很多優秀開源項目都已經停止了對 2.7 的支援,例如到今年 1 月份,NumPy 將停止支援 Python 2;到今年年末,Ipython、Cython 和 Pandas 等等都將陸續停止支援 Python 2。
所以,為了響應號召,順應趨勢。我們慢慢的向Python3.X去遷移,那我們如何能夠快速的掌握Python3.X版本的精髓呢?下面我們從幾個有趣的新特性入手,這些特性或方法都是 Python 3 各個版本中新加的,它們相比傳統的 Python 方法,更容易解決實踐中的一些問題。
所有的示例都是在 Python 3.7 的環境下編寫的,每個特性示例都給出了其正常工作所需的最低的 Python 版本。
潮流特性
Q
你覺得你Python中最騷的操作是哪些?
解包!裝飾器!Typing家族!DataClass類!
- 格式化字元串 f-string(最低 Python 版本為 3.6)
「如何格式化字元串」這個話題我想是每個開發者在接觸一門新語言的時候都會去學習的語法,而在Python中格式化語法的方式大家通常都會偏向於【Format】或者 【%S】這兩種方法,操作如下:
print("My name is %s" % ('phithon', )) print("My name is %(name)s" % {'name':'phithon'}) print("My name is {}".format("bob")) print("My name is {name}".format(name="bob"))
而到了Python3.6版本,推出了新的格式化字元串的靈活方法【f-string】,使用【f-string】編寫的與上面功能相同的程式碼是這樣的
name="bob" print(f"My name is {name}")
我們對比這幾種格式化字元串的方法,可以發現相比於常見的字元串格式符【%S】 或 【Format】 方法,【f-string】 直接在佔位符中插入變數顯得更加方便,也更好理解,關於格式化速度方面可以參考這個博文看看詳細的解釋。
2. 路徑管理庫 Pathlib(最低 Python 版本為 3.4)
從上個特性可以看出【f-string】 確實非常強大和美觀,而在文件路徑方面,Python遵循了他們的開發理念:萬物皆是對象,所以他們把路徑也單拎出來搞了一個路徑對象庫,也就是一個處理文件路徑的抽象庫【pathlib】。如果你不知道為什麼應該使用 【pathlib】,請參閱下面這篇 Trey Hunner 編寫的炒雞棒的博文以及它的後續版本,下面我們對比同一案例的新舊兩個版本Python的實現:
from glob import glob file_contents = [] for filename in glob('**/*.py', recursive=True): with open(filename) as python_file: file_contents.append(python_file.read()) from pathlib import Path file_contents = [ path.read_text() for path in Path.cwd().rglob('*.py') ]s')
如上所示,您可以read_text對Path對象使用方法和列表理解,將文件內容全部讀入一個新列表中,相比於使用舊版本Python的實現,在語法和美觀上無疑是更加出色!
3. 類型提示 Type hinting(最低 Python 版本為 3.5)
程式語言有很多類型,靜態編譯型語言和動態解釋型語言的對比是軟體工程中一個熱門的話題,幾乎每個人對此有自己的看法。在靜態語言中類型標註無疑是讓人又愛又恨,愛的是編譯速度加快,團隊合作中準確了解函數方法的入參類型,恨的是Coding時極其繁瑣的標註。不過,標註這種極其符合團隊文化的操作還是在Python3中被引入,並且很快得到了人們的喜愛。
def print_yes_or_no(codition: str) -> bool: pass
4. 枚舉(最低 Python 版本為 3.4)
大家在寫Java或者C語言的時候都會接觸到枚舉這個特性,枚舉也是幫我們節省了很多時間,也讓我們的程式碼更加美觀。舊版本Python中大家想要實現枚舉的話實現方法五花八門,「八仙過海,各顯神通」,充分發揮了Python的動態語言特性。我們下面舉些例子:
#利用type自建類的騷操作 def enum(**enums): return type('Enum', (), enums) Numbers = enum(ONE=1, TWO=2, THREE='three') # Numbers.ONE == 1, Numbers.TWO == 2 and Numbers.THREE == 'three' #利用type自建類的騷操作升級版 def enum(*sequential, **named): enums = dict(zip(sequential, range(len(sequential))), **named) return type('Enum', (), enums) Numbers = enum('ZERO', 'ONE', 'TWO') # Numbers.ZERO == 0 and Numbers.ONE == 1 #有帶值到名稱映射的 def enum(*sequential, **named): enums = dict(zip(sequential, range(len(sequential))), **named) reverse = dict((value, key) for key, value in enums.iteritems()) enums['reverse_mapping'] = reverse return type('Enum', (), enums) # Numbers.reverse_mapping['three'] == 'THREE' # 更有甚者,利用namedtuple實現的 from collections import namedtuple def enum(*keys): return namedtuple('Enum', keys)(*keys) MyEnum = enum('FOO', 'BAR', 'BAZ') # 帶字元數字映射的,像C/C++ def enum(*keys): return namedtuple('Enum', keys)(*range(len(keys))) # 帶字典映射的,可以映射出各種類型,不局限於數字 def enum(**kwargs): return namedtuple('Enum', kwargs.keys())(*kwargs.values())
看過了以上這麼多騷操作,現在Python3給你凈化一下眼睛,Python3.4新推出通過「Enum」類編寫枚舉的簡單方法。
from enum import Enum, auto class Monster(Enum): ZOMBIE = auto() WARRIOR = auto() BEAR = auto() print(Monster.ZOMBIE) for i in Monster: print(i) #Monster.ZOMBIE #Monster.ZOMBIE #Monster.WARRIOR #Monster.BEAR
以上我們可以看出枚舉是符號名稱(成員)的集合,這些符號名稱與唯一的常量值綁定在一起。在枚舉中,可以通過標識對成員進行比較操作,枚舉本身也可以被遍歷
5. 原生 LRU 快取(最低 Python 版本為 3.2)
快取是大家在開發中都會用到的一個特性,如果我們準確的使用好它,它會節省我們很多時間和成本。相信很多人初學Python裝飾器的時候都會去實現一個快取的裝飾器來節省斐波那契函數的計算時間。而Python 3 之後將 LRU(最近最少使用演算法)快取作為一個名為「lru_cache」的裝飾器,使得對快取的使用非常簡單。
下面是一個簡單的斐波那契函數,我們知道使用快取將有助於該函數的計算,因為它會通過遞歸多次執行相同的工作。
import time def fib(number: int) -> int: if number == 0: return 0 if number == 1: return 1 return fib(number-1) + fib(number-2) start = time.time() fib(40) print(f'Duration: {time.time() - start}s') # Duration: 30.684099674224854s
我們看到,我們沒用快取裝飾器的時候計算的時間是30秒左右,現在,我們可以使用「lru_cache」來優化它(這種優化技術被稱為「memoization」)。通過這種優化,我們將執行時間從幾秒降低到了幾納秒。
from functools import lru_cache @lru_cache(maxsize=512) def fib_memoization(number: int) -> int: if number == 0: return 0 if number == 1: return 1 return fib_memoization(number-1) + fib_memoization(number-2) start = time.time() fib_memoization(40) print(f'Duration: {time.time() - start}s') # Duration: 6.866455078125e-05s
可以看出,我們在開發計算函數的時候使用快取裝飾器是多麼提高成本的一種手段,另外,在新版本Python3.8之後,lru_cache現在可直接作為裝飾器而不是作為返回裝飾器的函數。因此這兩種寫法現在都被支援:
@lru_cache def f(x): ... @lru_cache(maxsize=256) def f(x): ...
6. 擴展的可迭代對象解包(最低 Python 版本為 3.0)
Python解包相信在我們初學Python的時候都有所了解,如果我們很多地掌握這個特性,相信是一件非常酷的事情。那什麼是擴展的解包呢?我們可以從pep3132中了解更多,舉個例子:
# Python 3.4 中 print 函數 不允許多個 * 操作 >>> print(*[1,2,3], *[3,4]) File "<stdin>", line 1 print(*[1,2,3], *[3,4]) ^ SyntaxError: invalid syntax >>> # 再來看看 python3.5以上版本 # 可以使用任意多個解包操作 >>> print(*[1], *[2], 3) 1 2 3 >>> *range(4), 4 (0, 1, 2, 3, 4) >>> [*range(4), 4] [0, 1, 2, 3, 4] >>> {*range(4), 4} {0, 1, 2, 3, 4} >>> {'x': 1, **{'y': 2}} {'x': 1, 'y': 2}
我們可以看到,解包這個操作也算的上Python中極其潮流的玩法了,耍的一手好解包,真的會秀翻全場啊!
7. Data class 裝飾器(最低 Python 版本為 3.7)
Python 3.7 引入了【data class】,新特性大大簡化了定義類對象的程式碼量,程式碼簡潔明晰。通過使用@dataclass裝飾器
來修飾類的設計,可以用來減少對樣板程式碼的使用,因為裝飾器會自動生成諸如「__init__()
」和「__repr()__
」這樣的特殊方法。在官方的文檔中,它們被描述為「帶有預設值的可變命名元組」。
from dataclasses import dataclass @dataclass class DataClassCard: rank: str suit: str #生成實例 queen_of_hearts = DataClassCard('Q', 'Hearts') print(queen_of_hearts.rank) print(queen_of_hearts) print(queen_of_hearts == DataClassCard('Q', 'Hearts')) #Q #DataClassCard(rank='Q', suit='Hearts') #True
而常規的類,按照Python 3.7之前的語法類似於這樣
class RegularCard def __init__(self, rank, suit): self.rank = rank self.suit = suit queen_of_hearts = RegularCard('Q', 'Hearts') print(queen_of_hearts.rank) print(queen_of_hearts) print(queen_of_hearts == RegularCard('Q', 'Hearts')) #'Q' #<__main__.RegularCard object at 0x7fb6eee35d30> #False
雖然這種寫法並沒有使用更多的程式碼量,但是我們很容易看到為了初始化,僅僅只是為了初始化一個對象,rank和suit已經重複了三次。此外,如果你試圖使用這個RegularCard類,你會注意到對象的表示不是很具描述性,並且已有的類與新聲明的類是無法比較是否相同的。因為每次聲明都會使用一個新的記憶體地址,而「==」不止比較類存儲的資訊,還比較記憶體地址是否相同。
dataclass還在底層給我們做了更多的有用的封裝。默認情況下dataclass實現了__repr__
方法,可以很好的提供字元串表示;也是了__eq__
方法,可以做基本的對象比較。而如果RegularCard想實現上面的功能需要寫大量的聲明,程式碼量多的嚇人。
class RegularCard(object): def __init__(self, rank, suit): self.rank = rank self.suit = suit def __repr__(self): #可以將類的資訊列印出來 return (f'{self.__class__.__name__}' f'(rank={self.rank!r}, suit={self.suit!r})') #大家可以試著將「!r」去掉或者將其中的r改變為s或a,看看輸出結果會有什麼變化 #conversion character: expected 's', 'r', or 'a' def __eq__(self, other): #可以比較類是否相同(不考慮記憶體地址) if other.__class__ is not self.__class__: return NotImplemented return (self.rank, self.suit) == (other.rank, other.suit)
8. 隱式命名空間包(最低 Python 版本為 3.3)
一種組織 Python 程式碼文件的方式是將它們封裝在程式包中(包含一個「init.py」的文件夾)。下面是官方文檔提供的示例。
sound/ Top-level package __init__.py Initialize the sound package formats/ Subpackage for file format conversions __init__.py wavread.py wavwrite.py aiffread.py aiffwrite.py auread.py auwrite.py . .. effects/ Subpackage for sound effects __init__.py echo.py surround.py reverse.py ... filters/ Subpackage for filters __init__.py equalizer.py vocoder.py karaoke.py ...
在 Python 2 中,上面每個文件夾都必須包含將文件夾轉化為 Python 程式包的「init.py」文件。在 Python 3 中,隨著隱式命名空間包的引入,這些文件不再是必須的了。
sound/ Top-level package __init__.py Initialize the sound package formats/ Subpackage for file format conversions wavread.py wavwrite.py aiffread.py aiffwrite.py auread.py auwrite.py ... effects/ Subpackage for sound effects echo.py surround.py reverse.py ... filters/ Subpackage for filters equalizer.py vocoder.py karaoke.py ...
正如有些人說的那樣,這項工作並沒有像這篇文章說的那麼簡單,官方文檔「PEP 420 Specification」指出,常規的程式包仍然需要「init.py」,把它從一個文件夾中刪除會將該文件夾變成一個本地命名空間包,這會帶來一些額外的限制。本地命名空間包的官方文檔給出了一個很好的示例,並且明確指出了所有的限制。
總結
上面給出的幾個很潮流的特性可能並不是很全,更多的還需要大家去探索符合自己和團隊的玩法,這篇文章只是向大家展示一些比較好玩的Python新功能,掌握它可以幫助你寫出更加Pythonic的程式碼。