複数あるリストを結合してひとまとめにしたいときってありますよね。多くの場合は+演算子を使えばいいでしょう。でも、それ以外にもいろいろな方法があります。知らない方法もあるかもしれませんよ!
以下は3つのリストa、b、cを1つのリストにまとめようとするコードだ。ただし、1から4のコードの中で例外を発生させるものがある。例外を発生させるコードを全て選択せよ(上の画像とは変数a、b、cへの代入が1行と3行の差があるが、振る舞いは同じなので気にしないでほしい)。
from itertools import chain
a = [0, 1, 2]
b = [3, 4, 5]
c = [6, 7, 8]
# 選択肢1
result = a + b + c
# 選択肢2
result = a.extend(b.extend(c))
# 選択肢3
result = list(chain(a, b, c))
# 選択肢4
result = sum([a, b, c], [])
例外を発生させるのは選択肢2の「result = a.extend(b.extend(c))」です。
以下に実際に問題文のコードを実行した結果を示します。ただし、変数resultの値を表示するようにしている点と、例外を発生させる選択肢2のコードを最後に実行するようにしている点には注意してください。
リストのextendメソッドは、呼び出しに使用したリスト自身を破壊的に変更するメソッドで、戻り値はありません。というか、Noneを返します。このため、「a.extend(b.extend(c))」というメソッド呼び出しの内側にある「b.extend(c)」の戻り値はNoneです。よって、「a.extend(None)」が実行されます。リストのextendメソッドは反復可能オブジェクトを受け取って、その内容を使い、呼び出しに使用したリストを拡張しますが、引数が反復可能オブジェクトではなくNoneなので、ここでTypeError例外が発生します。
以下では、選択肢を上から順番に見ていきましょう。
まず選択肢1ですが、これはみんながよくやる+演算子によるリストの結合です。
# 選択肢1
result = a + b + c
説明するまでもなく、このコードに問題はないでしょう。
次に選択肢2です。
# 選択肢2
result = a.extend(b.extend(c))
上でも述べたように、リストのextendメソッドは「反復可能オブジェクトを受け取って、その内容でリストを拡張し、その戻り値はない(戻り値はNone)」というものです。
「a.extend(b.extend(c))」というコードの動作を順に追うと、まず内側の「b.extend(c)」により、リストbがリストcの内容で拡張され、その戻り値はNoneとなります。以下は例外が発生した後のリストbの内容を確認したものです。
extendメソッドには戻り値がない(戻り値がNoneである)ので、「a.extend(b.extend(c))」は「a.extend(None)」となります。そして、反復可能オブジェクトではなくNoneがリストaのextendメソッドに渡されるので、想定しているオブジェクトの型(反復可能オブジェクト)とは違う値が渡されたとしてTypeError例外が発生しているのです。
選択肢3はitertoolsモジュールのchain関数を使うコードです。これはchainクラスのコンストラクタとして働き、引数に与えた0個以上の反復可能オブジェクトをひとまとめに扱うためのイテレータを作成します。
# 選択肢3
result = list(chain(a, b, c))
リストを結合した結果のリストが欲しいので、ここではchain関数で得たイテレータをlist関数に渡していることにも注意してください。リストを結合した結果を逐次的に処理するのであれば、この処理は不要です。
「選択肢1のように+演算子で連結する方がカンタンじゃん」と思う人もいるかもしれません。しかし、巨大なサイズのリスト(反復可能オブジェクト)が複数個あって、それらを何らかの事情で連続的に処理する必要がある場合に+演算子を使うと、既にリストがあるのに、それらをまとめたリストをもう一度作成することになります。これはメモリ使用量を無駄に増やすことになります。そうではなく、chain関数を使えば、できるのは元のリストを参照するイテレータなのでメモリの使用量を低減できるはずです。使う機会はそれほど頻繁ではないかもしれませんが、覚えておくといいことがあるかもしれません。
最後の選択肢4はちょっと意味が分からないですよね。
# 選択肢4
result = sum([a, b, c], [])
sum関数は反復可能オブジェクトを受け取り、それをstartパラメーターに指定した値(デフォルト値は0)に順番に加算していくことで、反復可能オブジェクトの総和を計算します。そして、実はstartパラメーターには数値以外を与えることもできるのです。
といっても、まだよく分からないかもしれません。sum関数と同様な処理をする関数をPythonで定義してみると次のようになるでしょう。
def mysum(iterable, /, start=0):
result = start
for item in iterable:
result += item
return result
+=演算子で結果(result)に反復可能オブジェクトを足しているところに注目してください。つまり、「sum([a, b, c], [])」というコードを実行すると、sum関数の内部では結果を保持する変数の初期値が空のリストとなり、その後、リストの要素を取り出して「結果 += リストの要素」のような処理をしているということです。
この結果、初期値として与えた空リストに3つのリストの要素が順次追加され、最終的には3つのリストの内容が1つのリストにまとめられるというわけです。
ギミックは面白いのですが、実行時間的には不利でしょうから常用はしない方がよいでしょう。また、上のmysum関数を見ると、初期値を空文字列にして、文字列や文字列を要素とするリストを渡してもいけるんじゃない?(文字列も反復可能オブジェクトだから)と思う人もいるかもしれません。
試してみましょう。以下はmysum関数を使って試しています。
result = mysum('abc', '')
print(result) # 'abc'
result = mysum(['a', 'b', 'c'], '')
print(result) # 'abc'
mysum関数では上のコードは例外を発生させることなく、思った通りの結果となります(ただし、「mysum('abc', '')」は元の文字列と同じ文字列が得られるだけなので、時間の無駄な気持ちはしますね)。
では、同じことをsum関数でやってみましょう。コードは省略して、実行結果だけを示します。
どちらのコードを実行してもTypeError例外が発生します。しかもご丁寧に「sum() can't sum strings [use ''.join(seq) instead]」(sum関数は文字列の和を求められません。''.join(seq)を使ってください)というメッセージ付きです。
+=演算子による文字列の結合は時間的にもメモリ的にも効率が良くありません。そのため、TypeError例外を発生させているのだと思われます。
今回は複数のリストをひとまとめにする方法を見てみました。最後のsum関数を使う方法は「役に立たない豆知識」ってヤツですよね。でも、何でそんなことが可能なのか、自分でコードを書いてみると、Pythonに対する理解も進むんじゃないかな? と思って選択肢に含めてみました。
そういうわけで、リストについて勉強してみようという方は「Python入門」の以下の記事にもぜひ目を通してもらえるとうれしいです。
Copyright© Digital Advantage Corp. All Rights Reserved.