02.02 Python Rust 迭代器對比


Python Rust 迭代器對比


迭代是數據處理的基石,而 Python 中所有集合都可以迭代,這是 Python 讓使用者感到非常方便的特徵之一。

下面是一些在 Python 中經常使用的迭代模式

<code># 列表
for i in [1, 2, 3, 4]:
print(i)

# 字典
di = {'a': 1, 'b': 2, 'c': 3}
# 迭代鍵
for k in di.keys():
print(k)
# 迭代鍵值
for k, v in di.items():
print('{}: {}'.format(k, v))/<code>

除了基本數據類型,Python 也支持為自定義的數據類型實現迭代器協議。Python 解釋器在需要迭代對象 x 時會自動調用 iter(x)。內置的 iter 函數有如下作用。

  1. 檢查對象是否實現了 __iter__ 方法,如果實現了就調用它,__iter__ 方法返回一個迭代器
  2. 如果沒有實現 __iter__ 方法,但是實現了 __getitem__ 方法,Python 會創建一個迭代器,嘗試按順序(從索引0)獲取元素。
  3. 如果上述兩個嘗試失敗,Python 會拋出 TypeError 異常,提示該元素不可迭代。

所以如果我們要讓某個對象是可迭代對象,只需要實現 __iter__,這個方法要求返回一個迭代器,那什麼是迭代器呢? Python 中標準的迭代器接口有兩個方法。

__next__

返回下一個可用的元素,如果元素,拋出 StopIteration 異常。

__iter__

返回迭代器自身,即 self,以便在應該使用可迭代對象的地方使用迭代器,如 for 循環中。

這裡需要說明的一點是,可迭代對象與迭代器是不同的,《流暢的 Python》這樣定義可迭代對象

也就是說每次對可迭代對象調用 iter(x) 都將返回一個新的迭代器。

那如果為一個可迭代對象實現 __next__ 方法,即把這個可迭代對象變成自身的可迭代對象會怎樣呢?沒人阻止你這樣做,但當你真正為這個對象實現這兩個方法時,你會發現麻煩不斷。舉個例子

<code>class MyData:
def __init__(self, values):

# 假設 value 為列表
self.values = values

def __iter__(self):
return self

def __next__(self):
# ???
raise NotImplementedError()/<code>

按照協議 __next__ 應該返回下一個元素或者拋出 StopIteration,顯然我們需要一個屬性存儲當前迭代位置,所以應該似乎應該這樣寫

<code>class MyData:
def __init__(self, values):
self.values = values
# 記錄當前迭代位置
self.current = 0

def __iter__(self):
# 每次調用重頭開始迭代
self.current = 0
return self

def __next__(self):
if self.current < len(self.values):
value = self.values[self.current]
self.current += 1
return value
else:
raise StopIteration/<code>

但考慮這樣一種情況,我們調用2次 iter,交替迭代獲得的2個迭代器,預期行為應該是2個迭代器不會干涉,但如果按上述代碼實現 MyData 對象行為並不符合預期。

<code>data = MyData([1, 2, 3, 4, 5])

data_iter1 = iter(data)
print(next(data_iter1)) # 結果為1

print(next(data_iter1)) # 結果為2

data_iter2 = iter(data)
print(next(data_iter2)) # 結果為1

print(next(data_iter1)) # 預期為3,但得到2/<code>

如果把 current 屬性變為列表,每次調用 iter 增加一個元素表示新的迭代器當前位置呢?但又會導致 __next__ 變得非常複雜,因為它必須找到不同迭代器對應當前位置,這樣才能保證正確的迭代行為。為什麼我們的迭代實現如此複雜呢?根本原因在於 __iter__ 總是返回自身,換言之,調用 iter 的迭代器都是一樣,這其實破壞了 每次調用 iter 返回新的迭代器 這一設計。

解決難題辦法很簡單,遵循設計,把可迭代對象和迭代器拆開。

<code>class MyData:
def __init__(self, values):
self.values = values

def __iter__(self):
return DataIterator(list(self.values))


class DataIterator:
def __init__(self, values):
self.values = values
self.current = 0

def __iter__(self):
return self

def __next__(self):

if self.current < len(self.values):
value = self.values[self.current]
self.current += 1
return value
else:
raise StopIteration/<code>

現在 __iter__ 將會返回新的迭代器,每個迭代器都保存著自身狀態,這讓我們不必費心費力第維護迭代器狀態。

所以,把可迭代對象變成其自身的迭代器是條歧路,反設計的。

在 Rust 中,迭代也遵循著相似的設計,Rust 中實現了 Iterator 特性的結構體就被認為是可迭代的。

我們可以像 Python 那樣使用 for 循環迭代

<code>let v1 = vec![1, 2, 3, 4, 5];

for item in v1 {
println!("{}", item);
}/<code>

https://doc.rust-lang.org/std/iter/trait.Iterator.html 只要求實現 next 方法即可,下面是一個https://doc.rust-lang.org/std/iter/index.html#implementing-iterator中的例子

<code>// 首先定義一個結構體,作為“迭代器”
struct Counter {
count: usize,
}

// 實現靜態方法 new,相當於構造函數
// 這個方法不是必須的,但可以讓我更加方便
// 地使用 Counter

impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}

// 實現 Iterator 特性
impl Iterator for Counter {
// 確定迭代器的返回值類型
type Item = usize;

// 只有 next() 是必須實現的方法
// Option<usize> 也可以寫成 Option<:item>
fn next(&mut self) -> Option<usize> {
// 增加計數
self.count += 1;

// 到 5 就返回 :)
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}

let mut counter = Counter::new();

let x = counter.next().unwrap();
println!("{}", x);

let x = counter.next().unwrap();
println!("{}", x);

let x = counter.next().unwrap();
println!("{}", x);

let x = counter.next().unwrap();
println!("{}", x);

let x = counter.next().unwrap();
println!("{}", x);/<usize>/<usize>/<code>

與 for 循環使用時,Python 使用 StopIteration 告訴編譯是時候定製循環了,在 Rust 則是 None,所以 next 方法返回值為 Option 。其實使用 for 循環是一種語法糖

<code>let values = vec![1, 2, 3, 4, 5];

for x in values {
println!("{}", x);
}/<code>

去掉語法糖後相當於

<code>let values = vec![1, 2, 3, 4, 5];
{
let result = match IntoIterator::into_iter(values) {
mut iter => loop {
let next;
match iter.next() {
Some(val) => next = val,
None => break,
};
let x = next;
let () = { println!("{}", x); };
},
};
result
}/<code>

編譯器會對 values 調用 into_iter 方法,獲取迭代器,接著匹配迭代器,一次又一次地調用迭代器的 next 方法,直到返回 None,這時候終止循環,迭代結束。

這裡又涉及到另一個特性 https://doc.rust-lang.org/std/iter/trait.IntoIterator.html,這個特性可以把某些東西變成一個迭代器。

IntoInterator 聲明如下:

<code>pub trait IntoIterator
where
<:intoiter as="" iterator="">::Item == Self::Item,
{
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}
/<code>

類比於 Python 中的概念,可以做出以下結論:

  1. 實現了 IntoIterator 特性的結構體是一個“可迭代對象”
  2. 實現了 Iterator 特性的結構體一個“迭代器”
  3. for 循環會嘗試調用結構的 into_iter 獲得一個新的“迭代器”,當迭代器返回 None 時提示迭代結束

基於以上結論,我們可以實現 Python 例子中類似的代碼

<code>#[derive(Clone)]
struct MyData{
values: Vec,
}

struct DataIterator {
current: usize,
data: Vec,
}

impl DataIterator {
fn new(values: Vec) -> DataIterator {
DataIterator {
current: 0,
data: values
}
}
}

impl Iterator for DataIterator {
type Item = i32;

fn next(&mut self) -> Option
{
if self.current < self.data.len() {
let ret = Some(self.data[self.current]);
self.current += 1;
ret
} else {
None
}
}
}

impl IntoIterator for MyData {
type Item = i32;
type IntoIter = DataIterator;

fn into_iter(self) -> DataIterator {
DataIterator::new(self.values)
}
}

fn main() {
let data = MyData { values: vec![1, 2, 3, 4] };
for item in data {
println!("{}", item);
}
}
/<code>


總結

Rust 不愧是一門多範式的現代編程語言,如果你之前對某個語言有相當深入的瞭解,在學習 Rust 是總會有“喔,這不是xxx嗎”的感覺。雖然之前閱讀過 《流暢的Python》,但在可迭代對象與迭代器這一章並沒有太多影響,因為在使用 Python 時真正要我實現迭代接口的場景非常少;直到最近學習 Rust,在嘗試使用 Rust 的 Iterator 特性為我的結構實現與 for 循環交互時被 Iterator 和 IntoInterator 特性高的有些蒙圈。最後是靠著 Python 和 Rust 相互對比,弄清迭代器與可迭代對象的區別後才感覺自己真正弄懂了迭代這一重要特性。

延申閱讀


  1. [http://www.ituring.com.cn/book/1564](http://www.ituring.com.cn/book/1564) - 第14章,可迭代的對象、迭代器和生成器
  2. https://doc.rust-lang.org/std/iter/trait.IntoIterator.html
  3. https://doc.rust-lang.org/std/iter/trait.Iterator.html
  4. 《Rust 編程之道》 6.3 迭代器


分享到:


相關文章: