Python Rust 迭代器對比

PrivateRookie發表於2019-07-10

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

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

# 列表
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))

除了基本資料型別,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 內建函式可以獲取迭代器的物件。如果物件實現了能返回迭代器的 iter 方法,那麼物件就是可迭代的。序列都可以迭代;實現了 getitem 方法,而且其參 數是從零開
始的索引,這種物件也可以迭代

迭代器則定義為
迭代器是這樣的物件:實現了無引數的 next 方法,返回序列中的下一個元素;如 果沒有元素了,那麼丟擲 StopIteration 異常。Python 中的迭代器還實現了 iter 方 法,因此
迭代器也可以迭代。

也就是說每次對可迭代物件呼叫 iter(x) 都將返回一個新的迭代器。

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

class MyData:
    def __init__(self, values):
        # 假設 value 為列表
        self.values = values

    def __iter__(self):
        return self

    def __next__(self):
        # ???
        raise NotImplementedError()

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

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

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

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

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

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

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

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

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

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

我們可以像 Python 那樣使用 for 迴圈迭代

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

for item in v1 {
    println!("{}", item);
}

std::iter::Iterator 只要求實現 next 方法即可,下面是一個官方文件中的例子

// 首先定義一個結構體,作為“迭代器”
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<Self::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);

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

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

for x in values {
    println!("{}", x);
}

去掉語法糖後相當於

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
}

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

這裡又涉及到另一個特性 std::iter::IntoIterator,這個特性可以把某些東西變成一個迭代器。

IntoInterator 宣告如下:

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

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

  1. 實現了 IntoIterator 特性的結構體是一個“可迭代物件”
  2. 實現了 Iterator 特性的結構體一個“迭代器”
  3. for 迴圈會嘗試呼叫結構的 into_iter 獲得一個新的“迭代器”,當迭代器返回 None 時提示迭代結束

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

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

struct DataIterator {
    current: usize,
    data: Vec<i32>,
}

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

impl Iterator for DataIterator {
    type Item = i32;

    fn next(&mut self) -> Option<i32> {
        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);
    }
}

總結

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

延申閱讀

  1. 《流暢的Python》 - 第14章,可迭代的物件、迭代器和生成器
  2. std::iter::IntoIterator - Rust
  3. std::iter::Iterator - Rust
  4. 《Rust 程式設計之道》 6.3 迭代器

知乎專欄:Python Rust 迭代器對比 - PrivateRookie的文章 - 知乎

Github 部落格:

本作品採用《CC 協議》,轉載必須註明作者和本文連結
多少事,從來急。天地轉,光陰迫。

相關文章