質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Q&A

解決済

3回答

558閲覧

Python3のyieldでのスレッド作成について

sdice

総合スコア12

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

0グッド

0クリップ

投稿2018/08/07 14:57

前提・実現したいこと

Python3でThreadPoolExecutorを使用してスレッドを実行しようと、
下記の様なソースコードを試しました。

実現したいことは関数「create_tran(tg_list)」で出力で引数のリストの文字列を順に出力するスレッドを作成し、ThreadPoolExecutorに読み込ませて平行にメッセージを出力したいです。
(例:出力したいメッセージ)

下記のソースコードを実行したところ、リストの最終文字(ccccc)が出力されるスレッドが三回実行されてしまいます。
(例:下記のソースコードを実行した場合の出力)

ジェネレータ理解不足で申し訳ないのですが、原因とジェネレータでスレッドを作成する時の注意点などありましたらアドバイス頂きたいと考えています。

出力したい出力

aaaaa bbbbb ccccc

下記のソースコードを実行した場合の出力

ccccc ccccc ccccc

該当のソースコード

Python

1import time 2import concurrent.futures 3 4 5def create_tran(tg_list): 6 for tg_name in tg_list: 7 def _tran(): 8 time.sleep(3) 9 print(tg_name) 10 yield _tran 11 12 13def main(): 14 tg_list = ['aaaaa', 'bbbbb', 'ccccc'] 15 tran_list = create_tran(tg_list) 16 executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) 17 for tran in tran_list: 18 executor.submit(tran) 19 20 21if __name__ == "__main__": 22 main() 23

試したこと

time.sleep(3)を削除しましたら、目的の出力が行われましたが関数「_tran()」内に重い処理を記載しましたらやはり同一の現象がおこりました。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

tg_namecreate_tranのローカル名前空間に紐付いているのでfor文がまわるたびに更新されます。で、print(tg_name)の実行時にはfor文はすでに3回まわった状態なので意図と反する'ccccc'が出力されます。

python

1def create_tran(tg_list): 2 for tg_name in tg_list: 3 def _tran(): 4 time.sleep(3) 5 print(tg_name) # for文1回目の間は'aaaaa'、2回目の間は'bbbbb'、3回目の間は'ccccc' 6 yield _tran

意図する動作をさせたいなら以下のようにtg_name_tranのローカル名前空間に紐付けないとダメです。

python

1def create_tran(tg_list): 2 def _tran(tg_name): 3 def inner(): 4 time.sleep(3) 5 print(tg_name) 6 return inner 7 8 for t in tg_list: 9 yield _tran(t)

投稿2018/08/07 17:22

YouheiSakurai

総合スコア6142

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sdice

2018/08/08 09:54

誤動作する理由と修正方法を簡潔に記載して頂きありがとうございました。 ローカルの名前空間へ紐づける必要があったのですね。
guest

0

これでいけますよ。

python3

1import time 2import concurrent.futures 3 4 5def create_tran(tg_list): 6 for tg_name in tg_list: 7 def _tran(tg_name=tg_name): 8 time.sleep(3) 9 print(tg_name) 10 yield _tran 11 12 13def main(): 14 tg_list = ['aaaaa', 'bbbbb', 'ccccc'] 15 tran_list = create_tran(tg_list) 16 executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) 17 for tran in tran_list: 18 executor.submit(tran) 19 20 21if __name__ == "__main__": 22 main()

投稿2018/08/07 17:51

退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sdice

2018/08/08 09:46

短い修正で実現する方法を教えて頂きありがとうございます。
guest

0

for文の制御変数tg_nameが関数_tranからどう参照されるのかの意味をよく注意すると原因と対処がわかってくると思います。本件の本質はどちらかといえばジェネレーターやスレッドとは独立した話題であり、単にPythonのfor文の制御変数と関数スコープに関する仕様にあると思います。

単純化した例を挙げますと・・・

リスト1

python

1functions = [] 2for i in range(2): # ...(A) 3 def f(): 4 return i 5 functions.append(f) 6print([f() for f in functions])

この結果は[0 1]とはならず[1 1]となります。
なぜそうなるかを上のコードとほぼ同様の意味の別の表現で表してみると次のようになります。

リスト2

Python

1i = 0 2def f0(): 3 return i 4i = 1 5def f1(): 6 return i 7functions = [f0, f1] 8print([f() for f in functions])

functionsの要素である2つの関数がprint文の引数を計算する際に順番に実行されますがそのときのiの値は1になっています。リスト1の(A)のfor文の制御変数iもリスト2の変数iも関数fから見れば実質的には同じであり、「iは関数fのスコープの外にある変数」なので関数fが実行された時点でのiの値が参照されることになります。

(for文の繰り返し処理の内側で関数を定義したとき、感覚的にはその関数を実行した際に関数を定義した時点でのiの値が関数内部から見えるかのように誤解しがちだと思いますが実はそうではなくあくまで変数iは関数を実行した時点での値が見えるだけであることに注意してください。)


対処:
あとから実行する関数の本体から参照している変数の値を「関数を定義した時点の値」としておきたいのはよくあること(というかより自然な設計)と思います。そうするにはその変数が「関数を定義した時点の値となる」ようにクロージャーを用いるとよいと思います。

リスト1を次のように書き換えるとそうできます。

リスト3

Python

1functions = [] 2def closure_of_f(i): 3 def f(): 4 return i # (B)このiはclosure_of_fの引数である 5 return f 6 7for j in range(2): 8 functions.append(closure_of_f(j)) # (C)引数(=i)の値はjのこの時点での値により確定 9print([f() for f in functions]) 10# => [0, 1]

closure_of_f関数は引数に指定した値が変数iの値となるような環境での関数fを返してくれます。
少々わかりにくいですがclosure_of_fという関数を定義せずともlambdaを用いると次のようにも書けます。

リスト4

Python

1def g(i): 2 return i 3 4functions = [] 5for i in range(2): 6 functions.append((lambda i: lambda: g(i))(i)) 7print([f() for f in functions]) 8# => [0, 1]

リスト3, 4どちらがよいか(あるいは根本的にもっと別の方法がよいか)は意見が分かれるかもしれません。

投稿2018/08/07 17:50

KSwordOfHaste

総合スコア18394

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sdice

2018/08/08 09:55 編集

丁寧に問題になる理由や、修正方法、今後理解を進めた方が良いところも解説して頂き、本当にありがとうございました。理由や着眼点を理解することができました。スコープ、クロージャを再度確認致します。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問