Pythonの基本的なデータ構造とその便利な使い方

みんなのPython勉強会 #112
2/13, 2025
辻真吾(www.tsjshg.info)

自己紹介

  • 辻 真吾(www.tsjshg.info

  • Pythonを使ったデータサイエンスが得意

    • バイオインフォマティクス
    • エネルギーシステム
    • 学習支援
  • 最近はRustを書きたい

  • RISC-Vに興味がある

  • Python、データサイエンス、アルゴリズムに関する著書多数

  • 毎月1回オンラインで『みんなんのPython勉強会』をやっています

応用基礎としてのデータサイエンス 改訂第2版の表紙
  • 応用基礎としてのデータサイエンス 改訂第2版 AI×データ活用の実践
  • 生成AIの解説など随所に新たなトピックが追加され448ページ!
  • 講談社のサイト
SoftwareDesign2025年2月号表紙
  • 第1特集2章データ構造を書きました(おおきな本屋さんならまだあるかも)
  • 3名様にプレゼント企画あります(電子版)

今日の話

  1. 基本的なデータ構造のはなし

  2. データ構造を組み合わせて使うはなし

  3. 比較のはなし

リスト(list)

データを順番に保持するデータ型

my_list = [1, 2, 3]
# 要素の追加が可能
my_list.append(4)
my_list
[1, 2, 3, 4]
  • 変数名をlistにしない
    • 組込関数listがその後使えなくなってしまうため

タプル(tuple)

データを順番に保持するデータ型(変更できない)

my_tuple = (1, 2, 3)
my_tuple
(1, 2, 3)
  • 変更できないことで辞書のキーにできる(後ほど紹介)

辞書(dict)

データの対応を保持するデータ型

my_dict = {'A': 1, 'B': 2, 'C': 3}
my_dict['B']
2
  • キーと値のペアは追加可能
  • Python3.6から追加した順序が保持されるようになった
    • 実装を見直して高速化した際の副産物
    • Python3.7から言語仕様

集合(set)

順序を持たず重複のないデータの集まり

my_set = {1, 1, 2, 3, 3}
my_set
{1, 2, 3}
  • 要素の追加は可能
  • setでは挿入順序は保持されません

nikkie-ftnextの日記

frozenset

変更できないset

my_fs = frozenset((1, 2, 2, 3, 3))
my_fs
frozenset({1, 2, 3})
  • 知名度が低い気はする
  • リストとタプル、setfrozenset

リストは辞書のキーにできない

my_dict[['A', 'B']] = 'エビ'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[94], line 1
----> 1 my_dict[['A', 'B']] = 'エビ'

TypeError: unhashable type: 'list'
  • 代わりにタプルを使う

リストはsetの要素にもできない

my_set.add([5, 6])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[95], line 1
----> 1 my_set.add([5, 6])

TypeError: unhashable type: 'list'

同じエラーが出ている

hash化可能

組込関数hashでエラー無くハッシュ値(整数)が得らるオブジェクトは辞書のキーやsetの要素にできる

hash(my_list)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[96], line 1
----> 1 hash(my_list)

TypeError: unhashable type: 'list'
hash(my_tuple)
529344067295497451
  • タプルだと整数値になっているのがわかる

ハッシュ値とは?

  • オブジェクトから計算される整数値
    • 多くの場合は固定長(桁数が決まっている)
  • 基本的には同じオブジェクトからは同じ数値が返り、違うオブジェクトのハッシュ値は違う(時々衝突する)
  • 辞書やsetの内部で利用されている
  • 暗号学的ハッシュ関数はデータが改竄されていないかの確認に使える
    • MD5ハッシュ値など
  • 組込関数hash以外に暗号学的ハッシュ関数を揃えたhashlibがある

リスト内包表記

メニューは料理名と価格のペアをタプルで保持

# タプルのリスト
# 改行しているのはPDF資料のためで、1行で書きます
menu = [("刺身定食", 1200), ("あじフライ定食", 1000),
        ("天ぷらうどん", 900)]
[v[0] for v in menu]
['刺身定食', 'あじフライ定食', '天ぷらうどん']

メニューの名前だけからなる新たなリストを作成

複雑なリスト内包表記

menu = [("刺身定食", 1200), ("あじフライ定食", 1000),
        ("天ぷらうどん", 900)]

[(x[0], y[0]) for x in menu for y in menu if x != y]
[('刺身定食', 'あじフライ定食'),
 ('刺身定食', '天ぷらうどん'),
 ('あじフライ定食', '刺身定食'),
 ('あじフライ定食', '天ぷらうどん'),
 ('天ぷらうどん', '刺身定食'),
 ('天ぷらうどん', 'あじフライ定食')]
  • 重複を許さないメニューの組み合わせ
  • わかりにくくなるので、普通にfor文で書く方が良いという意見が多い

辞書の内包表記

menu = [("刺身定食", 1200), ("あじフライ定食", 1000), 
        ("天ぷらうどん", 900)]
{v[0]: v[1] for v in menu}
{'刺身定食': 1200, 'あじフライ定食': 1000, '天ぷらうどん': 900}

キーは料理名、値は価格になっている辞書を作成

セットの内包表記

menu = [("刺身定食", 1200), ("あじフライ定食", 1000), 
        ("天ぷらうどん", 900), ("刺身定食", 1200)]
{v[0] for v in menu}
{'あじフライ定食', '刺身定食', '天ぷらうどん'}
  • 「刺身定食」が1つにまとまる
  • 追加した順序という概念がないことがわかる
  • 個人的にはあまり使わない印象がある

リストのソート

my_list = [6, 4, 3]
sorted(my_list)
[3, 4, 6]
  • sorted関数とsortメソッドがある
  • sortメソッドを使うとリスト自体を変更する

ちょっと高度なリストのソート

menu = [("刺身定食", 1200), ("あじフライ定食", 1000), 
        ("天ぷらうどん", 900)]
sorted([(v[1], v[0]) for v in menu])
[(900, '天ぷらうどん'), (1000, 'あじフライ定食'), (1200, '刺身定食')]

各タプルの最初の要素で昇順にソートしてくれる

key引数を使う

menu = [("刺身定食", 1200), ("あじフライ定食", 1000), 
        ("天ぷらうどん", 900)]
sorted(menu, key=lambda v: v[1])
[('天ぷらうどん', 900), ('あじフライ定食', 1000), ('刺身定食', 1200)]
  • sorted関数のkey引数に関数を渡してどの値でソートするかを指示できる
  • lambdaを使った無名関数はこういう場面で便利

itemgetterを使う

from operator import itemgetter

menu = [("刺身定食", 1200), ("あじフライ定食", 1000),
        ("天ぷらうどん", 900)]
sorted(menu, key=itemgetter(1))
[('天ぷらうどん', 900), ('あじフライ定食', 1000), ('刺身定食', 1200)]
  • importが面倒なので、個人的にはlambdaで無名関数を渡すのが好き
  • やっていることがわかりやすいという意見もある

辞書のキーでソート

# 順番が保持される
my_dict = {'b': 2, 'c': 3, 'a': 1}
for k in sorted(my_dict):
    print(f"{k} : {my_dict[k]}")
a : 1
b : 2
c : 3
  • そのままforで表示するとリテラルで記述した順番になる

小ネタ

enumerate関数にはstartという引数がある

sakana = ['いわし', 'さんま', 'かつお', 'まぐろ']
for i,v in enumerate(sakana, start=10):
    print(i, v)
10 いわし
11 さんま
12 かつお
13 まぐろ

オブジェクトの比較

'A' == "A"
True

書き方の違いでしかないので、当たり前

n = None
n is None
True

Noneはシングルトン(その実行環境にインスタンスが1つだけ)なので、isis notを使った比較をしてくださいとPEP8(Pythonの公式コーディング規約)に書いてある

見た目が違っても同じ

1 == 1.0 == True
True
{1, 1.0, True}
{1}
  • Pythonでは全部同じ値
  • setを作ると1つにまとまる

リストの比較

[1, 2, 3] == [1, 2, 3]
True
[1, 3, 2] == [1, 2, 3]
False
{1, 3, 2} == {1, 2, 3, 2}
True
  • リストは順番を考慮する
  • セットは要素だけを見て比較する

namedtupleのすすめ

from collections import namedtuple
Dish = namedtuple('Dish', ['name', 'price'])

menu = [Dish('刺身定食', 1200), Dish('あじフライ定食', 1000),
        Dish('天ぷらうどん', 900), Dish('とろろそば', 900)]
menu
[Dish(name='刺身定食', price=1200),
 Dish(name='あじフライ定食', price=1000),
 Dish(name='天ぷらうどん', price=900),
 Dish(name='とろろそば', price=900)]
  • 各要素に名前を付けられるタプル
  • タプルとして使える
    • menu[0][0] == "刺身定食"Ture

namedtupleのソート

sorted(menu, key=lambda x: x.price)
[Dish(name='天ぷらうどん', price=900),
 Dish(name='とろろそば', price=900),
 Dish(name='あじフライ定食', price=1000),
 Dish(name='刺身定食', price=1200)]
  • 値段でソートしていることがすぐわかるコードにできる
  • Pythonのソートは安定
    • 安定なソートはもとの順序を保持
    • 値段が同じ場合は、先の「天ぷらうどん」と後の「とろろそば」の順序が入れ替わることはない

NamedTuple

型ヒント付きの名前付きタプル

from typing import NamedTuple

class TypedDish(NamedTuple):
    name: str
    price: int

sashimi = TypedDish('刺身定食', 1200)
aji = TypedDish('あじフライ定食', '1000')

型が違ってもエラーにはならない

比較の実装

class TypedDish(NamedTuple):
    name: str
    price: int

    def __eq__(self, value):
        return self.price == value.price

tempura = TypedDish('天ぷらうどん', 900)
tororo = TypedDish('とろろそば', 900)
tempura == tororo
True
  • 値段が同じなら同じと判断する大雑把な比較を実装
  • 複雑なことができるけど基本はタプル

dataclass

from dataclasses import *

@dataclass
class Dish:
    name: str
    price: int

tempura = Dish('天ぷらうどん', 900)
  • ちょっとしたクラスを作るにはdataclassが便利
  • namedtupleNamedTupledataclassどれがいい?
    • dataclassを1番に考えるのがよさそう
    • dataclass(frozen=True)とすれば手軽にハッシュ可能になる(thanks to たかのりさん)

まとめ

  • Pythonで使われる基本的なデータ構造の紹介
  • ハッシュ可能なオブジェクトだけが辞書のキーやsetの要素になれる
  • 内包表記は便利
    • 深いループも書けるけどおすすめできない
  • リストや辞書のソート
  • オブジェクトの比較と名前付きタプル
    • dataclassがおすすめ

ご清聴ありがとうございました!