Bot プログラミングスキル 書籍・論文・web記事

開発記録#115(2024/10/15)「Pythonの話.2 データとオブジェクト:型、値、変数、名前」

前回の記事に引き続き、今回も仮想通貨botの開発状況をまとめていきます。

Yodaka

今回はpythonの基礎理解の学習まとめです。

関連
開発記録#112(2024/10/9)「Pythonの話.1 静的型付けと動的型付け」

続きを見る

Pythonのデータはオブジェクト

Pythonのデータはオブジェクトです。メモリ内のデータ値をオブジェクトの殻で包み込んでいます。

これは、ひとまとまりのデータをオブジェクトの箱に入れているようなイメージです。プログラム全体で見ると、オブジェクトの箱が棚に並べられている様子を思い浮かべると良いです。

オブジェクトの箱の中には主に4つの構造がある。

  • 型(データが何に使えるのかを表す)
  • 値(型に従って解釈できるデータの値)
  • ID(他のオブジェクトとの識別をするための位置情報のようなもの)
  • 参照カウント(メモリの管理)
Yodaka

本記事ではこれら4つの構造について詳しく解説していきます。

予備知識

Pythonにおいて「データはオブジェクトである」という表現は、Pythonの設計哲学である「すべてがオブジェクト(everything is an object)」を指します。これは、Pythonにおけるデータのすべての要素(数値、文字列、リスト、辞書、関数、クラスなど)がオブジェクトとして扱われるという意味です。この哲学により、Pythonは非常に一貫性があり、扱いやすい言語となっています。以下、この概念をさらに詳しく説明します。

オブジェクトとは

オブジェクトはデータとそのデータに対する操作(メソッド)をカプセル化するものです。Pythonにおける各データ項目はオブジェクトとして振る舞い、それぞれ固有の属性(プロパティ)とメソッド(関数)を持ちます。

オブジェクトの特性

  1. 属性とメソッド:
    • オブジェクトは属性(データ要素)とメソッド(そのデータに対する操作を定義した関数)を持ちます。例えば、文字列オブジェクトはテキストデータを保持し、文字列を操作するメソッド(.upper(), .lower()など)を提供します。
  2. 型情報:
    • 各オブジェクトは自身の型を知っており、その型に基づいて特定の振る舞いをします。例えば、数値オブジェクトは算術演算が可能ですが、文字列オブジェクトは異なる種類の操作(連結など)が可能です。
  3. 同一性と参照:
    • オブジェクトはメモリ内に一意のID(アドレス)を持ち、変数はオブジェクトへの参照として機能します。id() 関数を使ってオブジェクトのアイデンティティ(メモリアドレス)を確認できます。
  4. 不変性と可変性:
    • オブジェクトはイミュータブル(不変の)またはミュータブル(可変の)かのどちらかです。イミュータブルなオブジェクト(例: 数値、文字列、タプル)は作成後にその状態を変更できませんが、ミュータブルなオブジェクト(例: リスト、辞書)は内容を変更できます。

プログラミングの利点

  • 一貫性: データがオブジェクトとして統一的に扱われるため、異なる型のデータでも共通の操作(例えば、len() 関数で長さや要素数を取得)を行うことができます。
  • 拡張性: カスタムクラスを定義することで、独自のオブジェクトを作成し、特定の用途に適した振る舞いを実装することが可能です。

Pythonでは、基本的な数値や文字列から、より複雑なデータ構造や関数、クラスのインスタンスまで、すべてがこのオブジェクト指向のアプローチに従います。これにより、Pythonは直感的で強力なプログラミング言語となっています。

Yodaka

Pythonにおける型は、プログラムの正確性、効率、読みやすさを向上させるために中心的な役割を果たします。型はオブジェクトの振る舞いを理解し、正しい操作を行うための鍵となります。プログラミング中に型を意識することは、バグの発生を減らし、プログラムの保守を容易にするために重要です。

Pythonのオブジェクトに含まれる型(type)は、そのオブジェクトがどのような性質を持ち、どのように振る舞うかを定義します。Pythonは動的型付け言語であり、変数には明示的に型を指定する必要はありませんが、プログラム実行時にはすべてのオブジェクトが何らかの型を持っています。以下はPythonの型が持つ主な役割です。

役割

  1. データの表現:
    • 型はデータがどのようにメモリ上で表現されるかを決定します。たとえば、整数、浮動小数点数、文字列、リストなど、各データ型には固有のメモリ構造があります。
  2. 操作の定義:
    • 型はそのオブジェクトに対してどのような操作が可能かを定義します。例えば、数値型には加算や減算といった数学的操作が、文字列型には連結やスライスといった文字列操作が用意されています。
  3. メソッドの提供:
    • 特定の型は特定のメソッドや属性を持ちます。たとえば、リスト型には.append().remove()といったメソッドがあり、これらはリスト型オブジェクトでのみ使用できます。
  4. 安全性の向上:
    • 型によってデータの種類が制限されるため、不適切な操作を事前に防ぐことができます。例えば、整数に対して文字列操作を試みるとエラーが発生します。
  5. ポリモーフィズムのサポート:
    • Pythonでは、異なる型が同じインターフェースや抽象基底クラスを共有することができます。これにより、異なる型のオブジェクトを同様の方法で扱うことが可能になります(たとえば、forループで異なる型のコレクションを反復処理するなど)。

型を確認する方法

Pythonではtype()関数を使用してオブジェクトの型を確認することができます。例えば、以下のように使用します。

x = 10
print(type(x))  # <class 'int'>

y = "hello"
print(type(y))  # <class 'str'>

ミュータビリティ(可変性)

Pythonにおける「ミュータビリティ(可変性)」とは、オブジェクトの内容が作成後に変更可能かどうかを指します。Pythonのデータ型は「ミュータブル(可変)」と「イミュータブル(不変)」の二つに分類されます。

ミュータブル(可変)オブジェクト

ミュータブルオブジェクトは、作成後もその状態を変更できるオブジェクトです。これにより、同じオブジェクトに対して値の追加や削除、内容の変更などが可能になります。ミュータブルオブジェクトの例としては以下があります:

  • リスト(list
  • 辞書(dict
  • 集合(set

これらの型は、オブジェクトを作成した後にその内容を変更できるため、動的なデータ構造を扱う際に非常に便利です。

my_list = [1, 2, 3]
my_list.append(4)  # リストの末尾に4を追加
print(my_list)  # [1, 2, 3, 4]

イミュータブル(不変)オブジェクト

イミュータブルオブジェクトは、作成後その状態を変更することができません。この特性により、オブジェクトは安全に共有されることができ、予期しない変更から保護されます。イミュータブルオブジェクトの例としては以下があります:

  • 数値型(int, float
  • 文字列型(str
  • タプル(tuple

イミュータブルオブジェクトの内容を変更しようとすると、新しいオブジェクトが生成され、変更が加えられます。元のオブジェクトは変更されないため、プログラムの予測可能性と安全性が高まります。

my_string = "hello"
my_string = my_string + " world"  # 新しい文字列が作成される
print(my_string)  # "hello world"

ミュータビリティの重要性

ミュータビリティは、プログラムの設計において重要な要素です。ミュータブルオブジェクトは便利ですが、不注意によりデータが意図せず変更されることがあります。一方で、イミュータブルオブジェクトはその安全性から、関数の引数や返り値として使う際に副作用を避けるために選ばれることが多いです。また、イミュータブルオブジェクトはマルチスレッド環境での使用が安全であるという利点もあります。

Pythonでプログラムを書く際は、これらの特性を理解し、適切な型を選択することが、効率的かつ安全なコードを実現する鍵となります。

Pythonのオブジェクトに含まれる値は、そのオブジェクトが保持するデータや情報を指し、プログラムの実行において基本的で中心的な役割を果たします。Pythonはオブジェクト指向言語であり、ほとんどすべてがオブジェクトとして扱われます。以下に、オブジェクトに含まれる値の主な役割を説明します。

データの保持

オブジェクトの最も基本的な役割は、データを保持することです。オブジェクトは特定の型のデータ(数値、文字列、リストなど)を内部的に保持し、これによりプログラムは必要な情報を操作・管理することができます。

操作の対象

オブジェクトに含まれる値は、さまざまな操作の対象となります。例えば、数値オブジェクトに対する算術演算、文字列オブジェクトに対する文字列操作、リストオブジェクトに対する要素の追加や削除などです。これにより、プログラムはデータを動的に処理することが可能です。

プログラムの状態表現

オブジェクトに含まれる値は、プログラムの現在の状態を表現します。変数やデータ構造がプログラムの実行中に保持する情報は、その時点での計算結果や状態を反映しています。

メソッドの実行

クラスに基づくオブジェクトでは、オブジェクトに含まれる値がメソッド(クラス内で定義された関数)の実行に使用されます。例えば、オブジェクトが持つ属性(値)に基づいて異なる動作をするメソッドがあります。これにより、オブジェクト指向プログラミングの柔軟性と再利用性が向上します。

インターフェースの実現

オブジェクトに含まれる値は、他のオブジェクトや関数とのインターフェース(やり取り)の手段として機能します。オブジェクトが他のコンポーネントに渡されたり、関数から戻り値として返されたりすることで、プログラムの異なる部分が連携して動作します。

class Car:
    def __init__(self, color, mileage):
        self.color = color        # 車の色を保持する値
        self.mileage = mileage    # 走行距離を保持する値

    def drive(self, miles):
        self.mileage += miles     # driveメソッドによって走行距離を増加

# Carオブジェクトの作成と使用
my_car = Car("red", 20000)
print(my_car.mileage)  # 20000
my_car.drive(100)
print(my_car.mileage)  # 20100

この例では、Car クラスのオブジェクトは色と走行距離という値を保持しており、これらの値はオブジェクトの状態を表し、drive メソッドの動作に影響を与えます。このようにオブジェクトに含まれる値は、プログラムの挙動と状態管理の核心部分となります。

値:リテラルと変数

Pythonにおける「値」を理解する際、リテラルと変数の両方を考慮するのは正しいですが、それらは異なる概念を表しています。ここで少し詳しく説明します。

リテラル

リテラルは、ソースコードに直接記述される固定値のことです。プログラムが実行されると、リテラルはその表現した値自体を直接示します。リテラルの例には、数値リテラル(1233.14など)、文字列リテラル("hello")、リストリテラル([1, 2, 3])、辞書リテラル({"key": "value"})、ブール値リテラル(TrueFalse)などがあります。

変数

変数は、値を保持するための名前付きの場所(コンテナ)です。プログラムが実行されると、変数は割り当てられた値を参照する名前として機能します。変数にはリテラルだけでなく、他の変数の値や関数の戻り値など、さまざまなタイプのデータを割り当てることができます。

x = 5         # 整数リテラルを変数xに割り当て
y = x         # xの値を変数yに割り当て
z = "Hello"   # 文字列リテラルを変数zに割り当て

値としての理解

「値」という言葉は通常、プログラム中で扱うデータの具体的な内容を指します。これにはリテラルで直接書かれた値も含まれますし、変数に割り当てられている値も含まれます。したがって、リテラルは値の一形態であり、変数はその値を参照するための手段ということができます。

リテラルは不変の値を直接表しますが、変数は値そのものではなく、値が保存されている場所への参照または名前です変数の内容はプログラムの実行中に変更されることがありますが、リテラル自体は変更不可能です。そのため、Pythonにおける値とは、リテラルによって直接表されるデータや、変数を通じて間接的にアクセスされるデータの両方を含むと考えることができます。

変数:代入

Pythonにおける代入とは、変数に値を割り当てる操作を指します。代入を行うことで、プログラムのある時点での状態やデータを変数に保存し、後でその変数を通じてデータを参照したり操作したりすることができます。

Yodaka

Pythonでの代入の理解は、効率的でバグの少ないコーディングを行う上で基本的かつ重要です。

代入の基本形式

Pythonの基本的な代入の形式は以下の通りです:

変数名 = 値

ここで、= は代入演算子と呼ばれ、右側の値を左側の変数に割り当てます。

代入の特徴

  • 名前のバインディング: Pythonでは、代入を行うことで名前を値にバインド(結び付け)します。これにより、指定された名前(変数名)を使用して値を参照することができます。
  • ミュータブルとイミュータブルの扱い: 代入されたオブジェクトがミュータブル(可変)である場合(例: リスト、辞書)、オブジェクト自体を変更する操作がその変数を通じて行えます。一方、イミュータブル(不変)なオブジェクト(例: 数値、文字列、タプル)は、その内容を直接変更することはできず、新しい値を代入することで初めて変更されます。
  • 複数変数への同時代入: Pythonでは、複数の変数に同時に値を代入することも可能です。これをタプルアンパッキングやリストアンパッキングと呼びます。
x, y = 1, 2

代入の応用

  • チェーン代入: 複数の変数に同じ値を代入することができます。
x = y = z = 0

増分代入(オーグメント代入): 値を更新する際に、既存の変数に対して加算や乗算などの操作を行った上で代入を行うショートハンドが使えます。

x = 5
x += 1  # x = x + 1 と同じ

代入の影響

代入を行うことでプログラムの状態が更新され、その後の計算や操作に影響を与えます。また、代入は変数のスコープ(可視性と存続期間)にも関連しており、ローカル変数やグローバル変数への代入によって、その変数の有効範囲が異なります。

ID

PythonにおけるオブジェクトのIDは、そのオブジェクトに対する一意の識別子として機能します。具体的には、オブジェクトIDはそのオブジェクトのメモリアドレスを表す整数値です。このIDは、プログラム実行中、そのオブジェクトがメモリ上に存在する限り変わらないため、オブジェクトの同一性を確認するのに使用されます。

オブジェクトIDの役割

  1. オブジェクトの一意性の確認
    • 同一のオブジェクトかどうかを判断する際に、IDが利用されます。Pythonのid()関数を使ってオブジェクトのIDを取得し、二つのオブジェクトが同一のものか(つまり、メモリ上の同じ場所に存在するか)を確認できます。
  2. ハッシュテーブルのキーとしての利用
    • Pythonの辞書(dict)などのデータ構造では、オブジェクトのIDがハッシュ値として内部的に利用されることがあります。これにより、効率的なデータアクセスが可能となります。

a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(id(a))  # aのIDを表示
print(id(b))  # bのIDを表示
print(id(c))  # cのIDを表示

この例では、リストabは同一のオブジェクトを参照しているため、id(a)id(b)は同じ値を返します。一方で、cは内容はaと同じですが、別のオブジェクトとして生成されているため、id(c)aとは異なる値になります。

注意点

  • IDの一意性
    • IDはそのオブジェクトが生存している間は一意ですが、オブジェクトが破棄されると、そのIDは再利用される可能性があります。したがって、オブジェクトのライフタイムを超えてIDを信頼することは避けるべきです。
  • 異なる実行環境
    • 異なるプログラム実行や異なるマシン、またはPythonインタプリタの異なるインスタンスで同じ値を持つオブジェクトが同じIDを持つとは限りません。IDは実行中の環境に依存します。

Pythonのid()関数は、デバッグや内部状態の確認に非常に便利であり、オブジェクトがどのように管理され、参照されているかを把握するのに役立ちます。

参照カウント

Pythonにおける参照カウントは、メモリ管理の一環として非常に重要な役割を果たします。Pythonは、動的にメモリを管理する言語であり、オブジェクトが生成されると、そのオブジェクトを指す参照の数を追跡するために参照カウントというシステムを使用しています。

参照カウントの役割

  1. メモリ管理
    • Pythonのオブジェクトは、それを指し示す参照が存在する限りメモリ上に保持されます。オブジェクトに対する参照が作成されるたびに、そのオブジェクトの参照カウントが増加します。反対に、参照が削除されるか、参照範囲外になると参照カウントが減少します。
  2. ガベージコレクション
    • 参照カウントが0になると、Pythonはそのオブジェクトがもはやプログラムからアクセスされないと判断し、自動的にメモリからそのオブジェクトを解放します。このプロセスはガベージコレクションの一形態で、自動的にメモリリークを防ぐ役割を果たします。

参照カウントの例

import sys

a = []  # 空のリストを作成
print(sys.getrefcount(a))  # aの参照カウントを取得(通常は2が返される。1つはaの参照、もう1つはgetrefcountの引数としての参照)

b = a  # bにaを代入
print(sys.getrefcount(a))  # aの参照カウントが増加(3が返される)

b = None  # bの参照を解除
print(sys.getrefcount(a))  # aの参照カウントが減少(2が返される)

この例では、リストオブジェクトaが作成された時点で参照カウントが増加し、baが代入された時にさらに増加します。そして、bNoneを代入することで、aへの参照が一つ減り、それに伴い参照カウントも減少します。

注意点

参照カウントは循環参照の場合に問題を引き起こすことがあります。循環参照が発生すると、オブジェクト間で相互に参照し合っているため、参照カウントが0にならず、これらのオブジェクトが適切にガベージコレクションされないことがあります。Pythonではこの問題を解決するために、追加のガベージコレクタが用意されており、定期的に循環参照を検出して解消します。

Pythonの参照カウントは、効率的なメモリ管理を提供し、開発者がメモリリークについてあまり心配することなくプログラムを書けるように支援します。

予備知識

オブジェクト指向でない言語

予備知識2

Pythonにおいては、「すべてがオブジェクト」という設計原則に従っていますので、実際にはPythonのコンテキスト内でデータがオブジェクトでないケースは存在しません。すべてのデータ型(数値、文字列、関数、クラスなど)がオブジェクトとして扱われます。それぞれがメソッド、属性、および型情報を持っています。

ただし、プログラミング言語によってはデータがオブジェクトとして扱われないケースがあります。これを理解するために、Pythonとは異なるアプローチを取る言語の例を挙げて説明します。

C言語

C言語などのより低レベルの言語では、データはプリミティブ(基本的なデータ型)として扱われます。これには、整数型(int)、浮動小数点型(float)、文字型(char)などがあり、これらはオブジェクトではなく単なるメモリ上の値として扱われます。C言語では、これらのプリミティブにはメソッドや属性が関連付けられていません。

C++

C++もまた、プリミティブ型とオブジェクト型を区別します。C++にはクラス(オブジェクトの青写真)があり、これを使用してオブジェクト指向プログラミングを行いますが、基本的なデータ型(int, double, char など)はオブジェクトとして振る舞うことなく、単に値としてメモリ上に格納されます。

JavaScript

JavaScriptでは、プリミティブ型(数値、文字列、ブール値、null, undefined)とオブジェクト(オブジェクトリテラル、配列、関数など)があります。JavaScriptのプリミティブ型は基本的にはオブジェクトではありませんが、これらの型に対してメソッドを呼び出すと、一時的にオブジェクトのように振る舞います(オートボクシング)。

結論

多くのプログラミング言語では、データをオブジェクトとして扱うかどうかは言語の設計に依存します。Pythonのようにすべてをオブジェクトとして一貫して扱う言語もあれば、CやC++のようにプリミティブとオブジェクトを明確に区別する言語もあります。それぞれのアプローチには利点と欠点があり、使用するコンテキストやニーズに応じて選択されます。

Rust

予備知識3

Rustはシステムプログラミング言語で、安全性、並行性、そして速度に重点を置いて設計されています。Rustにおいては、データがオブジェクトであるかどうかという観点よりも、データの所有権、借用、および生存期間(ライフタイム)の管理に重きが置かれています。しかし、Rustでもデータ型とその振る舞いについては、多少オブジェクト指向の概念が適用されます。

Rustにおけるデータ型

Rustでは、データ型は大きく分けて二つのカテゴリに分類されます:

  1. プリミティブ型: これには整数型 (i32, u32 など)、浮動小数点型 (f32, f64)、ブール型 (bool)、文字型 (char)、タプルなどが含まれます。これらは単純な値として扱われ、メソッドやプロパティを直接持つことはありませんが、Rustの標準ライブラリにはこれらの型の値を操作するための多数の関数やマクロが用意されています。
  2. 複合型: これには構造体 (struct)、列挙型 (enum)、およびトレイト(インターフェースに似た概念)が含まれます。これらはメソッドや関連関数を持つことができ、オブジェクト指向のクラスに似た振る舞いを示します。

Rustにおけるオブジェクト指向プログラミング

Rustでは、伝統的なオブジェクト指向プログラミングの概念を採用していますが、いくつかの違いがあります:

  • トレイト: Rustのトレイトは、JavaやC#のインターフェース、またはC++の抽象基底クラスに似ています。トレイトを使用して、特定の型が持つべき振る舞いを定義し、さまざまな型に共通のインターフェースを提供します。
  • 構造体と列挙型のメソッド: Rustでは、構造体や列挙型にメソッドを定義することができます。これにより、データとそれを操作する関数を一緒にまとめることができ、カプセル化と情報隠蔽の原則が適用されます。
  • パターンマッチング: Rustの強力なパターンマッチング機能は、列挙型の値を検査し、その型に応じて異なるアクションを取ることを可能にします。これは、伝統的なオブジェクト指向言語の多態性を一種の方法で表現しています。

結論

Rustは、データを単純な値やより複雑なデータ構造として扱いますが、すべてが「オブジェクト」というわけではありません。ただし、オブジェクト指向のいくつかの概念はトレイトや構造体/列挙型のメソッドを通じて実現されており、これにより多態性やカプセル化などの利点を享受できます。このバランスにより、Rustはシステムレベルでのプログラミングにおいても非常に効果的です。

まとめ

今回はPythonで扱うデータの構造がオブジェクトであるということ。その中身が、型、値、ID、参照カウントの4つで構成されているということを解説しました。

また、それらのデータを扱う土台として、代入という概念やリテラルと変数の区別についても掘り下げました。

Pythonについて調べる中で、他の言語についても段階的に理解が深まってきていることも感じます。

Yodaka

Pythonの基本構造を丁寧に理解することで、bot開発の土台作りを固めていきます。

今後もこの調子でbot開発の状況を発信していきます。

-Bot, プログラミングスキル, 書籍・論文・web記事