3. 物件迭代與反迭代相關問題與解決技巧
阿新 • • 發佈:2018-12-15
一. 如何實現可迭代物件和迭代器物件
實際案列
某軟體要求, 從網路抓取各個城市氣溫資訊, 並依次顯示:
北京: 15-20
天津: 17-22
長春: 12-18
...
如果一次抓取所有城市氣溫再顯示, 顯示第一個城市氣溫時, 有很高的延時,
並且浪費儲存空間, 我們期望以“用時訪問”的策略, 並且能把所有城市氣溫
封裝到一個物件裡, 可用for語句進行迭代。 如何解決?
可迭代物件和迭代器原理
from collections import Iterable, Iterator l = [1,2,3,4,5] isinstance(l, Iterable) # True 列表是可迭代物件 issubclass(list, Iterable) # True 列表是Iterable的子類 issubclass(str, Iterable) # True 字串是Iterable的子類 issubclass(dict, Iterable) # True 字典是Iterable的子類 it = iter(l) # 呼叫了__iter__方法 生成Iterator 迭代器 next(it) # next 可以對迭代器進行迭代 直到丟擲 StopIteration異常 無法next list(it) # 迭代器傳給列表, 可以將剩下沒next的 變成列表 for x in it: # 迭代器也是一個可迭代物件 print(x) isinstance(it, Iterable) # True 迭代器也是一個可迭代物件 it.__iter__() is it # True 或者iter(it) 都會返回它自身
解決方法
Step1: 實現一個迭代器物件WeatherIterator, __next__方法 每次返回一個城市的氣溫
Step2: 實現一個可迭代物件WeatherIterable, __iter__方法 返回一個WeatherIterator物件
from collections import Iterable, Iterator import requests class WeatherIterator(Iterator): def __init__(self, caties): self.caties = caties self.index = 0 def __next__(self): if self.index == len(self.caties): raise StopIteration city = self.caties[self.index] self.index += 1 return self.get_weather(city) def get_weather(self, city): url = 'http://wthrcdn.etouch.cn/weather_mini?city=' + city # 獲取相應城市氣溫 r = requests.get(url) data = r.json()['data']['forecast'][0] return city, data['high'], data['low'] class WeatherIterable(Iterable): def __init__(self, cities): self.cities = cities def __iter__(self): return WeatherIterator(self.cities) def show(w): for x in w: print(x) w = WeatherIterable(['北京', '上海', '廣州'] * 10) show(w)
程式碼的注意點
如果不要WeatherIterable, 直接用WeatherIterator, 也可以。
但是 如果 show 結束 再次呼叫show 就什麼都沒有了。
因為Iterator 迭代器是一次性的, 但迭代器物件Iterable不是一次性的
二. 如何使用生成器函式實現可迭代物件
實際案列
實現一個可迭代物件的類, 它能迭代出給定範圍內所有素數: pn = PrimeNumbers(1, 30) for k in pn: print k 輸出結果: 2 3 5 7 11 13 17 19 23 29 用一節 中的 方法也能實現, 但用yield生成器 會更簡單
解決方案
將該類的__iter__方法實現生成器函式, 每次yield返回一個素數
from collections import Iterable
class PrimeNumbers(Iterable):
def __init__(self, a, b):
self.a = a
self.b = b
def __iter__(self):
for k in range(self.a, self.b + 1):
if self.is_prime(k):
yield k
def is_prime(self, k):
return False if k < 2 else all(map(lambda x: k % x, range(2, k))) # k%x 為0 必然不是素數
pn = PrimeNumbers(1, 30)
for n in pn:
print(n)
三. 如何進行反向迭代以及如何實現反向迭代
實際案例
實現一個連續浮點數發生器FloatRange(和range類似), 根據給定範圍(start, end) 和步進值(step)
產生一些連續浮點數, 如迭代FloatRange(3.0, 4.0, 0.2) 可產生序列:
正向: 3.0->3.2->3.4->3.6->3.8->4.0
反向: 4.0->3.8->3.6->3.4->3.2->3.0
實現反向迭代
from decimal import Decimal # 專門處理10進值浮點數
class FloatRange:
def __init__(self, a, b, step):
# 有時候會發生3.400000000000000004 這樣的誤差持續放大的問題,
# 使用Decimal解決, 產生10進值的浮點數 就沒有誤差。 在需要精準資料時, 常用
self.a = Decimal(str(a))
self.b = Decimal(str(b))
self.step = Decimal(str(step))
def __iter__(self):
t = self.a
while t <= self.b:
yield float(t) # 使用者需要的是python的float, 需要轉換。另外的注意點:decimal物件是無法與float進行運算的
t += self.step
def __reversed__(self): # 實現反向迭代 reversed
t = self.b
while t >= self.a:
yield float(t)
t -= self.step
fr = FloatRange(3.0, 4.0, 0.2)
for x in fr:
print(x)
print('-' * 20)
for x in reversed(fr):
print(x)
四. 如何對可迭代物件做切片操作
實際案列
有某個文字檔案, 我們想讀取其中某範圍的內容如100~300 行之間的內容, python中檔案是可迭代物件,
我們是否可以使用類似列表切片的方式得到一個100~300行檔案內容的生成器?
f = open('/var/log/dpkg.log.1')
for line in f[100:300]: # 可以嗎? 答案是 不可以
原理解析
l = list(range(10))
l[2:8] # 即 l.__getitem__(slice(2,8)) slice 是切片函式, 對應[2:8]
l[2:8: 2] # 即 l.__getitem__(slice(2,8, 2))
l[3] # 即 l.__getitem__(3)
f = open('/var/log/dkpg.log.1')
# f.readlines可以 使用切片, 但會把真個檔案都讀入記憶體, 不推薦
解決方案
使用 itertools.islice, 它能返回一個迭代物件切片的生成器
from itertools import islice
f = open('/var/log/dkpg.log.1')
for line in islice(f, 100-1, 300, 2): # 引數: 可迭代物件 開始行(預設從0開始) 結束行 步進
print(line)
自己實現簡化版的islice
def my_islice(iterable, start, end, step=1):
tmp = 0
for i, x in enumerate(iterable):
if i >= end:
break
if i >= start:
if tmp == 0:
tmp =step
yield x
tmp -= 1
五. 如何在一個for語句中迭代多個可迭代物件
實際案例
情況1(並行): 某班學生期末考試成績, 語文, 數學, 英語 分別儲存在3個列表中, 同時迭代三個列表, 計算每個學生的總分
情況2(序列): 某年級有4個班, 每次考試每班英語成績分別儲存在4個列表中, 依次迭代每個列表, 統計全學年成績高於90分的人數
解決方案
- 並行: 使用zip函式 將多個可迭代物件 合併成一個
from random import randint
chinese = [randint(60, 100) for _ in range(20)]
math = [randint(60, 100) for _ in range(20)]
english = [randint(60, 100) for _ in range(20)]
t = []
for s1, s2, s3 in zip(chinese, math, english): # zip得到的元組 是傳入 的可迭代物件 中最少的
t.append(s1+s2+s3) # 得到三科總分
- 序列:使用標準庫中的itertools.chain, 它能將多個可迭代物件連線
class1 = [randint(60, 100) for _ in range(20)]
class2 = [randint(60, 100) for _ in range(20)]
class3 = [randint(60, 100) for _ in range(23)]
class4 = [randint(60, 100) for _ in range(25)]
from itertools import chain
len([for x in chain(class1, class2, class3, class4) if x > 90]) # 得到分數大於90的 學生人數