標籤:
飛躍式發展的後現代Python世界
如果現代Python有一個標誌性特性,那麼簡單說來便是Python對自身定義的越來越模糊。在過去的幾年的許多項目都極大拓展了Python,並重建了“Python”本身的意義。
與此同時新技術的湧現侵佔了Python的份額,並帶來了新的優勢:
- Go - ( Goroutines, Types, Interfaces )
- Rust - ( Traits, Speed, Types )
- Julia - ( Speed, Types, Multiple Dispatch )
- Scala - ( Traits, Speed, Types )
- Clojure ( Metaprogramming, DSLs, Protocols )
這是一篇Python對這些新技術、新庫及模型響應的簡短指南:
元編程
MacroPy 是一個元編程架構,它提供了多種文法結構,將現代語言元素編譯成標準的Python代碼,擴充了Python AST。舉個例子,我們可以實現對代數資料類型的衡量:
from macropy.case_classes import case@caseclass Nil(): pass@caseclass Cons(x, xs): passCons(1, Cons(2, Cons(3, Nil())))
然後模式和聲明的類型相匹配了:
def reduce(op, my_list): with switch(my_list): if Cons(x, Nil()): return x elif Cons(x, xs): return op(x, reduce(op, xs))
消失的部分仍然是一個沿著camlp4路線,可擴充階段的元編程系統。但是 Mython提供了一個pgen2解析架構,給引用塊定義了新的文法,來解決這個問題。
my[namedtupledef] Point(x, y): passmy[c]: int add (int x, int y) { return x + y; }print "Regular Python"
類型
Python 是動態類型語言,並且引以為傲。我當然不希望對類型的“聖戰”煽風點火,但同時肯定有大學派認為構建可靠的應用程式需要有比只使用單元測試更加有力的保障。Benjamin Pierce對類型系統的定義如下:
...一種易於處理的文法,通過根據計算值的類型對片語分類證明了缺少了特定的程式行為
重點是證明有關Runspace的屬性, 所有程式行為的Runspace替代了只是簡單地羅列有限種情況的Runspace。全靜態類型對於Python是否是正確的選擇讓人十分疑惑,但是在過度的動態類型和靜態類型保證之間肯定有更加合適的方案。MyPy project找到了一個不錯的平衡點,允許有類型的和沒有類型的代碼能夠同時存於語言的超集中。例如:
def simple_typed(x : int, y : int) -> int: return x + ysimple_typed(1, 2) # Type-checks succesfully# Fails: Argument 2 to "simple_typed" has incompatible type # "float"simple_typed(1, 2.0)# Fails: Argument 2 to "simple_typed" has incompatible type "str"simple_typed(1, "foo")
當然對C語言沒有太多的用處。所以我們不只限於簡單類型的函數,參數類型也有泛型,指標類型和各種各樣內建的類型級的函數。
from typing import Iterator, typevar, Generic, Function, ListT = typevar(‘T‘)def example_typed(x : Iterator[int]) -> Iterator[str]: for i in x: yield str(i)def example_generic(x : Iterator[T]) -> Iterator[T]: for i in x: yield i
我們也能定義更加進階的泛型結構例如函子和單元
a = typevar(‘a‘)b = typevar(‘b‘)class Functor(Generic[a]): def __init__(self, xs : List[a]) -> None: self._storage = xs def iter(self) -> Iterator[a]: return iter(self._storage)def fmap(f : Function[[a], b], xs : Functor[a]) -> Functor[b]: return Functor([f(x) for x in xs.iter()])class Monad(Generic[a]): def __init__(self, val : a) -> None: self.val = valclass IdMonad(Monad): # Monad m => a -> m a def unit(self, x : a) -> Monad[b]: return IdMonad(x) # Monad m => m a -> (a -> m b) -> m b def bind(self, x : Monad[a], f : Function[[a], Monad[b]]) -> Monad[b]: return f(x.val) # Monad m => m (m a) -> m a def join(self, x : Monad[Monad[a]]) -> Monad[a]: return x.val
速度
“高效能”Python最近最重要的進展是Pandas庫提供的更高等級DataFrame容器的開發。Pandas混合各種Python進行操作,對於某些操作使用NumPy,其它的使用Cython,對於某些內部雜湊表甚至使用C語言。Panda底層架構非教條式的方法已經讓它成為資料分析領域的標準庫。Pandas的開發體現了很多讓數值Python生態系統成功的東西。
In [1]: from pandas import DataFrameIn [2]: titanic = DataFrame.from_csv(‘titanic.csv‘)In [3]: titanic.groupby(‘pclass‘).survived.mean()pclass1st 0.6191952nd 0.4296033rd 0.255289Name: survived
然而改善Python效能最近的嘗試是利用LLVM編譯器有選擇的編譯某些Python程式碼片段為本地代碼。雖然不同的技術的實現方式不同,但是大部分與下述方式類似:
- 在函數上添加@jit或@compile這樣的裝飾器。
- 函數的AST或者bytecode被提取出來放入編譯器流水線,在流水線中被映射到內部AST,給定特定的輸入類型集合決定如何將給定的函數邏輯降低為機器代碼。
- 編譯過的函數與一群組類型一起被調用,參數被檢查過,代碼在給定類型下產生。產生的程式碼連同參數被緩衝使得接下來的調用直接分發到本地代碼。
這些項目增加了大家對Python語言技術和llvmpy項目開發的興趣,我猜測llvmpy在Python的曆史上位元定的JIT編譯器更重要。
最簡單的例子(來自極好的Kaleidescope教程)是建立一個簡單的本地乘加函數,然後通過解箱三個Python整數調用它:
import llvm.core as lcimport llvm.ee as lemod = lc.Module.new(‘mymodule‘)i32 = lc.Type.int(32)funty = lc.Type.function(lc.Type.int(), [i32, i32, i32])madd = lc.Function.new(mod, funty, "multiply")x = madd.args[0]y = madd.args[1]z = madd.args[2]block = madd.append_basic_block("L1")builder = lc.Builder.new(block)x0 = builder.mul(x, y)x1 = builder.add(x0, z)builder.ret(x1)print modtm = le.TargetMachine.new(features=‘‘, cm=le.CM_JITDEFAULT)eb = le.EngineBuilder.new(mod)engine = eb.create(tm)ax = le.GenericValue.int(i32, 1024)ay = le.GenericValue.int(i32, 1024)az = le.GenericValue.int(i32, 1024)ret = engine.run_function(madd, [ax, ay, az])print ret.as_int()print mod.to_native_assembly()
上述代碼編譯產生下述LLVM IR。
define i32 @multiply(i32, i32, i32) {L1: %3 = mul i32 %0, %1 %4 = add i32 %3, %2 ret i32 %4}
雖然這個例子不太直觀,但是可以產生很快的JIT‘d函數,與NumPy這樣的庫整合的很好,把資料做為大塊的解箱記憶體儲存。
介面
分解行為到可組合的單元,而不是顯式的繼承階層是一個Python沒有解決好的問題,經常導致噩夢般的複雜的使用mixin。然而通過使用ABC模組模仿靜態定義的介面可以緩解這個問題。
import heapqimport collectionsclass Heap(collections.Sized): def __init__(self, initial=None, key=lambda x:x): self.key = key if initial: self._data = [(key(item), item) for item in initial] heapq.heapify(self._data) else: self._data = [] def pop(self): return heapq.heappop(self._data)[1] def push(self, item): heapq.heappush(self._data, (self.key(item), item)) def len(self): return len(self._data)
例如建立一個等價類別,讓所有類的執行個體實現eq()方法。我們可以這樣做::
from abc import ABCMeta, abstractmethodclass Eq(object): __metaclass__ = ABCMeta @classmethod def __subclasshook__(cls, C): if cls is Eq: for B in C.__mro__: if "eq" in B.__dict__: if B.__dict__["eq"]: return True break return NotImplementeddef eq(a, b): if isinstance(a, Eq) and isinstance(b, Eq) and type(a) == type(b): return a.eq(b) else: raise NotImplementedErrorclass Foo(object): def eq(self, other): return Trueclass Fizz(Foo): passclass Bar(object): def __init__(self, val): self.val = val def eq(self, other): return self.val == other.valprint eq(Foo(), Foo())print eq(Bar(1), Bar(1))print eq(Foo(), Bar(1))print eq(Foo(), Fizz())
然後擴充這種類型的介面概念到多參數的函數,使得查詢__dict__越來越可能發生,在組合的情況下很脆弱。問題的關鍵是分解所有的事情到單一類型不同的介面,當我們真正想要的是聲明涵蓋一組多類型的介面時。OOP中的這種缺點是 運算式問題的關鍵。
諸如Scala、Haskell和Rust這樣的語言以trait和typeclass這樣的形式提供該問題的解決方案。例如Haskell可以自動地為所有類型的交叉產品推匯出微分方程。
instance (Floating a, Eq a) => Floating (Dif a) where pi = C pi exp (C x) = C (exp x) exp (D x x‘) = r where r = D (exp x) (x‘ * r) log (C x) = C (log x) log [email protected](D x x‘) = D (log x) (x‘ / p) sqrt (C x) = C (sqrt x) sqrt (D x x‘) = r where r = D (sqrt x) (x‘ / (2 * r))
非同步編程
在這個主題下,我們還是有很多縫縫補補的解決方案,解決了部分的問題,但是引入了一整與常規Python背道而馳的套限制和模式。Gevent通過剪接底層C堆棧保持了Python自己的一致性。產生的API非常優雅,但是使得推理控制流程和異常非常複雜。
import geventdef foo(): print(‘Running in foo‘) gevent.sleep(0) print(‘Explicit context switch to foo again‘)def bar(): print(‘Explicit context to bar‘) gevent.sleep(0) print(‘Implicit context switch back to bar‘)gevent.joinall([ gevent.spawn(foo), gevent.spawn(bar),])
控制流程展示在下面:
通過對標準庫相當不優美的縫縫補補(monkey-patching),我們可以模仿Erlang式帶有非同步進入點和內部狀態的actor行為:
import geventfrom gevent.queue import Queuefrom SimpleXMLRPCServer import SimpleXMLRPCServerclass Actor(object): _export = [ ‘push‘, ] def __init__(self, address): self.queue = Queue() self._serv = SimpleXMLRPCServer(address, allow_none=True, logRequests=False) self.address = address for name in self._export: self._serv.register_function(getattr(self, name)) def push(self, thing): self.queue.put(thing) def poll(self): while True: print(self.queue.get()) def periodic(self): while True: print(‘PING‘) gevent.sleep(5) def serve_forever(self): gevent.spawn(self.periodic) gevent.spawn(self.poll) self._serv.serve_forever()def main(): from gevent.monkey import patch_all patch_all() serve = Actor((‘‘, 8000)) serve.serve_forever()
DSLs
Z3工程是嵌在Python對象層的擴充API。用Z3的執行個體來解決N皇后問題可以被描述為Python運算式和擴充SMT來解決問題:
from Z3 import *Q = [ Int(‘Q_%i‘ % (i + 1)) for i in range(8) ]# Each queen is in a column {1, ... 8 }val_c = [ And(1 <= Q[i], Q[i] <= 8) for i in range(8) ]# At most one queen per columncol_c = [ Distinct(Q) ]# Diagonal constraintdiag_c = [ If(i == j, True, And(Q[i] - Q[j] != i - j, Q[i] - Q[j] != j - i)) for i in range(8) for j in range(i) ]solve(val_c + col_c + diag_c)
在Theano,SymPy,PySpark中的其它工程大量使用基於Python運算式的重載操作符的方式。
from sympy import Symbolfrom sympy.logic.inference import satisfiablex = Symbol(‘x‘)y = Symbol(‘y‘)satisfiable((x | y) & (x | ~y) & (~x | y))
飛躍式發展的後現代 Python 世界