見出し画像

ゆるいPython [3]

前回からの続きです。


データ構造

リストのようなデータのことを「データ構造」と呼びます。プログラミングの基礎となるデータは数です。制約条件を持たせて数を複数集めることで作られるのがデータ構造です。
Pythonでデータとしての文字を表現したいときには「文字列」というデータ構造を使います。文字列は,次のように引用符で囲んで表現します。Pythonのプログラム自体,文字を使って記述します。プログラムを記述するために必要な,変数名や記号のような文字と分けるために,引用符を使うのです。引用符はちょうど,日本語の文章に出てくるカギ括弧のような役割で使われます。
プログラムを実行してみたい人は,こちらのリンクをクリックしてください。

職業名 = "勇者"

Pythonの内部では,文字列は数として扱われています。国際ルールによって決められた文字と数の対応を元に,「A」なら65,「あ」なら12354というように自動的に変換されます。大文字の「A」と小文字の「a」は別の文字として扱われます。だからコンピュータはたいてい「A」と「a」を区別するのです。「0」のような数字も48のような数になります。
数に変換される文字の集まりを「文字集合」といいます。文字集合には,アルファベットや記号,漢字や絵文字など,コンピュータで扱う膨大な文字が含まれています。文字列は,Pythonの内部では文字集合に対応する離散数として扱われていることになります。文字を表示するときは,文字集合と数の対応が逆に辿られて,数から文字への変換が行われるのです。
文字列として扱われるのは,短い単語や文章のような一般的な文字データだけではありません。生命の設計図「遺伝子」も,文字列としてプログラム上で扱われています。遺伝子は4種類の部品で構成されています。部品となる「塩基」の頭文字を取って「ATGC」の4種類の文字を組み合わせた文字列として,Pythonで扱うことができます。
人間の遺伝子「ヒトゲノム」は36億の長さを持った文字列データとして扱えます。生物の遺伝子を文字列にして比べることで,遺伝子の類似度を調べることができます。文字列はリストと同じく,離散数を順番に並べたデータ構造です。
遺伝子情報を文字列というデータ構造として扱うことで,様々な「応用」が開けます。たとえば,ループを使って一文字ずつ取り出して比較することで,塩基AとBの構造がどの程度似ているかをプログラムで調べることができます。文字列情報の中に特別なパターンを持った文字列があるかどうかを調べることで,分かることも数多くあります。遺伝子は設計図のようなものです。遺伝子を調べることで,特定の機能があるかどうかや,遺伝子が由来になって起こる疾病の起こる確率を調べることもできます。
物質としての遺伝子を文字列に置き換えるには,シーケンサと呼ばれる特別な機械を使います。シーケンサを使うと,壊れやすい生の物質から高速に塩基情報を取り出し,安価に離散情報に置き換えることができます。ヒトゲノムのような複雑な遺伝子だけでなく,動植物から細菌まで,ありとあらゆる生物の遺伝子が解析され,文字列に変換されています。
パソコンやスマホを使うと,紙に書いた文字を書き換えるよりも数段簡単に文字編集を実行できます。これは,離散数を入れ替えたり,差し込んだりする手順をプログラムが自動実行してくれるおかげです。これとまったく同じように,というわけにはいきませんが,ゲノム情報も切ったりはったりする編集技術があるのです。人類社会に脅威を及ぼす類いのウイルスにmRNAで対抗できるのは,近年の高度なゲノムの解析と編集技術の恩恵によるものです。

組み込み型

これまでのプログラムでは,読みやすさを優先して変数名として日本語を使ってきました。世界中にいるPython使いにとって読みやすいように,英小文字と数字を組み合わせた名前をつける,という暗黙のルールがあります。今後はこのルールを守ってプログラムを作ることにしましょう。

# 魔法使いのデータ構造
name = "魔法使いA"
hp = 50   # 体力
mp = 30   # 魔法力
exp = 12  # 経験値
level = 2    # レベル
inventory = ["魔法の杖", "魔法の帽子", "命の指輪"] # 持ち物

このように,色々な種類のデータを組み合わせて作った変数のグループもデータ構造の一種です。変数に代入されているリストや文字列もデータ構造の一種ですが,数とうまく組み合わせて使うと,さらにいろいろなものごとをPythonに取り込むことができます。プログラムにしたいものごとを要素に分割し,名前をつけます。それぞれの要素の性質に合わせて,連続数と離散数を組み合わせて使うのです。
変数に代入され,データ構造を形成するための基本となるデータのことをPythonでは「組み込み型」と呼んでいます。皆さんが知っているものの中では,「数値(数)」,「リスト」と「文字列」が組み込み型の仲間です。この三種類は,Pythonのプログラムを作るときに基本となる組み込み型です。
6個の変数を組み合わせて作られたデータ構造は,ゲームの登場人物の「状態」を表現しています。
状態を変更する「できごと」が発生しました。

damage = 10  # 戦闘で敵に受けたダメージ
if "命の指輪" in inventory:
    # 命の指輪を持っていると,ダメージが半減する
    damage /= 2  # damageの値を1/2にする
hp -= damage

「何を」を表現するために使われているのは,変数を組み合わせて作ったデータ構造です。「どうする」の部分は,プログラムの手順を組み合わせて変数を変更することで作ります。必要に応じて変数を調べたり,変数を変えたり,時にはループを使いながら手順を組み立てて行きます。
Pythonに慣れてくると,変数への代入や条件分岐などが集まって,一つの「ものごと」や「できごと」を表現していることが,プログラムを読むだけで理解できるようになってきます。プログラムに潜む「暗黙の構造」を読み解くことができるようになるのです。

辞書

変数と組み込み型を組み合わせてデータ構造を作り始めると起こる問題の一つは「名前の衝突」です。プレイヤーが操作するゲームの登場人物が戦う敵を,データ構造として表現するときのことを考えます。登場人物と敵はまったく同じか,とてもよく似たデータ構造を持っているはずです。敵の体力を保存するため,変数に数を代入してみましょう。

hp = 20  # スライムの体力

Pythonで同名の変数に再代入を行うと,値の更新が行われることを思い出して下さい。この変数名には魔法使いの体力が保存されていたので,そちらの体力が20に減ってしまいます。同じ種類のデータ構造をプログラムで複数扱いたい時に,名前の衝突が起こってしまうのです。
名前の付け方を工夫することで,この問題を解決することができます。たとえば「wizard_hp」や「slime_hp」のように,変数名の前に接頭辞を付けることで別の変数にするのです。
Pythonの「辞書」というデータ型を使うことでも,問題を解決することができます。

wizard = {"name":"魔法使いA",  # 名前
          "hp":50,            # 体力
	      "mp":30,            # 魔法力
		  "exp":12,           # 経験値
		  "level":2,          # レベル
		  "inventory":["魔法の杖", "魔法の帽子", "命の指輪"]
		 }

記号が多く複雑に見えますが,ちょっとしたポイントを抑えると,このプログラムがデータ構造に見えてきます。まず,変数名に相当する部分が,コロンの左側に文字列として書かれています。コロンの右側には,変数名に代入されていた数や文字列,リストなどの組み込み型が添えられています。コロンの右側を変数名に,コロンを「=」に読み替えると,波括弧で囲まれた部分がデータ構造に見えてくるはずです。
辞書は組み込み型の仲間です。ですから,変数に代入して名前を付けることができます。同じように敵のデータ構造を辞書にして,変数に代入してみましょう。

slime = {"name":"スライムA",  # 名前
         "hp":20,           # 体力
		 "mp":0,            # 魔法力
		 "exp":5,           # 経験値
		 "level":1,         # レベル
		 "inventory":["天の涙"]
		}

プログラムで複雑なものごとを表現するとき,一つの変数にデータ構造を代入しておけるのはとても便利です。変数名の衝突を避けながら,同じ種類のデータ構造を管理できるからです。
辞書で変数名に相当する部分のことを「キー」と言います。代入されるデータのことを「値」といいます。辞書とキーを使うと,値を取り出すことができます。辞書の入った変数に続けて角括弧を書き,その中にキーの名前を文字列にして置くのです。次のプログラムでは,辞書のキーを使って値を取り出し,さらに引き算を行って値の更新をしています。

slime["hp"] -= 12  # スライムの体力を12減らす
wizard["hp"] -= 5   # 魔法使いの体力を5減らす

名前空間

プログラムが複雑になってくると,変数の種類が増えて行きます。すると,変数の中に入っているデータを頭の中で覚えておくことができなくなってきます。コンピュータは,人間の限られたワーキングメモリーよりずっと容量が大きく,かつ高速にアクセスできる記憶装置を持っています。実際にプログラムを動かすことで,いくつもの変数に記録された状態の更新を,コンピュータに行わせることができます。
変数の管理をコンピュータに任せることができれば,人間はデータ構造を選んでプログラムにしたいものごとをモデル化し,できごとを計算モデルで表現することに集中できます。小さなプログラムを作ったら,プログラムを実行して変数の状態を確認します。モデル化とプログラムの実行をくり返すのが,プログラムを作るときのよくあるサイクルです。たいていのプログラムは,ゴールを目指して一気呵成に作られることはありません。コンピュータと対話をしながら,漸近的に,時にはゴールを変更することもいとわず,作り上げるのがプログラムなのです。
Pythonのようなプログラミング言語は,変数を覚えておくための特別な仕組みを持っています。「名前空間」と呼ばれる仕組みで,変数名と代入されたデータの組み合わせを覚えてくれます。
新しい変数名に対して代入が行われたとき,Pythonは名前空間に名前を追加します。既存の変数名に対して再代入が行われたときは,名前空間から同じ名前を取り出し,中にあるデータを更新します。これが,Pythonが変数を管理するために使う基本ルールです。
プログラムを動かし始めた状態では,まだなにも変数が登録されていません。名前空間は「空(から)」の状態になっています。名前空間が空の状態で,次のプログラムを実行すると,Pythonは名前空間に「language」という名前を登録します。そして,変数に対応するデータとして,「日本語」という文字列を登録します。

language = "日本語"

Pythonの辞書は,名前空間ととてもよく似た動きをします。「{}」というように波括弧の中に何もない記号を書くことで,キーと値が一つも登録されていない「空(から)の辞書」を作ることができます。ちょっとした実験をしてみましょう。

namespace = {}

この辞書に対して,新しいキーを使った代入を行うと,キーと値のペアが追加されます。

namespace["language"] = "日本語"

この状態で,namespaceという変数に入った辞書には”language”というキーが追加されます。このキーに対応する値は”日本語”という文字列です。
辞書に対して,未知のキー添えると,新しいキーと値を追加します。ちょうど新しい変数に対して代入を行うのと同じですね。既存のキーを角括弧に添えて代入を行うと,キーに対応する値を更新します。これは既存の変数を更新する場合に相当します。このように,辞書はPythonの名前空間にとても似た振る舞いをすることがわかります。

オブジェクト

名前空間と似た振る舞いをする辞書は,データ構造を手軽に登録することができてとても便利です。反面,波括弧や角括弧,引用符など,記号を多く使う必要があり,プログラムを読んだり書いたりするのが面倒というのが欠点と言えます。
Pythonには,変数と似たシンプルな記法でデータ構造を運用できる「オブジェクト」という仕組みがあります。まず,日付のデータ構造を「読み込む」ためのプログラムを実行します。

from datetime import date

どこから読み込むのか,具体的に何を読み込んでいるのは何かということについては,今は知らなくてもかまいません。「date」という名前に注目して,次のプログラムを読んでみて下さい。

doha_miracle = date(2022, 11, 23) # ドーハの奇跡

まず,イコールの右側を見てみましょう。ここに書かれているのは,皆さんが知っているPythonの知識から言うと「関数の呼び出し」のように見えます。3つの引数は,一般常識を使って「西暦,月,日」という並びになっていることが分かるはずです。
イコールの左にある変数名は,コメントにある「ドーハの奇跡」から取られたものだと分かります。関数呼び出しのように見える部分の結果が,変数に代入されています。
doha_miracleという変数には,日付のデータ型が入っています。変数名に続けて「ドット(.)」を書き,「year」という変数名を続けると,日付のデータ構造から西暦だけを取り出すことができます。同様に「doha_miracle.month」や「doha_miracle.day」のようにして,月や日を取り出すことができます。

doha_miracle.year  # 西暦を取り出す

このことから,doha_miracleという変数の中には,「year(西暦)」「month(月)」「day(日)」という3種類の変数からなる日付のデータ構造が入っていることが分かります。また,データ構造の入った変数(doha_miracle)から,中のデータ構造を取り出すためには,ドットに続けて取り出したいデータの「名前」を書けばよい,というルールがあることも分かります。
このようにデータ構造を変数に代入して運用できる仕組みを,Pythonでは「オブジェクト」と呼ばれています。データ構造を変数の中に入れることができるので,名前の衝突が起こりづらい,という利点があります。データ構造の中にある変数を取り出すためにはドットを使うので,辞書より記法が煩わしくないというのも良い点です。
同じ種類のデータ構造から,オブジェクトをもう一つ作ってみましょう。別の日付を,新しい変数に代入します。

doha_tragedy = date(1993, 10, 28)  # ドーハの悲劇

ところで,先ほどもでてきた「date」とはなんなのでしょうか。これは,日付のデータ構造につけられた「名前」です。Pythonでは「クラス」と呼ばれています。
数の入った変数だけをプログラムとして実行すると,中に入っている数が表示されました。同じようにして,doha_tragedyという変数に入っている「日付(というデータ構造)のオブジェクト」を表示すると,何が表示されるのか見ましょう。

datetime.date(1993, 10, 28)

「datetime」というのはデータ構造に添えられた「分類名」で,「日時に関するデータ」くらいの意味です。日付のデータ構造を読み込んだときのプログラム「from datetime import …」に出てきたのを覚えているでしょうか。分類名から,ドットを挟んでデータ構造の名前である「date」が書かれています。日付のデータ構造から中のデータを抜き出すときにもドットを使いました。このことから,ドットは「親子関係」を表現するために使われることが類推できます。
括弧の中に並んでいるのは,日付を表現するための数ですね。この数は,変数の中に入っているオブジェクトがどんな日付を表現しているのかによって変化します。
次のプログラムを見て下さい。二つの日付オブジェクトを引き算してみましょう。

doha_miracle-doha_tragedy  # 日付の差を計算

このプログラムを実行すると,次のような結果が表示されます。

datetime.timedelta(days=10618)

ドットの前に見えるのは,先ほどと同じ分類名です。その次の「timedelta」はデータ構造の種類に付けられた名前です。「delta(デルタ)」は「差」である,という予備知識と合わせてこの名前の意味を読み解くと,「時間の差」を表現するためのデータ構造であることが分かります。また括弧の中を見ると,2つの日付には10618日の差があることが分かります。
ところで,日付の差を計算した結果を表現するために,どのようなデータ構造が必要になるでしょうか。日付の引き算だから日付にすればいい,と考える人がいるかも知れません。1月1日を起点にして10618日を表現して「date(29, 10, 22)」のようにするわけですね。でもよく考えると,この方法はあまりスジが良くないことが分かります。月の長さはそれぞれ異なる上,一年の長さもうるう年かどうかによって変わります。
ここまで深く考えると,日付の引き算の差を日数にする,というのはかなり理にかなった戦略であることが分かります。データ構造を設計する神様がいて,「日付の差は日数で返すのだ。日付に日数を足すことも矛盾がないし,かけ算も引き算もできる。算術の持つ構造にもかなっておる」と言っているのが聞こえてくる気がしませんか。
「ドーハの悲劇」と「ドーハの奇跡」の間には1万の日差があることが分かりました。時間の差は秒でも表現することができますね。
まず,日差を表現するオブジェクトを,変数に代入してみます。

doha_delta = doha_miracle-doha_tragedy  # 日差を変数に代入する

その後,変数に続けてドットを書き,関数を呼び出して日差から秒差を計算します。

doha_delta.total_seconds()   # 日差から秒差を計算する

「917395200.0」という結果が表示されます。約9億1739万秒差,という答えが出ました。
このように,データ構造を表現するためのデータだけでなく,関数を持っているのもオブジェクトの特徴です。計算に使われたのは,オブジェクトの持っている日数をであることは容易に想像できます。計算に必要なデータはすべてオブジェクトが持っているため,秒差を計算する関数に引数が指定する必要がなかったのです。

次の記事へ


読んでくれて嬉しいです!


この記事が気に入ったらサポートをしてみませんか?