天天看點

愉快地遷移到 Python 3

為資料科學家準備的 Python 3 特性指南

Python 已經成為機器學習和一些需處理大量資料的科學領域的主流語言。它支援了許多深度學習架構和其他已确立下來的資料處理和可視化的工具集。

然而,Python 生态系統還處于 Python 2 和 Python 3 并存的狀态,且 Python 2 仍然被資料科學家們所使用。從 2019 年底開始,系統工具包将會停止對 Python 2 的支援。對于 numpy,2018 年之後任何更新将隻支援 Python 3。

為了讓大家能夠順利過渡,我收集了一系列 Python 3 特性,希望對大家有用。

愉快地遷移到 Python 3

圖檔來源于 Dario Bertini post (toptal)

使用 pathlib 更好的對路徑進行處理

pathlib 是 Python 3 中的預設子產品,能幫你避免過多的使用 os.path.join:

from pathlib import Path

dataset = 'wiki_images'

datasets_root = Path('/path/to/datasets/')

train_path = datasets_root / dataset / 'train'

test_path = datasets_root / dataset / 'test'

for image_path in train_path.iterdir():

    with image_path.open() as f: # note, open is a method of Path object

        # do something with an image
           

在之前的版本中總是不可避免的使用字元串連接配接(簡潔但明顯可讀性很差),如今使用 pathlib 後,代碼會更安全、簡潔、易讀。

同時 pathlib.Path 提供了一系列方法和特性,這樣一來 python 的初學者就不需搜尋了:

p.exists()

p.is_dir()

p.parts

p.with_name('sibling.png') # only change the name, but keep the folder

p.with_suffix('.jpg') # only change the extension, but keep the folder and the name

p.chmod(mode)

p.rmdir()           

pathlib 會節省你大量的時間,具體用法請參考 文檔 和 說明。

類型提示現在是 Python 的一部分啦

Pycharm 中類型提示示例:

愉快地遷移到 Python 3

Python 已不再是一個小型的腳本語言了,如今的資料處理流程包含許多步驟,每步涉及不同的構架(而且有時會涉及不同的邏輯)

引入類型提示功能有助于處理日漸複雜的程式,是以機器就可以幫助實作代碼驗證。而以前是不同的子產品需使用自定義的方式在文檔字元串(doctrings)中指定類型(提示:pycharm 能夠将舊的 doctrings 轉換為新的 Type hinting)。

下圖是一個簡單的例子,這段代碼對不同類型的資料均有效(這正是我們喜歡 Python 資料棧的原因)。

def repeat_each_entry(data):

    """ Each entry in the data is doubled

    <blah blah nobody reads the documentation till the end>

    """

    index = numpy.repeat(numpy.arange(len(data)), 2)

    return data[index]           

這段代碼樣例适用于 numpy.array(含多元數組)、astropy.Table 及 astropy.Column、bcolz、cupy、mxnet.ndarray 等。

這段代碼可用于 pandas.Series,但方式不對:

repeat_each_entry(pandas.Series(data=[0, 1, 2], index=[3, 4, 5])) # returns Series with Nones inside           

這還僅是兩行代碼。想象一下一個複雜系統的行為将是多麼的難以預測,僅因一個函數就有可能行為失常。在大型系統中,明确各類方法的期望類型是非常有幫助的,這樣會在函數未得到期望的參數類型時給出警告。

def repeat_each_entry(data: Union[numpy.ndarray, bcolz.carray]):           

如果你有重要的代碼庫, MyPy 這樣的提示工具很可能成為持續內建途徑的一部分。由 Daniel Pyrathon 發起的名為“讓類型提示生效”的線上教程可為您提供一個很好的介紹。

旁注:不幸的是,類型提示功能還未強大到能為 ndarrays 或 tensors 提供細粒度分型,但是或許我們很快就可擁有,這也将是 DS 的特色功能。

類型提示→運作中的類型檢查

在預設情況下,函數注釋不會影響你代碼的運作,但也僅能提示你代碼的目的。

然而,你可以使用像 enforce 這樣的工具在運作中強制類型檢查,這有助你調試(當類型提示不起作用時會出現很多這樣的情況)。

@enforce.runtime_validation

def foo(text: str) -> None:

    print(text)

foo('Hi') # ok

foo(5)    # fails

@enforce.runtime_validation

def any2(x: List[bool]) -> bool:

    return any(x)

any ([False, False, True, False]) # True

any2([False, False, True, False]) # True

any (['False']) # True

any2(['False']) # fails

any ([False, None, "", 0]) # False

any2([False, None, "", 0]) # fails           

函數注釋的其他慣例

如前所述,函數注釋不會影響代碼的執行,但是它可提供一些供你随意使用的元資訊(譯者注:關于資訊的資訊)。

例如,計量機關是科學領域的一個常見問題,astropy 包能夠提供一種簡單裝飾器來控制輸入量的機關并将輸出量轉換成所需機關。

# Python 3

from astropy import units as u

@u.quantity_input()

def frequency(speed: u.meter / u.s, wavelength: u.m) -> u.terahertz:

    return speed / wavelength

frequency(speed=300_000 * u.km / u.s, wavelength=555 * u.nm)

# output: 540.5405405405404 THz, frequency of green visible light           

如果你正使用 Python 處理表格式科學資料(資料量很大),你應該試一試 astropy。

你也可以定義你的專用裝飾器以同樣的方法對輸入量和輸出量進行控制或轉換。

使用 @ 進行矩陣乘積

我們來執行一個簡單的機器學習模型,帶 L2 正則化的線性回歸(也稱脊回歸):

# l2-regularized linear regression: || AX - b ||^2 + alpha * ||x||^2 -> min

# Python 2

X = np.linalg.inv(np.dot(A.T, A) + alpha * np.eye(A.shape[1])).dot(A.T.dot(b))

# Python 3

X = np.linalg.inv(A.T @ A + alpha * np.eye(A.shape[1])) @ (A.T @ b)           

使用 @ 的代碼更可讀也更容易在各深度學習架構間轉譯:一個單層感覺器可以在 numpy、cupy、pytorch、tensorflow(和其他操作張量的架構)下運作相同的代碼 X @ W + b[None, :] 實作。

使用 ** 作通配符

遞歸檔案夾的通配符在 python 2 中實作起來并不簡單,實際上我們要自定義 glob2 子產品來克服這個問題。而從 Python 3.6 以後将支援周遊标志:

import glob

# Python 2

found_images =

    glob.glob('/path/*.jpg')

  + glob.glob('/path/*/*.jpg')

  + glob.glob('/path/*/*/*.jpg')

  + glob.glob('/path/*/*/*/*.jpg')

  + glob.glob('/path/*/*/*/*/*.jpg')

# Python 3

found_images = glob.glob('/path/**/*.jpg', recursive=True)

在 python 3 中有更好的選擇,那就是使用 pathlib(-1 導入!):

# Python 3

found_images = pathlib.Path('/path/').glob('**/*.jpg')           

Print 現在是函數

沒錯,現在寫代碼需要這些煩人的圓括号,但是這有許多好處:

  • 簡化使用檔案描述符的文法:
print >>sys.stderr, "critical error"      # Python 2

print("critical error", file=sys.stderr)  # Python 3           
  • 無需 str.join 輸出制表符:
# Python 3

print(*array, sep='t')

print(batch, epoch, loss, accuracy, time, sep='t')           
  • 改寫或重定義 print 的輸出
# Python 3

_print = print # store the original print function

def print(*args, **kargs):

    pass  # do something useful, e.g. store output to some file           

在 jupyter 中,可以将每一個輸出記錄到一個獨立的文檔(以跟蹤斷線之後發生了什麼),這樣一來我們就可以重寫 print 函數了。

下面你可以看到名為 contextmanager 的裝飾器暫時重寫 print 函數的方式:

@contextlib.contextmanager

def replace_print():

    import builtins

    _print = print # saving old print function

    # or use some other function here

    builtins.print = lambda *args, **kwargs: _print('new printing', *args, **kwargs)

    yield

    builtins.print = _print

with replace_print():

    <code here will invoke other print function>           

這種方法并不推薦,因為此時有可能出現些小問題。

  • print 函數可參與清單了解和其他語言建構。
# Python 3

result = process(x) if is_valid(x) else print('invalid item: ', x)           

數值中的下劃線(千位分隔符)

PEP-515 在數值中引入下劃線。在 Python 3 中,下劃線可用于整數、浮點數、複數的位數進行分組,增強可視性。

# grouping decimal numbers by thousands

one_million = 1_000_000

# grouping hexadecimal addresses by words

addr = 0xCAFE_F00D

# grouping bits into nibbles in a binary literal

flags = 0b_0011_1111_0100_1110

# same, for string conversions

flags = int('0b_1111_0000', 2)           

使用 f-strings 簡便可靠的進行格式化

預設的格式化系統具有一定的靈活性,但這卻不是資料實驗所需要的。這樣改動後的代碼要麼太冗長,要不太零碎。

典型的資料科學的代碼會反複的輸出一些固定格式的日志資訊。常見代碼格式如下:

# Python 2

print('{batch:3} {epoch:3} / {total_epochs:3}  accuracy: {acc_mean:0.4f}±{acc_std:0.4f} time: {avg_time:3.2f}'.format(

    batch=batch, epoch=epoch, total_epochs=total_epochs,

    acc_mean=numpy.mean(accuracies), acc_std=numpy.std(accuracies),

    avg_time=time / len(data_batch)

))

# Python 2 (too error-prone during fast modifications, please avoid):

print('{:3} {:3} / {:3}  accuracy: {:0.4f}±{:0.4f} time: {:3.2f}'.format(

    batch, epoch, total_epochs, numpy.mean(accuracies), numpy.std(accuracies),

    time / len(data_batch)

))           

樣本輸出:

120  12 / 300  accuracy: 0.8180±0.4649 time: 56.60           

f-strings 全稱為格式化字元串,引入到了 Python 3.6:

# Python 3.6+

print(f'{batch:3} {epoch:3} / {total_epochs:3}  accuracy: {numpy.mean(accuracies):0.4f}±{numpy.std(accuracies):0.4f} time: {time / len(data_batch):3.2f}')           

同時,寫查詢或者進行代碼分段時也非常便利:

query = f"INSERT INTO STATION VALUES (13, {city!r}, {state!r}, {latitude}, {longitude})"           

重點:别忘了轉義字元以防 SQL 注入***。

‘真實除法’與‘整數除法’的明确差別

這對于資料科學而言是非常便利的改變(但我相信對于系統程式設計而言卻不是)

data = pandas.read_csv('timing.csv')

velocity = data['distance'] / data['time']           

在 Python 2 中結果正确與否取決于‘時間’和‘距離’(例如,以秒和米做測量機關)是否以整型來存儲。而在 Python 3 中這兩種除法的結構都正确,因為商是以浮點型存儲的。

另一個案例是整數除法現在已經作為一個顯式操作:

n_gifts = money // gift_price  # correct for int and float arguments
           

注意:這個特性既适用于内置類型又适用于由資料包(比如:numpy 或者 pandas)提供的自定義類型。

嚴格排序

# All these comparisons are illegal in Python 3

3 < '3'

2 < None

(3, 4) < (3, None)

(4, 5) < [4, 5]

# False in both Python 2 and Python 3

(4, 5) == [4, 5]
           
  • 防止不同類型執行個體的偶然分類
sorted([2, '1', 3])  # invalid for Python 3, in Python 2 returns [2, 3, '1']           
  • 有助于訓示處理原始資料時發生的問題

    旁注:适當的檢查 None(兩種版本的 Python 均需要)

if a is not None:

  pass

if a: # WRONG check for None

  pass
           

自然語言處理(NLP)中的統一編碼标準(Unicode)

s = '您好'

print(len(s))

print(s[:2])           

輸出:

  • Python 2: 6n��
  • Python 3: 2n您好.
x = u'со'

x += 'co' # ok

x += 'со' # fail           

Python 2 失效而 Python 3 如期輸出(因為我在字元串中使用了俄文字母)

在 Python 3 中 strs 是 unicode 字元串,這更友善處理非英國文本的 NPL。

還有其他好玩的例子,比如:

'a' < type < u'a'  # Python 2: True

'a' < u'a'         # Python 2: False           
from collections import Counter

Counter('Möbelstück')           
  • Python 2 是:Counter({‘xc3’: 2, ‘b’: 1, ‘e’: 1, ‘c’: 1, ‘k’: 1, ‘M’: 1, ‘l’: 1, ‘s’: 1, ‘t’: 1, ‘xb6’: 1, ‘xbc’: 1})
  • Python 3 是:Counter({‘M’: 1, ‘ö’: 1, ‘b’: 1, ‘e’: 1, ‘l’: 1, ‘s’: 1, ‘t’: 1, ‘ü’: 1, ‘c’: 1, ‘k’: 1})

雖然在 Python 2 中這些可以正确處理,但在 Python 3 下會更加友好。

字典和 **kwargs 的儲存順序

在 CPython 3.6+ 中,預設情況下字典的行為類似于 OrderedDict(這在 Python 3.6+ 版本中已被保證)。這樣可在了解字典(及其他操作,例如:json 序列化或反序列化)時保持了順序。

import json

x = {str(i):i for i in range(5)}

json.loads(json.dumps(x))

# Python 2

{u'1': 1, u'0': 0, u'3': 3, u'2': 2, u'4': 4}

# Python 3

{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
           

這同樣适用于 kwargs(在 Python 3.6+ 中),即按照 kwargs 在參數中出現的順序來儲存。在涉及到資料流這個順序是至關重要的,而以前我們不得不用一種麻煩的方法來實作。

from torch import nn

# Python 2

model = nn.Sequential(OrderedDict([

          ('conv1', nn.Conv2d(1,20,5)),

          ('relu1', nn.ReLU()),

          ('conv2', nn.Conv2d(20,64,5)),

          ('relu2', nn.ReLU())

        ]))

# Python 3.6+, how it *can* be done, not supported right now in pytorch

model = nn.Sequential(

    conv1=nn.Conv2d(1,20,5),

    relu1=nn.ReLU(),

    conv2=nn.Conv2d(20,64,5),

    relu2=nn.ReLU())

)                   

你注意到了嗎?命名的惟一性也是自動檢查的。

疊代拆封

# handy when amount of additional stored info may vary between experiments, but the same code can be used in all cases

model_paramteres, optimizer_parameters, *other_params = load(checkpoint_name)

# picking two last values from a sequence

*prev, next_to_last, last = values_history

# This also works with any iterables, so if you have a function that yields e.g. qualities,

# below is a simple way to take only last two values from a list

*prev, next_to_last, last = iter_train(args)           

使用預設的 pickle 工具更好的壓縮數組

# Python 2

import cPickle as pickle

import numpy

print len(pickle.dumps(numpy.random.normal(size=[1000, 1000])))

# result: 23691675

# Python 3

import pickle

import numpy

len(pickle.dumps(numpy.random.normal(size=[1000, 1000])))

# result: 8000162           

節省三倍空間,并且更快速。實際上 protocol=2 參數可以實作相同的壓縮(但是速度不行),但是使用者基本上都會忽視這個選項(或者根本沒有意識到)。

更安全的解析

labels = <initial_value>

predictions = [model.predict(data) for data, labels in dataset]

# labels are overwritten in Python 2

# labels are not affected by comprehension in Python 3
           

超級簡單的 super 函數

在 Python 2 中,super(…) 是代碼裡常見的錯誤源。

# Python 2

class MySubClass(MySuperClass):

    def __init__(self, name, **options):

        super(MySubClass, self).__init__(name='subclass', **options)

# Python 3

class MySubClass(MySuperClass):

    def __init__(self, name, **options):

        super().__init__(name='subclass', **options)           

更多關于 super 函數及其方法的解析順序參見 stackoverflow.

更好的 IDE:支援變量注釋

使用類似 Java、C# 這類程式設計語言最享受的就是 IDE 會給出非常棒的建議,因為在執行程式前每種辨別符都是已知的。

在 Python 中這是很難實作的,但是變量注釋可以幫你

  • 以一種清晰的格式寫出你的期望值
  • 從 IDE 中得到很好的建議
愉快地遷移到 Python 3

這是一個 PyCharm 中使用變量注釋的例子。即使你使用的函數是未注釋的(比如:由于向後相容),這也仍然生效。

多重拆封

如下是現在如何合并兩個字典:

x = dict(a=1, b=2)

y = dict(b=3, d=4)

# Python 3.5+

z = {**x, **y}

# z = {'a': 1, 'b': 3, 'd': 4}, note that value for `b` is taken from the latter dict.           

可參見 StackOverflow 中的文章與 Python 2 比較。

同樣的方法也适用于清單(list),元組(tuple)和集合(set)(a,b,c 是任意可疊代對象):

[*a, *b, *c] # list, concatenating

(*a, *b, *c) # tuple, concatenating

{*a, *b, *c} # set, union           

對于使用的 *args 和 **kwargs 的函數也同樣支援:

Python 3.5+

do_something(**{**default_settings, **custom_settings})

# Also possible, this code also checks there is no intersection between keys of dictionaries

do_something(**first_args, **second_args)           

永不過時的 API:使用僅帶關鍵字的參數

我們考慮下這段代碼

model = sklearn.svm.SVC(2, 'poly', 2, 4, 0.5)
           

很明顯,這段代碼的作者還沒有掌握 Python 的代碼風格(作者極可能是剛從 C++ 或者 Rust 跳過來的)。很不幸,這個問題不僅僅是風格的問題,因為在 SVC 函數中改變參數順序(增或删)會導緻代碼崩潰。特别是函數 sklearn 會經常對大量的算法參數進行重拍序或重命名以保持和 API 的一緻性。每次的重構都可能導緻破壞代碼。

在 Python 3 中,庫的編寫者可使用 * 來明确的命名參數:

class SVC(BaseSVC):

    def __init__(self, *, C=1.0, kernel='rbf', degree=3, gamma='auto', coef0=0.0, ... )           
  • 現在使用者必須明确輸入參數名,比如:sklearn.svm.SVC(C=2, kernel=’poly’, degree=2, gamma=4, coef0=0.5)
  • 這種機制将 API 的可靠性和靈活性進行了極好的融合

次重點:math 子產品中的常量

# Python 3

math.inf # 'largest' number

math.nan # not a number

max_quality = -math.inf  # no more magic initial values!

for model in trained_models:

    max_quality = max(max_quality, compute_quality(model, data))
           

次重點:單整型

Python 2 提供兩種基本的整型:int 型(64 位有符整型)和用于長時計算的 long 型(C++ 後非常讓人困惑)。

Python 3 有單精度的 int 型,它整合了長時計算的要求。

下面是怎樣檢查整型值:

isinstance(x, numbers.Integral) # Python 2, the canonical way

isinstance(x, (long, int))      # Python 2

isinstance(x, int)              # Python 3, easier to remember           

其他

  • Enums 理論是有用處的,但:
  • 在 python 的資料棧中,字元串輸入已被廣泛采用
  • Enums 似乎并不與 numpy 互動,也不屬于 pandas 範疇
  • 協程聽起來也很有希望做資料流程(參考 David Beazley 的幻燈片),但是我還沒有看到他們被采用。
  • Python 3 有穩定的 ABI
  • Python 3 支援 unicode(是以 ω = Δφ / Δt 是可以的),但是最好還是使用雖舊但好用的 ASCII 碼。
  • 一些庫,比如:jupyterhub(雲服務版 jupyter)、django 和新版 ipython,隻支援 Python 3,是以一些聽起來對你無用的特性卻對那些你也許隻想用一次的庫非常有用。

資料科學特有的代碼遷移難題(以及如何解決它們)

  • 放棄支援嵌套參數
map(lambda x, (y, z): x, z, dict.items())           
  • 然而,它依然能很好的對不同的了解起效。
{x:z for x, (y, z) in d.items()}           

通常來說,在 Python 2 和 Python 3 之間,了解也更好于‘翻譯’。

  • map()、.keys()、.values()、.items() 傳回的是疊代器而不是清單。疊代器的主要問題是:
    • 沒有瑣碎的分片
    • 不能疊代兩次

幾乎全部的問題都可以通過将結果轉化為清單來解決。

  • 當遇到麻煩時,參見 Python 問答:我如何遷移到 Python 3

使用 python 教授機器學習和資料科學的主要問題

教授者應該首先花時間講解什麼是疊代器,它不能像字元串一樣被分片、級聯、倍乘、疊代兩次(以及如何處理)。

我認為大部分教授者會很高興規避這些細節,但是現在這幾乎是不可能的。

結論

Python 2 與 Python 3 共存了将近 10 年,但是我們應當轉移到 Python 3 了。

遷移到僅有 Python 3 的代碼庫後,研究所寫的代碼和産品開發的代碼都會變得更簡短、更可讀、更安全。

現在大部分庫同時支援這兩個 Python 版本。我都有點等不及了,工具包放棄支援 Python 2 享受新語言特性的美好時刻快來吧。

遷移後代碼絕對會更順暢,參見“我們再也不要向後相容啦!”

參考

  • Key differences between Python 2.7 and Python 3.x
  • Python 3 for scientists