こんにちは、Pythonエンジニア育成推進協会 顧問理事の寺田です。私は試験の問題策定とコミュニティ連携を行う立場です。
実践試験Tipsは、当協会のPython3エンジニア認定実践試験を受験される予定の方に向けて、何回かに分けてちょっとした情報をお伝えしていく目的で作成しています。
前回は主教材Chapter8日付と時刻の処理についてお話しました。
今回はChapter9 データ型とアルゴリズムの学習ポイントについてお話しします。
■学習のポイント:Chapter9データ型とアルゴリズム
Python3エンジニア認定実践試験は、主教材「Python実践レシピ」(技術評論社)の中から出題されます。
出題範囲については以下のページをご確認ください。
https://www.pythonic-exam.com/exam/jissen
主教材Chapter9ではデータ型とアルゴリズムについて解説されています。
この章からの出題は5問(12.5%)です。9.3の bisectと、9.5の pprint は除外されます。
この章はこの試験において重要な部分となっており、出題数もほかの章と比べ多めになっていますので、取り逃しのないようにして欲しいなと思っています。
とはいえ、少々理解が難しいであろうという9.3の二分法アルゴリズムのbisectと、デバックなどで使用されるpprintは試験範囲からは外れていますので、試験勉強という意味では、ここは学習の範囲から除外していただいて構いません。このTipsでもこの2つについては除外します。
■押さえておきたいポイント
Chapter9の各節で押さえておいてほしいポイントは次の通りです。
9.1ソート
Pythonにはソートに相当する機能が関数でやるものとリストのメソッドになっているものの2種類があります。これは歴史的にこのような形になっており、どちらも同じように機能します。
メソッドで行う方のソートは、リスト名に.sort()を付けて使用します。この手法は元のリストの中身そのものを破壊的に変更してしまうものになっています。
一方、sorted()という組み込み関数を使用する方は、組込関数の方がリストの元のデータを変えずに、新しいリストを作ります。
私としてはsorted()関数を使う方をおすすめしています。もちろん利用用途によってはメソッドの方が適している時もありますが、一般的な通常の用途においては組み込み関数のsorted()を使う方がいいと考えています。
この節ではほかに、reversed()という、逆順にさせる方法に加え、Key引数や、そこで使えるoperatorモジュールの解説もされています。
数値系のデータにおいてはソートをすること自体はよくありますので、実際にコードを打って確認して、身に着けておいてほしいと思います。
ソートを行う上で重要なのは、keyを引数にして、どのような並べ方ができるか(例:すべての文字を小文字に変換した後に、ソートさせる)の例が書籍に記載されています。さらに複雑なソートをしたい場合には、operator()モジュールで対応できるようになっていますので、operator()モジュールを含めて勉強していただければいいかなと思います。
9.2様々なコンテナー型を扱う
コンテナー型を扱うためのcollectionsモジュールが取り上げられています。
collectionsモジュールには、様々なコンテナー型のオブジェクトを扱うときに便利な機能がたくさん入っていますので、うまく使いこなせるようになると、Pythonの特徴を捉えた形で良いコーディングができるようになります。まずはこういうものがあるのだと覚えていただいたうえで、どうやって使っていくのかということを考えていただければと思います。
さて、この書籍では、Counterと、defaultdict、OrderdDict、namedtupleの4つが紹介されています。
collectionsモジュールには他にも機能がありますが、試験の学習という意味では、この4つについてまず学習していただくことになります。collectionsに苦手意識を抱えている人もいるかとは思いますが、試験としてはまず最低限、この4つを覚えていただいて、collectionsへの苦手意識をなくしてほしいと思います。興味のある方はcollectionsモジュール全体を一度深掘りしていただくといいかと思います。
・Counter:
データの件数を数えるためのものです。
len関数では単純な要素数が返ってきますが、Counterは、オブジェクトを数えるという機能に特化していますので、どれだけのものが何件あるのかを返してくれます。データを捉えたい、どのようになっているか把握したいときによく使われる機能です。特に辞書に対して使うと便利だと思います。
様々なサンプルデータを使って試してもらえれば、十分挙動は確認できるかと思います。
・defaultdict
最初は理解しにくいと思いますし、実際に使うのも結構難しいかもしれませんが、defaultdictを使えるようになるととても便利です。
defaultdict がよく使われる例としては、int型 で定義する場合や、list型 で定義する場合などがあります。こうした使い方は比較的よくあるパターンではないでしょうか。私自身も、int を使った defaultdict や、list を使った defaultdict を作ることがよくあります。
では、なぜこのようなものを使うのかという点ですが、まずは int型 の defaultdict を例に説明します。
たとえば、カウンターのように値を「プラス1」しながら集計したい場合があります。ある要素が出現するたびに、その要素数を辞書に格納し、「この要素は何回出てきたか」を足し算しながら数えていくイメージです。通常の辞書でこれを行う場合、対象のキーがまだ存在しなければ、まずキーを0で初期化してから値を加算する必要があります。
続いて、リストの場合ですが、辞書の値としてリストを持たせ、そこへ .append()を使って要素をどんどん追加していきたいケースでよく使われます。
ただ、通常の辞書では、対象のキーがまだ存在していない場合、いきなり.append()を呼び出すことはできません。そこで defaultdict(list)と書いたオブジェクトを用意しておくと、新しいキーが追加されたタイミングで、自動的にlistのコンストラクターで初期化されてリストが展開され、中に入ってくるという仕組みができます。その結果、最初から.append()を使えるようになります。
この辺の便利さは、実際にやってみないとなんとも言えないところかもしれませんが、defaultdictにできる箇所を見つけて慣れてもらうしかありませんので、積極的に使ってみてください。
・OrderdDict:
OrderdDict自体は、最近ちょっと出番が減っているように思えるものかもしれません。というのも、Pythonの辞書が、一時期、値を挿入された順番を保持しないという仕様になっていたため、このOrderdDictを使うことで、しっかり順番を保持したまま、辞書的な動きをして欲しいというときに使われていました。
辞書に関しては、挿入順番を意識するという使い方はそんなに多くはないと思いますが、それでも順番に処理をしたいというときはありますので、そういったときに明示的にするためにOrderDictを使用します。
ただ、現在のPythonの仕様では辞書の挿入順番は固定されるようになっていますので、そのままでも問題なくなったためにこのOrderdDictの出番は減っています。こういうものがあるんだなということは知っておいてもらえればと思います。
・namedtuple:
namedtupleを使用するとタプルに名前を付けて管理することができます。タプルは順番に取り出すことができますが、辞書的に名前でも呼び出せるようにするために便利なオブジェクトとして使われています。大きなライブラリーの中では結構使われているようなものです。
9.4列挙型による定数の定義を行う
9.4は列挙型を定義するenumについて学びます。
例えば、Webシステムなどでプルダウンの要素があるときにenumでしっかり定義しておくときに使用されたりします。もちろん定義せずとも、文字列としてプルダウンを作ったとしてもそれで成り立ちますが、最近の型安全について考えるなら、このenum型で、この選択肢以外は受け入れられないことを宣言しておくことになります。それによってコードも見やすくなりますし、これ以外のデータが来たとしても対処がしやすくなります。
enumは今の時点では比較的、型安全や、APIをしっかり定義するという部分でよく使われています。私自身も、Pydantic (パイダンティック)のバリデーションなどを書くときには、しっかりenum型で宣言して使うようにしています。そうしておくことで、早い段階で適切なバリデーションができますし、コードの中にそれ以外のデータが入ってこないということが保証されますので、コードが見やすくなります。そのため、私はenum型が機能として追加されてからは積極的に使うようになっています。
enum型で定数化して、型安全として便利に使ってみてください。
9.6イテレーターの組み合わせで処理を組み立てる―itertools
イテレーターの組み合わせで処理するitertoolsが紹介されています。
イテレーターであるため、連続でいろんなものの処理ができる、リストのようなものですが、このイテレーターをどのように扱うかがポイントになります。
・chain():
chain()は、複数のオブジェクトを連続したイテレーターにすることができます。例えば、それぞれ別のリストや数字、タプル、辞書が複数あったときに、ただ単純にフラットな1行・1列という1つの要素の中に連続した要素として入れたい場合に使用されます。
・groupby()
groupby()は連続する値を一つにまとめます。
この処理は、同じ文字列のデータをグループ化して、このグループ化した処理をした後にイテレーションを回せることができるようにするためのものです。
用途が限られているため、扱いは難しいと考える方は多いと思いますが重要で便利な機能ではあります。
・islice()
リスト型であれば、たとえば [:3] のように書くことで、先頭から3要素を取得できます。インデックス番号で言うと 0、1、2 までを順番に取得してくるイメージです。
ただ、イテレーターやジェネレーターのようなものの場合、要素が最初から決まっていないという理由でブラケットアクセスができない場合があります。
そうした場合に使えるのが itertools.islice です。
islice を使うことで、リストに対して [:3] と書く代わりに、「どこまで取得するか」をイテレーターに対して指定でき、また、start、stop、step といった指定も可能になります。つまり、リストに対して行うスライス操作を、ジェネレーターのようなイテラブルオブジェクトに対しても実行できる、ということです。
特にライブラリーを使っていると、返ってくるオブジェクトが必ずしもリストとは限りません。その場合、いったん list() に変換してからスライスすることもできますが、islice を使えば、変換せずに効率よく必要な部分だけ取り出せます。
・zip()、zip_longest()
これは複数のイテラブルを組み合わせて扱いたいときに使うものです。
たとえば、名前のリストと年齢のリストがあり、それぞれに3つずつ要素が入っているとします。この2つを zip() に渡すと、1番目の要素の組み合わせ、2番目の要素の組み合わせ、3番目の要素の組み合わせで取得できるようになります。
つまり、別々のリストとして存在していても、順番が保証されている場合には、それらをうまく組み合わせて取ってくることができます。こちらはデータの短いほうに揃えることになっていますので、3要素と4要素来た場合には、3要素までしか返ってきません。
一方、zip_longest()は先ほどの要素数が違う場合に、長いほうに合わせます。
とはいえ、そもそも要素が合わない時にzip()を使っていいのかという問題はありますし、要素数が違ったとしてもエラーにならないようにしたいときはあります。そのためにzip_longest()が歴史的にずっと存在し続けているのではないかと思います。
・データを組み合わせたイテレーターを取得する
組み合わせたイテレーターを取得するproductなどが紹介されていますが、書籍を参考にしていただきつつ、実際にコードを打って動きを感じ取っていただくのが一番かと思います。
itertoolsは結構奥が深く、数学的プログラミングのアルゴリズムを考える上ではとても面白い部分かとは思います。必要になったときにこういうのがあると思い出せるようにしておくのがポイント化と思います。
9.7ミュータブルなオブジェクトをコピーする―copy
ミュータブルなオブジェクトをコピーするためのcopyモジュールについて解説されています。
Pythonの変数代入については、名札やラベルをつけているという感覚が正しいかと思います。
例えば、「5」という数字に対して、「user_id=」とつけると、5にはuser_idという名札を付けたということになります。その後、「user_id」に対して、次の行で、別の数字(例:7)と入れると7にデータが置き換わります。これは名前付けなので、コピーをしているわけではないためです。
では、リストの中の要素に対して、どのような扱いを求めるかと言えば、いろいろなパターンがあります。
例えば、3要素のリストがあったときに、それぞれに1、2、3っていう要素があったとします。この時、2番目の要素である「2」を「20」に変更しようすればリストも変わります。
例えば、引き継いだ後に、そのリストを加工してしまえば、外に影響してしまう可能性がでてしまいます。とはいえ、影響されてしまっては困りますので、copy.copy()で別のオブジェクトを作って扱えば前のものを変えずに適切に扱うことができます。
ただ、リストの中にリストがあるような、ネストされた構造の場合は、ネストされた内側は参照が使われているため、コピーされません。それを防ぐにはdeepcopy(ディープコピー)という手法が使われます。ディープコピーについて勉強していただければ、Pythonの中でリストがどのように扱われているのか、理解できるかと思います。その部分を理解するためにもこの部分の学習はしっかりしてもらえればと思います。
