リスト

教科書 Lesson 5

これ以降は、教科書の Lesson 5 の続きを進め、、たいところなのですが、幾らか変則的な進め方になります。以下のように幾らかスキップするところもあるので、教材の指示に注意しながら進めてください。

5.4 リストの注意

この節は教科書ではなく、この教材で説明します。

変数と値の関係

これからリストを扱っていく上で、幾らか不思議(あるいは直感的に理解できない)ことが出てきます。特に変数と値の関係についてはこの機会に考え直しておくのが良いです。つまり教科書 p.30 からの「変数」を「値を保持できる」箱とした説明は(最初の考え方としては分かりやすくて良いのですが)Python においては真実ではありません。

変数への値の代入とはどのようなものか

一般に「変数に値を代入する」時は以下のような動きを想像するでしょう。「 a 」という名前のついた「場所」があり、そこに値を格納した、というものです。p.30 以降でもそのように説明していました。

image-20231103142236186

しかし Python では以下のような動きをしていると考えて下さい。つまり「 a 」という名前(識別子)に、10という値を結びつけたのです。

image-20231103142327722

「a が10を指している」ので a の中身を見ると「10」となる、というコースです。

この状態で a = 100 と、違う値を代入すると、こんなことになります。

image-20231103142358497

a はもう 10 を指しておらず、100 を指している、という状態ですね。

ちょっとややこしいですが、a = a + 1 のような処理はどうなるかというと、、、

image-20231103142436206

といった動きになります。

束縛
専門的にはこの結びつけを「束縛」と表現します。「識別子とその実体の結合」を意味します。もともとの英語での表現が binding なのでこれを直訳して束縛としたのでしょうが、どちらかというと「結合」「結びつけ」くらいが想像しやすくて良かったような気もします。計算機系の専門用語ですので覚えてください。

リストの代入

上のことを念頭に、リストを代入するときの様子を見てください。

まず list1 = [10, 20, 30, 40] として、list1 変数を作ります。

image-20231106105520939

ここで list2 = list1 として、list2 変数に list1 変数を代入します。するとこんなことが起きます。

image-20231106105545789

ここまでの実行結果を示します。

>>> list1 = [10, 20, 30, 40]
>>> list2 = list1
>>> list1
[10, 20, 30, 40]
>>> list2
[10, 20, 30, 40]
>>> 

さてここからが問題です。想像がつくでしょうが、list1[1] と list2[1] を参照すると、結果は同じく 20 となります。しかしこれは「一つの、つまり同じもの」を指しています。つまり list1[1] = 99 とすると、以下のようになるでしょう。

image-20231106105608688

こうなると list2[1] を参照しても「99」だ、となるでしょう。実際やってみた結果を示します。

>>> list1[1]
20
>>> list1[1] = 99   <<< list1 の二番目要素を 99 にした
>>> list1[1]
99
>>> list2[1]
99
>>> list1
[10, 99, 30, 40]  <<< list1 の二番目の要素は 20 から 99 になった
>>> list2
[10, 99, 30, 40]  <<< なんと(直接触っていない) list2 側も 99 になった
>>> 

実は

ところでリストの構造に関する上の図はいくらかウソがあります。つまり実際にはリストは下図の左のような形ではなく、右のような構造になっています。厳密さには欠けますが、わかりやすさを優先して左のように書いているわけです。そのことを頭に置いて読んでください。

image-20231201104349210

リストの複製

リストの代入は結果的に「同じものを指す」状態を作る事を上に示しました。しかし下図のように「リストの複製を作って、そちらを指す」、ということもあるでしょう。

image-20231106105652321

その場合は copy( ) メソッドが使えます。以下のように list2 = list1.copy( ) と書きます。

>>> list1 = [10, 20, 30, 40]
>>> list2 = list1.copy()      <<< .copy() メソッドで複製を作り、list2 に代入する
>>> list1
[10, 20, 30, 40]
>>> list2
[10, 20, 30, 40]
>>> 

上の実験だけでは代入と違って複製ができているのかどうか区別がつきません。そこで list1[1] = 99 としてみます。list1 の二番目の要素 20 は 99 に書き換えられていますが、list2 の二番目の要素は 20 のままですね。

>>> list1[1]=99
>>> list1
[10, 99, 30, 40]  <<< list1 の二番目の要素は 20 から 99 になった
>>> list2
[10, 20, 30, 40]  <<< list2 の方は影響がない
>>> 

下にこの状況を図示しておきます。

image-20231106105755066

list( ) 関数によるリストの複製
教科書ではリストの複製の第一の方法として list( ) 関数を紹介しています。そこ(p.111)に書かれている通り、list( ) 関数を用いてリストを複製することも可能ではあります。ただ、list( ) 関数はどちらかというとタプルなど、もともとリストでないものを含めてリストに変換するための性質がありました。Python3 で純粋にリストを複製するための copy( ) メソッドが導入された今となっては、copy( ) を使えば良いと思います。

5.5 リストの連結とスライス

この節も教科書ではなく、この教材で説明します。

リストを連結する

「+」演算子による連結

リストどうしで「 + 」演算子を使うと、二つのリストを連結して新しいリストを作ります。以下に例を示します。

>>> sale1 = [1, 2, 3, 4, 5, 6]
>>> sale2 = [7, 8, 9, 10, 11, 12]
>>> ysale = sale1 + sale2             <<< 「+」演算子で連結
>>> ysale
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
>>> 

上の連結操作の結果、ysale は独立した新しいリストになります。つまり sale1 や sale2 の中身を参照しているわけではなく「複製」を作ったような状態になります。

extend( ) メソッドによる連結(リストの拡張)

sale1.extend(sale2) と書いて、二つのリストを連結することもできます。この時、sale1 の後ろにsale2の要素が追加されるような形で操作されます。追加に際してはデータの複製が作られるので、下に示した通り、sale2 は残りますが、もう sale1 に影響を与えることはありません。

>>> sale1 = [1, 2, 3, 4, 5, 6]
>>> sale2 = [7, 8, 9, 10, 11, 12]
>>> sale1.extend(sale2)                  <<< extend() で連結
>>> sale1
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]  <<< sale1 が拡張された
>>> sale2
[7, 8, 9, 10, 11, 12]                    <<< sale2 はそのまま
>>> sale2[0] = 99                        <<< sale2 を変更した
>>> sale2
[99, 8, 9, 10, 11, 12]
>>> sale1
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]  <<< しかし sale1 は影響なし
>>> 

「+=」演算子による連結(リストの拡張)

sale1 += sale2 と書いて、二つのリストを連結することもできます。この操作は .extend( ) メソッドと同様、sale1の後ろにsale2の要素が追加されるような形で行われます。

>>> sale1 = [1, 2, 3, 4, 5, 6]
>>> sale2 = [7, 8, 9, 10, 11, 12]
>>> sale1 += sale2                      <<< 「+=」演算子で連結
>>> sale1
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
>>> sale2
[7, 8, 9, 10, 11, 12]                   <<< sale2 を変更した
>>> sale2[0] = 99
>>> sale1
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] <<< しかし sale1 は影響なし
>>> 

スライスで指定する

ここから少しだけ教科書を使います。教科書 p.115 からp.117 (の図5-12)まで読み進めてください。スライスは多様な記述が可能ですので、バリエーションを覚えてください。

補足:繰り返しパターンとしての利用

教科書ではあまり強調されていませんが、スライスはリストから指定したパターンで要素を取り出すループなどに直接使えます。

>>> data = [1, 2, 3, 4, 5, 6]
>>> for n in data[1::2]:
...     print(n)
... 
2
4
6
>>> 

補足:スライスによるリストの複製

例えば list2 = list1[:] とすることで、list1 の複製を作って list2 とすることができます。部分的な複製を作ることももちろんできます。

リストを逆順にするには

p.117 のこの節から再びこの教材で説明します。

つまり教科書ではかなり手間をかけてイテレータのことを説明していますが、このクラスではイテレータは扱いません。ここでは単にテクニックとしてリストを逆順にする方法を紹介するに留めます。

スライスによる逆順リストの作成

list2 = list1[::-1] として逆順にした複製を作ることができます。

>>> data = [1, 2, 3, 4, 5]
>>> data
[1, 2, 3, 4, 5]
>>> newdata = data[::-1]
>>> newdata
[5, 4, 3, 2, 1]
>>> 

reverse() メソッドによるリストの順番の入れ替え

list1.reverse() と書いて、リストの要素の順番を逆順に入れ替えることができます。逆順にした結果が再び list1 に代入されるので、見かけ上は「list1の中身が逆順になった」ように見えます。

>>> data = [1, 2, 3, 4, 5]
>>> data
[1, 2, 3, 4, 5]
>>> data.reverse()
>>> data
[5, 4, 3, 2, 1]
>>> 

reverse() メソッドは newdata = data.reverse() のように代入する形では使いませんので念の為。

5.6 リスト要素の組み合わせと分解

この節はこのクラスではスキップします。

リスト内包表記
この節で出てくる「リスト内包表記」は最も Python 「らしい」記法というか機能かもしれません。プログラミング初学者を混乱させる(あるいは「理解を放棄して呪文を覚える」方向に押す)のでスキップしますが、慣れた頃に見直すと良いと思います。all( ), any( ) 関数とセットで使っている例などを見掛けることがあるでしょうから。

5.7 リストの集計と並べ替え

教科書 p.129 から、実際に試しながら読み進んでください。

5.8 多次元のリスト

教科書 p.132 から、実際に試しながら読み進んでください。

Deep Copy
copy( ) メソッドで複製が作れる、と簡単に書きましたが、多段リスト(多次元配列)つまりリストの中にリストが入っているような場合はちょっと厄介です。
Python ドキュメントの https://docs.python.org/ja/3/library/copy.html や、他の文献を参考にしてください。

Lesson 5 のまとめ

さて、これで教科書の p.135 「5.9 レッスンのまとめ」まで来ました。かなりスキップした要素があるので、扱ったところだけを選んで以下に列挙しておきます。ここに書かれていることが理解出来ますか?

確認課題

課題1. 要素から重複を除いて抜き出す

変数名 data にリストとして数値データが入っています。ここから重複しない要素だけを抜き出したものを、unique という変数に格納してください。ただし要素の出現順を変えないように。

image-20231106151309240

アルゴリズムとしては以下のようにすれば良いでしょう。

  1. data の要素を前から順番に取り出す
  2. その要素の値がターゲットとなる unique の中にあるかどうか確認
  3. なかった場合はその要素を unique に追加
  4. 既存だった場合は何もしない
  5. 要素がなくなるまで手順 1. から繰り返す

step 1. ひな形コードを確認する

unique.py に、以下のようなひな形コードを置いておきました。

data = list(map(int, input().split()))
unique = [ ]
print("data =", data)
## ここにコードを追加する
print("unique =", unique)

これを以下のようにして実行して結果を見てください。

yasuda@Slack 10 % echo 2 8 3 8 2 8 5 6 | python3 unique.py
data = [2, 8, 3, 8, 2, 8, 5, 6]
unique = []
yasuda@Slack 10 % 

コードの以下の点に注目してください。

step 2. 抜き出し処理を作る

あとはコメントとなっているところに data から unique への抜き出し処理を追加すれば良い状態ですね。すぐ上に「アルゴリズム」として出した 1. 〜 5. の処理を実装するのです。

意識すべきことについて列挙しておきます。

ひな形コードに、必要な処理を追加して、以下のような出力をするプログラムとして完成させてください。

yasuda@Slack 10 % echo 2 8 3 8 2 8 5 6 | python3 unique.py
data =  [2, 8, 3, 8, 2, 8, 5, 6]
unique =  [2, 8, 3, 5, 6]
yasuda@Slack 10 % 

様々なパターンで試して動作確認を済ませてから Moodle に提出してくださいね。

補足:リストにその値が含まれているかどうか確認する

リストにその値が含まれているかどうかは in (あるいは not in)で判定可能です。

>>> a = [1, 2, 3]    <<< 実験用のリスト a を作る
>>> 1 in a           <<< 1 は a に含まれるか検査する
True                 <<< 真となる(含まれている)
>>> 100 in a         <<< 100 は a に含まれるか検査する
False                <<< 偽となる(含まれていない)
>>> 

つまり「リスト data に変数 num の値が含まれている」ことを確認するには、if num in data: のように書けば良いのです。

課題2. 回文チェック

「きいろいき(黄色い木)」「やすいいすや(安い椅子屋)」のように前から読んでも後ろから読んでも同じ文になるものを回文という。Python のリストに含まれる値がそのような並びであることを確認するプログラムを作ってください。ただしまだこのクラスではリストの要素としては数値しか扱っていないので、「1 4 3 4 1」や「1 2 4 4 2 1」のようなものを回文、「1 2 3 4」は回文ではない、と判定するようにしてください。

アルゴリズムとしてはリストの前後から一文字ずつループさせてチェックでも可能ですが、今回はリストの分割・反転・一致判定などの機能を用いることにします。以下のような手順でしょうか。

  1. 与えられたリストを前半分と後ろ半分に分割する
  2. 後ろ半分の要素を逆順に並べ替える
  3. 二つのリストを比較する
  4. 一致していたら “Anagram”、一致していなければ “Not Anagram” と出力する

image-20231106163354803

step 1. ひな形コードを確認する

anagram.py に、以下のようなひな形コードを置いておきました。

data = list(map(int, input().split()))
head = data[0:2]
tail = data[0:2]
## ここにコードを追加する
if head == tail:
    print("Anagram")
else:
    print("Not Anagram")

これを以下のようにして実行して結果を見てください。

yasuda@Slack 10 % echo 1 2 3 3 2 1 | python3 anagram.py 
Anagram
yasuda@Slack 10

コードの以下の点に注目してください。

step 2. 抜き出し処理を作る

あとはコメントとなっているところに data から head, tail を合成する処理を追加すれば良い状態ですね。すぐ上に「アルゴリズム」として出した 1. 〜 4. の処理を実装するのです。

意識すべきことについて列挙しておきます。

ひな形コードに、必要な処理を追加して、以下のような出力をするプログラムとして完成させてください。

yasuda@Slack 10 % echo 1 2 3 3 2 1 | python3 anagram.py 
Anagram
yasuda@Slack 10 % echo 1 2 3 3 2 9 | python3 anagram.py
Not Anagram
yasuda@Slack 10 % echo 1 | python3 anagram.py 
Anagram
yasuda@Slack 10 % 

step 3. テストスクリプトでの動作確認

およそ動作する、と思える状態になったら、Web 教材にあるテストスクリプト anagram.sh を実行して動作を確認してください。以下に引用しておきますが、見ての通り最初の 5 例が回文、次の 3 例が回文ではないもの、です。

# 前半は Anagram
echo 1 | python3 anagram.py
echo 1 2 1 | python3 anagram.py
echo 1 2 2 1 | python3 anagram.py
echo 1 2 3 2 1 | python3 anagram.py
echo 1 2 3 4 5 6 7 8 8 7 6 5 4 3 2 1 | python3 anagram.py
# 後半は非 Anagram
echo 1 2 | python3 anagram.py
echo 1 2 3 2 2 | python3 anagram.py
echo 1 2 3 4 2 1 | python3 anagram.py

実行すると以下のように、5 つ Anagram、3 つ Not Anagram と並ぶはずです。

yasuda@Slack 10 % zsh anagram.sh
Anagram
Anagram
Anagram
Anagram
Anagram
Not Anagram
Not Anagram
Not Anagram
yasuda@Slack 10 % 

ここまで確認できたら Moodle に提出してください。

参考:リストの比較

if 文で使っていた比較演算子、「 == 」「 != 」が、そのままリストの内容比較に使えます。

>>> list1 = [10, 20, 30, 40]
>>> list2 = list1.copy()   <<< リストを複製
>>> list1 == list2         <<< 内容を比較
True                       <<< 複製したのだから内容は同一
>>> list1[1]=99            <<< 要素の一つを 99 に変更した
>>> list1 == list2         <<< この状態で比較すると、、
False                      <<< 内容が違うことが確認できた
>>>