KNIMEST’s diary

KNIME使いがPythonも使いたくて勉強中

【Python学習帳】022_内包表記と副作用

初学者を対象としている京都大学の無料Python教材の学習中です。
「プログラミング演習 Python 2021」
Version2021_10_08_01.pdf

hdl.handle.net

CC-BY-NC-NDライセンスに従って引用します。

第5章の制御構造の勉強中で、内包表記に関して教材を離れて少し調べていました。

knimest.hatenablog.com

良い点として内包表記の中で変数を使っても、いかなる副作用もなく処理できることが挙げられていました。
一方で、for文をつかったループ処理を行った際には、ループが終了した後にも存在する x と y という名前の変数を作る (または上書きする) ことに注意が必要なようです。

こちらを実際に体験してみることにしました。ChatGPTに「内包表記を使わないで書き換えて」もらいました。

上図だけではわかりにくいでしょうから何をしたかを書いてみます。
内包表記の前後ではx = 0のまま変化はないです。

 

a = [5, 1, 3, 4]          #リストaを定義

x = 0

a = [x for x in a if x%2 != 0]     #リストaの要素で奇数の要素のみを残す

 

print(a)     # [5, 1, 3]になった

print(x)     # 0のまま

 

で、上図のようにChatGPTにfor文を使って書き換えをしてもらいました。

そのコードを使ってループ処理後のxをprintすると、もともとは0だったのが、リストaの要素を抜き出すループ処理に変数xを使ってしまったので、もともとのリストaの最終要素だった4に変わっていますね。

今回のような簡単なコードならまだ気付けるとは思うんですが、何十行、何百行も前に使っていた変数名を気付かないでどこかで書き換えてしまってその後の処理がおかしくなるという副作用に果たして自分は気が付けるだろうかと怖くなりました。

さて、以下は全くの余談ですが、ChatGPT(以下C:)は初心者にとってのPythonの先生役になれますね。以下私(以下K:)とのやり取りを抜粋します。

 

K:
a = [5, 1, 3, 4] #リストaを定義
x = 0
a = [x for x in a if x%2 != 0] #リストaの要素で奇数の要素のみを残す
print(a)
を内包表記を使わないで書き換えてもらえませんか?

C:
はい、以下は内包表記を使わないで書き換えたコードです。

a = [5, 1, 3, 4]          #リストaを定義

new_a =

for x in a:

    if x % 2 != 0:

        new_a.append(x)

a = new_a

 

print(a)


このコードでは、空のリストnew_aを作成し、元のリストaの要素をループして処理します。要素が奇数の場合は、new_aに追加します。最後に、リストaをnew_aに置き換え、new_aを印刷します。

内包表記と同じような効果を持ちますが、より明示的で直感的な書き方です。

 

K:

おっしゃる通りとても明示的で直観的ですね。ありがとうございました。 では空のリストを作成しない方法はありますか?

 

C:

はい、以下はリストを作成せずに、元のリストを直接変更する方法です。

a = [5, 1, 3, 4]          #リストaを定義

i = 0

while i < len(a):

    if a[i] % 2 != 0:

        i += 1

    else:

        a.pop(i)

 

print(a)

このコードでは、whileループを使用してリストaを直接変更します。リストaの各要素について、要素が奇数であれば、インデックスを増やして次の要素に移動します。要素が偶数である場合は、リストから削除します。これにより、リストaの各要素がチェックされ、偶数の要素は削除されます。最後に、新しいリストaが印刷されます。

ただし、リストを直接変更する場合、副作用が発生する可能性があるため、注意が必要です。そのため、元のリストを変更しない方が好ましい場合があります。

 

K:

while文はまだ習っていないので、他の書き方があったら教えてください。

 

C;

そうでしたか、了解しました。while文を使用しないで、forループを使用して元のリストを直接変更する方法もあります。以下はその例です。

 

a = [5, 1, 3, 4]          #リストaを定義

to_remove =

for x in a:

    if x % 2 == 0:

        to_remove.append(x)

for x in to_remove:

    a.remove(x)

 

print(a)

 

このコードでは、まず、リストから削除する要素を一時的に保存するリストto_removeを作成します。to_removeリストには、偶数の要素だけが追加されます。次に、to_removeリストの各要素をループし、リストaから削除します。最後に、変更されたリストaを印刷します。

ただし、この方法でもリストを直接変更しているため、副作用が発生する可能性があるため、注意が必要です。リストを直接変更する代わりに、新しいリストを作成する方が望ましい場合があります。

 

K:

副作用にまで注意喚起していただきありがとうございました。 内包表記もwhileも空のlist作成もしない手法がもしあったら教えてください。 無ければその旨回答ください。


C:

もちろん、他にも方法があります。

例えば、filter関数を使う方法があります。以下はその例です。

 

a = [5, 1, 3, 4]          #リストaを定義

a = list(filter(lambda x: x % 2 != 0, a))

 

print(a)

このコードでは、filter関数を使用して、元のリストaから奇数の要素だけを抽出して、新しいリストとして再構築しています。lambda関数は、与えられた引数が偶数である場合にFalseを返し、奇数である場合にTrueを返します。filter関数は、与えられた条件がTrueである要素のみを含む新しいイテラブルオブジェクトを返します。

この方法でも、新しいリストを作成するため、リストの副作用が発生する心配はありません。

他にも方法があるかもしれませんが、今のところこれらの方法が思いつきます。


K:

lambda式も教えていただきありがとうございました。 まだほかにも思いつきますか?

 

C:

はい、まだいくつかの方法があります。例えば、以下のようにmap関数を使って、元のリストから偶数を除いた新しいリストを作成することができます。

a = [5, 1, 3, 4]          #リストaを定義

a = list(map(lambda x: x, filter(lambda x: x%2 != 0, a)))

 

print(a)

この方法では、まずfilter関数を使用して、元のリストaから奇数の要素だけを抽出します。そして、map関数を使って、抽出された奇数の要素を新しいリストとして再構築します。

また、以下のようにreduce関数を使って、元のリストから偶数を除いた新しいリストを作成することもできます。

from functools import reduce

 

a = [5, 1, 3, 4]          #リストaを定義

a = reduce(lambda x,y: x + [y] if y%2 != 0 else x, a, )

 

print(a)

この方法では、reduce関数を使用して、元のリストaから奇数の要素だけを抽出して新しいリストを再構築します。reduce関数は、イテラブルオブジェクトの要素を1つずつ取り出して、指定された関数を適用し、その結果を次の要素に適用していくことを繰り返します。

いずれの方法でも、新しいリストを作成するため、リストの副作用が発生する心配はありません。

 

K:

とても勉強になりました。初心者なのでlambda式は少し難しいと感じました。 出来ればfor文数行で書いてみていただけますか?

 

C:

a = [5, 1, 3, 4]

new_list =

for i in a:

    if i % 2 == 0:

        continue

    new_list.append(i)

print(new_list)

ここでは、continue文を使って偶数の場合は処理をスキップしています。そして、新しいリストnew_listに奇数の要素だけを追加しています。
この場合、forループとリスト操作を組み合わせることで、奇数要素を別のリストに格納し、元のリストをそのリストに置き換えるという方法をとっています。

 

K:

空のリストを用いない場合は内包表記かlambda式を必要とするという認識でよろしいでしょうか

 

C:

そうですね、リスト内包表記やlambda式は、リストを作成する際にコンパクトで簡潔なコードを書けるため、空のリストを使わなくてもよくなります。ただし、コードが簡潔であるがゆえに読みにくくなってしまう場合もありますので、適宜使い分けるとよいでしょう。

 

上記のやり取り、人間相手だったら私はここまでしつこく質問しかねますし、ChatGPTの解答が10秒以内ぐらいで返ってくるので手軽に次へ進めます。

ブレストや壁打ちの相手としてはいいのではないかというコメントを読んではいましたがこれは有用だと実感しました。まあ、世界中にテキストベースの情報があふれかえっているPython初心者のための情報を教えてもらったからだと思います。

 

ChatGPTが堂々と間違ったコードを教えてくれることもあって面白かったですが、私(K:)の質問が悪かったようで、その後のやり取りで目的を果たすことができました。使い手の方も成長しないとですね。

qiita.com

最後に復習です。

上記のChatGPTの生成したコード群で、副作用が生じないものはどれでしょうか?

次回はwhile文による制御を学びます。

knimest.hatenablog.com