こんにちは、つくたろうです。
今回は久々に、Pythonについての記事を書きたいと思います。
「いやいい加減にポケモンの記事を書けよ」という声が聞こえてきそうなんですが、それについても別途書き進めているので近いうちに更新できると思います。しばしお待ちを…
今回は自分に対する備忘録というか、簡易pythonライブラリ的な便利関数を作ったので、記憶が新しいうちに記事にしておこうと思った次第であります。
はじめに
さて、ということで今回作ったのは「Pythonの2次元や3次元、それ以上の多次元配列(多次元list・多次元tuple・多次元ndarray)について、任意の要素がその多次元list内に含まれているか検索して、インデックスを返す関数」です。
無駄に長くなったので簡潔に言い直すと、「Pythonの多次元listについて、任意の要素の検索ができる関数」を作りました!というわけです。
Pythonのリストってかなり便利ですが、2次元リスト(もしくは、それ以上の多次元リスト)の中から要素を検索するのが絶妙に面倒だと思いませんか?
それほど使う機会がない代わりに、出くわしたときに実装するのが面倒くさい。そういった処理の筆頭なんじゃないかなと思います。
それに、必要となる頻度がそれほど多くないからか、検索しても出てこない気がします。なんだかんだで毎回適当なワンライナーを書いたりしてお茶を濁している感じ。
とはいえ、最近APIを叩いて返ってきたjsonとか、アノテーションツールから吐き出された注釈データなんかをPythonで処理することが増えて、汎用性の低いワンライナーをその都度書くのがだんだん面倒になってきました。
それに、2次元リストならまだ大したことないんですけど、データによっては3次元とか4次元にまでネストされていたりしてワンライナーじゃさすがに大変。保守性も下がりますし。
ということで、(主に自分のために)「何次元にネストされていても該当要素のインデックスを返してくれる関数」を作ってみました。
実装コード
ということで、今回実装したコードを紹介したいと思います。
jupyter notebookのコードセルみたいな感じでいくつかに分けて書いていきます。
コードが見切れている場合は横にスクロールできると思います。スマホの方は横画面にすれば幾分見やすくなるかもです。
モジュールのインポート
import collections import numpy as np from typing import Union
numpy
だけ標準モジュールじゃないので、pip install numpy
とかで入れてください。(これはndarray
の型アノテーションのためにインポートしているので、ndarray
を使わない場合はここでインポートせず、後々出てくる型アノテーションも削ってしまっても問題ないと思います。)
ヘルパー関数
今回、要素の検索対象を多次元listに対応させることで、汎用性を高める実装を行いました。
そのため、ネストの深さの測定や不規則な次元を弾く処理(後述)のためにいくつかヘルパー関数を用意しています。これらの関数はメインの関数の中で使うものですので詳しい説明は省略しますが、やっていることはコード内に書いたコメントの通りになっています。
こちらもnotebookで行う場合はそのまま張り付けてセルで実行すれば大丈夫です。
# リストのネストの深さを求める関数 def nest_depth(item:Union[tuple,list,np.ndarray]): if not item: return 0 else: if isinstance(item, (tuple,list,np.ndarray)): return max([nest_depth(i) for i in item]) + 1 else: return 0 # リストを決められた回数だけ平坦化する関数 def flatten(input_list, flatten_num:int): flatten_num = flatten_num - 1 if flatten_num < 0: for item in input_list: yield item else: for item in input_list: if isinstance(item, collections.abc.Iterable) and not isinstance(item, (str, bytes)): yield from flatten(item, flatten_num) else: yield item # 不規則な次元を含んだ多次元リストかどうかをチェックする関数 def isirregular(input_list): flatten_num = nest_depth(input_list) - 2 flattened_list = list(flatten(input_list, flatten_num)) return not len(list(set([nest_depth(item) for item in flattened_list]))) == 1
ヘルパー関数の書き方や考え方については、以下の書籍に載っているので、ぜひ参考にしてみてください。オススメの本の一つです。
多次元のリストから任意の要素について検索してインデックスを調べる
さて、ようやく本命の「要素を検索する関数」の定義です。
以下のようにmultidimension_search()
関数を定義します。
# 多次元のリストから任意の要素について検索してインデックスを調べる関数 def multidimension_search( input_list:Union[tuple,list,np.ndarray], query:Union[int,str,tuple,list,np.ndarray], contains_all=False ): if isirregular(input_list): print("An irregular dimensional list cannot be saerchable.") return if isinstance(query, (int, str)): query = [query] if nest_depth(input_list) == 1: if contains_all: return all(map(lambda x:x in input_list, query)) else: return [i for i,l in enumerate(input_list) if any(map(lambda x:x == l, query))] elif nest_depth(input_list) > 2: return [multidimension_search(sublist,query,contains_all) for sublist in input_list] if contains_all: return [i for i,l in enumerate(input_list) if all(map(lambda x:x in l, query))] else: return [i for i,l in enumerate(input_list) if any(map(lambda x:x in l, query))]
では、このmultidimension_search()
関数について、引数や返り値の説明をしておきます。
入力の引数について
input_list: 要素が存在しているか調べたいリスト(またはタプル, ndarray)
検索対象となる多次元リストです。例: [[1,2,3],[2,3,4],[3,4,5]]
2次元以上のリストを想定していますが、一応1次元のリストでも動作はします。
また、タプル型やndarray型でも大丈夫です。
query: 調べたい要素 (リストやタプルの形で複数渡すことも可能)
input_list
で渡した多次元リストに対して、含まれているか検索したい要素がこの引数です。
例えば100
という値がinput_list
に含まれているか確認したい時はquery=100
としたり、文字列"aaa"
が含まれていることを確認したい場合はquery="aaa"
のように渡します。
また、この値はリストやタプルの形で複数渡すことが可能になっています。例:[1, 2, 3]
、("abc", "xyz")
複数渡した場合に「どれか一つでも含まれているのか調べる」のか、「全てが含まれているのかを調べる」のかは、次の第3引数であるcontains_all
で制御します。
contains_all: queryの全ての要素をもつ要素のインデックスが欲しい場合はTrue、query内のどれか一つでも該当するものを検索したい場合はFalse
bool値です。先述の通りquery
引数をタプルやリストで複数渡した場合に、contains_all=False
だと「query
内の要素のどれか一つでも含まれているのか調べる」モードとなり、contains_all=True
だと「query
内の要素全てが含まれているのか調べる」モードとなります。
デフォルトではcontains_all=False
となっています。
返り値について
関数multidimension_search()
の返り値は、基本的には該当する要素番号を持った多次元リストとなります。
ということで、返り値がどうなるか、せっかくなので実行例を使ってみていきましょう。
2次元リストから検索した場合
# 検索対象の2次元リスト list1 = [[1,2,3],[2,3,4],[3,4,5]] # 探す要素 query = [1,2] # contains_all=Trueとし、[1,2]の両方を持っている要素を検索 search_result1 = multidimension_search(list1, query, contains_all=True) print(search_result1) # [0] # list1[0]に1と2の両方が含まれていることが分かる.
3次元リストから検索した場合
# 検索対象の3次元リスト list2 = [[[11,12,13],[1,15,16],[17,18,1]],[[11,12,13],[11,1,13],[11,12,13]]] # 探す要素 query = [1,2] # contains_all=Falseとし、[1,2]のどちらか一方でも持っている要素を検索 search_result2 = multidimension_search(list2, query, contains_all=False) print(search_result2) # [[1, 2], [1]] # list2[0][1], list2[0][2], そしてlist2[1][1]が1または2を持っていることが分かる。
不規則な次元を持っているリストの場合
pythonのリストは不規則な次元を持っている場合があります。
例としては、[1,2,3,[4,5,6]]
のようなリストです。ある部分では1次元であり、ある部分では2次元であるようなリストですね。
今回の関数では、こういった不規則な次元を持ったリストに対しては検索を受け付けないようにしました。
理由としては、こういったリストは「厳密に〇次元のリストである」として扱うことが難しいため、インデックスの位置を番号で返すことができないことが挙げられます。
今回作った関数では、こういった不規則な次元を持ったリストをinput_list
として入力した場合、コンソールにエラーメッセージを出力するとともに、None
が返却されるようになっています。
# 検索対象の3次元リスト list3 = [1,2,3,[4,5,6]] # 探す要素 query = [1,2] # contains_all=Falseとし、[1,2]のどちらか一方でも持っている要素を検索 search_result3 = multidimension_search(list3, query, contains_all=False) # An irregular dimensional list cannot be saerchable. のエラーメッセージが出る。 print(search_result3) # None # 不規則次元のリストを入力したため、Noneが返った
文字列についての実行例
ここまでは数値が含まれている場合について多次元リストから検索した実行例をお見せしてきましたが、もちろん文字列についても検索が可能です。
というか僕的にはこの関数を、APIを叩いて返ってきた文字列の多次元リストから検索するために作ったみたいなとこがあります(笑)
ということで、文字列を検索した場合の実行例についても紹介します。
# 以下の4種でテスト string_list1 = [["aaa","bbb","ccc"],["aaa","bbb","ccc"],["aaa","bbb","ccc"]] string_list2 = [["aaa","aaa","aaa"],["bbb","bbb","bbb"],["ccc","ccc","ccc"]] string_list3 = [[["aaaa","aaaa","aaaa"],["bbbb","bbbb","bbbb"]],[["bbb","bbb"],["cccc","cccc"]]] string_list4 = [[["abc","xyz"],["aaa","bbb","ccc"]],[["aaa","xyz"],["abc","abc","abc"]]] # "aaa","bbb","ccc"について、含まれているインデックスを検索 string_query = ["aaa","bbb","ccc"]
# "aaa","bbb","ccc"のいずれか一つでも入っているか調べる例 print(multidimension_search(string_list1, string_query, contains_all=False)) print(multidimension_search(string_list2, string_query, contains_all=False)) print(multidimension_search(string_list3, string_query, contains_all=False)) print(multidimension_search(string_list4, string_query, contains_all=False)) # [0, 1, 2] # [0, 1, 2] # [[], [0]] # [[1], [0]]
# "aaa","bbb","ccc"の全てが入っているか調べる例 print(multidimension_search(string_list1, string_query, contains_all=True)) print(multidimension_search(string_list2, string_query, contains_all=True)) print(multidimension_search(string_list3, string_query, contains_all=True)) print(multidimension_search(string_list4, string_query, contains_all=True)) # [0, 1, 2] # [] # [[], []] # [[1], []]
出力結果をboolで確認する拡張関数
今回作ったmultidimensional_search()
関数は、先ほどまでの例のとおり「該当要素のインデックスを返す」というものになっています。
要素の存在よりも、どちらかというと「どこに要素があるのかを知りたい」というニーズの方が多いかな~と個人的に感じているためです。
しかし、単純に「要素がそのリスト内に存在しているかどうかを知りたい」というタイミングもあると思います。
ということで、拡張関数item_exist_checker()
も一緒に作りました。multidimensional_search()
からの返り値を入れると、該当のインデックスが一つでもあればTrue
を、一つも見つかっていなければFalse
を返すようになっています。
# 出力結果をboolで確認する拡張関数 def item_exist_checker(search_result): if nest_depth(search_result) == 0: if search_result == 0: return True else: return bool(search_result) else: return any([item_exist_checker(res) for res in search_result])
存在確認の拡張関数の使用例
使用例は以下のような感じになっています。
引数のlist1
とかquery
とかは、先の実行例で定義しているものと同じです。
search_result1 = multidimension_search(list1, query, contains_all=True) search_result2 = multidimension_search(list3, query, contains_all=True) # 不規則次元なのでAn irregular dimensional list cannot be saerchable.が出る search_result3 = multidimension_search(string_list1, string_query, contains_all=True) search_result4 = multidimension_search(string_list3, string_query, contains_all=True) print(search_result1) print(search_result2) print(search_result3) print(search_result4) # [0] # None # [0, 1, 2] # [[], []]
print(item_exist_checker(search_result1)) print(item_exist_checker(search_result2)) print(item_exist_checker(search_result3)) print(item_exist_checker(search_result4)) # True # False # True # False
ということで、multidimension_search()
関数で多次元配列の中身から要素の存在を確認して、その結果をbool
値として使いたい場合は、item_exist_checker()
関数を使ってください。
ちなみに、はじめは「いや、要素が見つからなければ空のlist[]
が返ってくるんだし、出力をbool
でキャストすれば良いでしょ!」なんて思っていたんですが、出力結果をそのままbool
としてしまうと、search_result = [[], []]
などの時にTrue
が返ってしまうことに気が付きました。
そうなんですよ、pythonってbool([])=False
なのに、bool([[]])=True
なんですよね。Pythonの仕様的に単純なbool
キャストでは「要素が見つからなくて空の多次元リストが返ってきた」みたいな場合でもTrue(存在してるよ!)
が間違って返ってしまう場合があることを踏まえて、存在確認のための拡張関数item_exist_checker()
を作った次第です。
割と間に合わせで作ってしまった感が否めないので、これについてはmultidimension_search()
からまとめて返せるように作り直したいな~なんて思っていたりいなかったり。(ただ、再帰関数を多用しているので、ちょっと面倒なのかも、、、笑)
クイックスタート(シンプルな2次元配列検索API)
さて、ここまで実装したmultidimension_search()
およびその使い方について長々と書いてきたわけですが、この関数(もしくはライブラリ)は、とりあえず先述の通り「多次元配列に対応した、かなりリッチな作り」になっています。
ネストが何重になっていても対応できるような設計にした代わりに、内部でnest_depth()
やisirregular()
といった他の関数を呼び出しまくる必要が出てきてしまっているわけです。
これでは、そういった関数も一緒に定義しておく必要があるため、もしかしたら「ちょっとコピペして使いたい」という時に少し不便かもしれません(セルで実行しているなら問題ないとは思いますが)。
なので今回、さくっと使える軽量なAPIとして「2次元配列の検索専用の関数two_dim_search()
」も一緒に実装してみました。
使いどころとしては、「2次元のリストから検索したいだけの時」に使えるようになっています。
中で他の関数を読んでいないため、このコードをコピペして貼るだけでどこでも使えます。引数はmultidimension_search()
と同じです。
def two_dim_search( input_list:Union[tuple,list,np.ndarray], query:Union[tuple,list,np.ndarray], contains_all=False ): if contains_all: return [i for i,l in enumerate(input_list) if all(map(lambda x:x in l, query))] else: return [i for i,l in enumerate(input_list) if any(map(lambda x:x in l, query))]
クイックAPIの注意点
今回クイックAPIとして実装したtwo_dim_search()
は、先述の通り、内部でnest_depth()
などの他の関数を呼び出さないようにしているため、次元判定をしていません。つまり、2次元list前提の作りになっています。そのため2次元以外の配列を入力するとエラーが出たり、正しい答えが得られなかったりします。
「不規則な次元を含んでいない2次元リスト」であることが分かっている配列から要素を検索するためだけに使ってください。2次元以上の多次元配列から検索したい場合は、multidimension_search()
をお使いください。よろしくお願いします。
ちなみに存在判定のためにitem_exist_checker()
を使う分には問題ないので、そちらはぜひ使ってください。
クイックAPIの実行例
Pythonの2次元配列から要素を検索するクイックAPItwo_dim_search()
についても、実行例を載せておきます。検索対象の配列およびクエリは先のものと同じなので、先と同じ結果が得られていることが分かります。
# 不規則次元を含まない2次元配列のみ入力可能 list1 = [[1,2,3],[2,3,4],[3,4,5]] string_list1 = [["aaa","bbb","ccc"],["aaa","bbb","ccc"],["aaa","bbb","ccc"]] # 1,2および"aaa","bbb","ccc"を含むかどうか調べる query = [1,2] string_query = ["aaa","bbb","ccc"]
# [1,2]のどちらか一方でも入っているか調べる例 print(two_dim_search(list1, query, contains_all=False)) # [1,2]の全てが入っているか調べる例 print(two_dim_search(list1, query, contains_all=True)) # "aaa","bbb","ccc"のいずれか一つでも入っているか調べる例 print(two_dim_search(string_list1, string_query, contains_all=False)) # "aaa","bbb","ccc"の全てが入っているか調べる例 print(two_dim_search(string_list1, string_query, contains_all=True)) # [0, 1] # [0] # [0, 1, 2] # [0, 1, 2]
GitHub
今回紹介したコードは、全て私のGitHub上にアップロードしています。
また、GitHubにはcodes.ipynb
という名前でこの記事で使った全てのコードを動かせるjupyter notebookも用意しておいたほか、.py
形式のファイルも同様に作ってあるので、git clone
等でぜひ試してみてください。
また、実行例について、この記事では簡単なサンプルのみを紹介しましたが、GitHubではもう少し多くの例も載せているので、実行例などについて詳しく見たい方も、ぜひ覗いてみてください。
参考にした書籍等
今回の開発にあたって、以下の書籍なんかを参考にしました。自分は紙の本を手元に置いておく派なのですが、Pythonを書いていくにあたって以下の書籍はとても使いやすく、他の書籍には載っていない点も網羅的に学べるため非常に重宝しています。皆さんにもオススメしておきます。
Python初心者の方には以下の書籍がオススメです(第1版を大学の授業で教科書に指定されて使っていたんですが、イラストや漫画が豊富で理解しやすいのでオススメです。第2版は最近出ました)。
おわりに
さて、ということで今回は「pythonにて、2次元以上の多次元配列から任意の要素の検索を行う関数」を作成しました。
はじめに言った通り自分の欲しいものを作ったつもりですが、かなり汎用性の高いものになったような気がするので、ぜひ皆さんも使ってみてください。
そういった中で、「ここはこうした方がいい!」とか「ここがよくわからない!」みたいな感想や意見がございましたら、コメント欄やGitHubのissue等でなんなりと伝えてください。お待ちしております。
また、GitHubのプルリクエストなんかも随時募集中です。皆さんのコミット、お待ちしております。
さて、このコードに関して、今後の展望としては、まず「要素そのものにもサクッとアクセスできるようなAPIを作る」という点と、それをするならもういっそ「クラスの形にして、もろもろの処理をメソッド化する」という2点を考えています。
どれぐらい需要のあるプログラムになっているかは分かりませんが、暇があればそういったさらなるバージョンも作っていきたいと思います。
ということで、今回の記事はこの辺で筆を擱きたいと思います。ここまで読んでいただき、ありがとうございました!
2022/01/10 つくたろう
カテゴリ: