Pythonのポインター:ポイントは何ですか?

Pythonのポインタ:ポイントは何ですか?

CやC ++のような低レベル言語で作業したことがあるなら、おそらくポインターを聞いたことがあるでしょう。 ポインターを使用すると、コードの一部を非常に効率的に作成できます。 また、初心者にとって混乱を招き、専門家であってもさまざまなメモリ管理バグを引き起こす可能性があります。 Pythonのどこにありますか?また、Pythonでポインターをどのようにシミュレートできますか?

ポインターは、CおよびC ++で広く使用されています。 基本的に、これらは別の変数のメモリアドレスを保持する変数です。 ポインタの復習については、このoverview on C Pointers.を確認することを検討してください。

この記事では、Pythonのオブジェクトモデルをより深く理解し、Pythonのポインターが実際に存在しない理由を学びます。 ポインターの動作を模倣する必要がある場合は、メモリ管理の悪夢を伴わずにPythonでポインターをシミュレートする方法を学習します。

この記事では、次のことを行います。

  • Pythonのポインターが存在しない理由を学ぶ

  • C変数とPython名の違いを調べる

  • Pythonでポインターをシミュレートする

  • ctypesを使用して実際のポインタを試す

Note:この記事では、「Python」は、CでのPythonのリファレンス実装(CPythonとも呼ばれます)を指します。 この記事では言語の内部について説明しているため、これらの注意事項はCPython 3.7には当てはまりますが、将来または過去の言語の繰り返しでは当てはまらない場合があります。

Free Bonus:Click here to get access to a chapter from Python Tricks: The Bookは、Pythonのベストプラクティスと、より美しい+ Pythonicコードをすぐに適用できる簡単な例を示しています。

Pythonにポインターがないのはなぜですか?

真実は私が知らないということです。 Pythonのポインターはネイティブに存在できますか? おそらく、しかしポインタはZen of Pythonに反するようです。 ポインターは、明示的ではなく暗黙的な変更を推奨します。 多くの場合、特に初心者にとっては、単純ではなく複雑です。 さらに悪いことに、彼らは自分自身を足で撃つ方法を懇願するか、またはあなたが想定されていなかった記憶の一部から読むような本当に危険なことをします。

Pythonは、メモリアドレスなどの実装の詳細をユーザーから抽象化しようとする傾向があります。 Pythonは、速度よりもユーザビリティに重点を置くことがよくあります。 その結果、Pythonのポインターは実際には意味がありません。 恐れることはありませんが、Pythonはデフォルトで、ポインターを使用する利点をいくつか提供します。

Pythonのポインターを理解するには、Pythonの実装の詳細を少し迂回する必要があります。 具体的には、次のことを理解する必要があります。

  1. 不変オブジェクトと可変オブジェクト

  2. Python変数/名前

メモリアドレスを保持して、始めましょう。

Pythonのオブジェクト

Pythonでは、すべてがオブジェクトです。 証明のために、REPLを開いて、isinstance()を使用して探索できます。

>>>

>>> isinstance(1, object)
True
>>> isinstance(list(), object)
True
>>> isinstance(True, object)
True
>>> def foo():
...     pass
...
>>> isinstance(foo, object)
True

このコードは、Pythonのすべてが実際にオブジェクトであることを示しています。 各オブジェクトには、少なくとも3つのデータが含まれます。

  • 参照カウント

  • Type

reference countはメモリ管理用です。 Pythonのメモリ管理の内部を詳しく調べるには、Memory Management in Pythonを読むことができます。

型はCPythonレイヤーで使用され、実行時の型の安全性を確保します。 最後に、値があります。これは、オブジェクトに関連付けられている実際の値です。

ただし、すべてのオブジェクトが同じというわけではありません。 理解する必要があるもう1つの重要な違いがあります。不変オブジェクトと可変オブジェクトです。 オブジェクトのタイプの違いを理解することは、Pythonのポインターであるタマネギの最初の層を明確にするのに本当に役立ちます。

不変オブジェクトと可変オブジェクト

Pythonには、2種類のオブジェクトがあります。

  1. Immutable objectsは変更できません。

  2. Mutable objectsは変更できます。

この違いを理解することは、Pythonでポインターのランドスケープをナビゲートする最初の鍵です。 一般的なタイプの内訳と、それらが可変か不変かを以下に示します。

Type 不変?

int

Yes

float

Yes

bool

Yes

complex

Yes

tuple

Yes

frozenset

Yes

str

Yes

list

No

set

No

dict

No

ご覧のとおり、一般的に使用される多くのプリミティブ型は不変です。 Pythonを書くことで、これを自分で証明できます。 Python標準ライブラリのツールがいくつか必要です。

  1. id()は、オブジェクトのメモリアドレスを返します。

  2. isは、2つのオブジェクトが同じメモリアドレスを持っている場合にのみTrueを返します。

ここでも、REPL環境でこれらを使用できます。

>>>

>>> x = 5
>>> id(x)
94529957049376

上記のコードでは、値5xに割り当てています。 追加してこの値を変更しようとすると、新しいオブジェクトが取得されます。

>>>

>>> x += 1
>>> x
6
>>> id(x)
94529957049408

上記のコードはxの値を変更しているように見えますが、応答としてnewオブジェクトを取得しています。

strタイプも不変です。

>>>

>>> s = "real_python"
>>> id(s)
140637819584048
>>> s += "_rocks"
>>> s
'real_python_rocks'
>>> id(s)
140637819609424

この場合も、sは、+=操作の後にdifferentメモリアドレスになります。

Bonus:+=演算子は様々なメソッド呼び出しに変換される。

listのような一部のオブジェクトの場合、+=__iadd__()に変換されます(インプレース追加)。 これにより、selfが変更され、同じIDが返されます。 ただし、strintにはこれらのメソッドがないため、__iadd__()ではなく__add__()が呼び出されます。

詳細については、Pythonhttps://docs.python.org/3/reference/datamodel.html#objectiadd [データモデルドキュメント]をご覧ください。

文字列sを直接変更しようとすると、エラーが発生します。

>>>

>>> s[0] = "R"
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'str' object does not support item assignment

上記のコードは失敗し、Pythonはstrがこのミューテーションをサポートしていないことを示しています。これは、strタイプが不変であるという定義と一致しています。

listのような可変オブジェクトと比較してください。

>>>

>>> my_list = [1, 2, 3]
>>> id(my_list)
140637819575368
>>> my_list.append(4)
>>> my_list
[1, 2, 3, 4]
>>> id(my_list)
140637819575368

このコードは、2種類のオブジェクトの大きな違いを示しています。 my_listには元々IDがあります。 4がリストに追加された後でも、my_listsameIDを持ちます。 これは、listタイプが変更可能であるためです。

リストが変更可能であることを示す別の方法は、割り当てを使用することです。

>>>

>>> my_list[0] = 0
>>> my_list
[0, 2, 3, 4]
>>> id(my_list)
140637819575368

このコードでは、my_listを変更し、最初の要素を0に設定します。 ただし、この割り当て後も同じIDを維持します。 可変オブジェクトと不変オブジェクトが邪魔にならないように、Python enlightenmentへの旅の次のステップは、Pythonの可変エコシステムを理解することです。

変数を理解する

Python変数は、CまたはC ++の変数と根本的に異なります。 実際、Pythonには変数さえありません。 Python has names, not variables.

これはつまらなく見えるかもしれませんが、ほとんどの場合そうです。 ほとんどの場合、Pythonの名前を変数として考えることは完全に受け入れられますが、違いを理解することは重要です。 これは、Pythonでポインターのトリッキーな主題をナビゲートしている場合に特に当てはまります。

違いを理解しやすくするために、Cで変数がどのように機能するか、それらが何を表すかを見て、それをPythonでの名前の機能と比較することができます。

Cの変数

変数xを定義する次のコードがあるとします。

int x = 2337;

この1行のコードには、実行時にいくつかの明確なステップがあります。

  1. 整数に十分なメモリを割り当てます

  2. そのメモリ位置に値2337を割り当てます

  3. xがその値を指していることを示します

メモリを簡略化して表示すると、次のようになります。

In-Memory representation of X (2337)

ここで、変数xの偽のメモリ位置が0x7f1で、値が2337であることがわかります。 プログラムの後半でxの値を変更したい場合は、次のようにすることができます。

x = 2338;

上記のコードは、変数xに新しい値(2338)を割り当て、それによってoverwritingが前の値になります。 これは、変数xmutableであることを意味します。 更新されたメモリレイアウトには、新しい値が表示されます。

New In-Memory representation of X (2338)

xの場所は変更されておらず、値自体のみが変更されていることに注意してください。 これは重要なポイントです。 これは、名前だけでなく、xis the memory locationを意味します。

この概念を考えるもう1つの方法は、所有権の観点です。 ある意味では、xがメモリ位置を所有します。 xは、最初は、整数値を格納できる1つの整数に正確に収まる空のボックスです。

xに値を割り当てると、xが所有するボックスに値が配置されます。 新しい変数(y)を導入したい場合は、次のコード行を追加できます。

int y = x;

このコードは、yというnewボックスを作成し、値をxからボックスにコピーします。 これで、メモリレイアウトは次のようになります。

In-Memory representation of X (2338) and Y (2338)

yの新しい場所0x7f5に注目してください。 xの値がyにコピーされた場合でも、変数yはメモリ内に新しいアドレスを所有しています。 したがって、xに影響を与えることなく、yの値を上書きできます。

y = 2339;

これで、メモリレイアウトは次のようになります。

Updated representation of Y (2339)

ここでも、yの値を変更しましたが、notはその場所です。 さらに、元のx変数にはまったく影響していません。 これは、Pythonの名前がどのように機能するかとはまったく対照的です。

Pythonの名前

Pythonには変数がありません。 名前があります。 はい、これは重要な点であり、変数という用語は好きなだけ使用できます。 変数と名前には違いがあることを知っておくことが重要です。

上記のCの例から同等のコードを取得して、Pythonで記述しましょう。

>>>

>>> x = 2337

Cの場合と同様に、上記のコードは実行中にいくつかの異なるステップに分割されます。

  1. PyObjectを作成します

  2. PyObjectのタイプコードを整数に設定します

  3. PyObjectの値を2337に設定します

  4. xという名前を作成します

  5. xを新しいPyObjectにポイントします

  6. PyObjectのrefcountを1増やします

Note:PyObjectはPythonのobjectと同じではありません。 CPythonに固有であり、すべてのPythonオブジェクトの基本構造を表します。

PyObjectはC構造体として定義されているため、typecodeまたはrefcountを直接呼び出すことができない理由がわからない場合は、構造体に直接アクセスできないためです。 sys.getrefcount()のようなメソッド呼び出しは、いくつかの内部を取得するのに役立ちます。

メモリ内では、次のようになります。

Python In-Memory representation of X (2337)

メモリレイアウトは、以前のCレイアウトとは大きく異なることがわかります。 値2337が存在するメモリブロックをxが所有する代わりに、新しく作成されたPythonオブジェクトが2337が存在するメモリを所有します。 Python名xは、C変数xがメモリ内の静的スロットを所有していたように、anyメモリアドレスを直接所有していません。

xに新しい値を割り当てようとする場合は、次のことを試すことができます。

>>>

>>> x = 2338

ここで起こっていることは、Cの同等のものとは異なりますが、Pythonの元のバインドとあまり違いはありません。

このコード:

  • 新しいPyObjectを作成します

  • PyObjectのタイプコードを整数に設定します

  • PyObjectの値を2338に設定します

  • xが新しいPyObjectを指す

  • 新しいPyObjectのrefcountを1増やします

  • 古いPyObjectのrefcountを1つ減らします

メモリー内では、次のようになります。

Python Name Pointing to new object (2338)

この図は、xがオブジェクトへの参照を指し、以前のようにメモリ空間を所有していないことを示すのに役立ちます。 また、x = 2338コマンドが割り当てではなく、名前xを参照にバインドしていることも示しています。

さらに、前のオブジェクト(2337値を保持していた)は現在、参照カウント0でメモリ内にあり、garbage collectorによってクリーンアップされます。

Cの例のように、新しい名前yをミックスに導入できます。

>>>

>>> y = x

メモリには、新しい名前が付けられますが、必ずしも新しいオブジェクトではありません。

X and Y Names pointing to 2338

これで、新しいPythonオブジェクトにnotが作成されたことがわかります。これは、同じオブジェクトを指す新しい名前だけです。 また、オブジェクトの参照カウントが1つ増えました。 オブジェクトの同一性をチェックして、それらが同じであることを確認できます。

>>>

>>> y is x
True

上記のコードは、xyが同じオブジェクトであることを示しています。 ただし、間違いはありません。yは依然として不変です。

たとえば、yで加算を実行できます。

>>>

>>> y += 1
>>> y is x
False

追加呼び出しの後、新しいPythonオブジェクトが返されます。 これで、メモリは次のようになります。

x name and y name different objects

新しいオブジェクトが作成され、yが新しいオブジェクトを指すようになりました。 興味深いことに、y2339に直接バインドした場合、これは同じ最終状態です。

>>>

>>> y = 2339

上記のステートメントは、追加と同じエンドメモリ状態になります。 要約すると、Pythonでは変数を割り当てません。 代わりに、名前を参照にバインドします。

Pythonのインターンオブジェクトに関する注意

Pythonオブジェクトがどのように作成され、名前がそれらのオブジェクトにバインドされるかを理解したので、機械にレンチを投げるときです。 そのレンチは、インターンされたオブジェクトの名前になります。

次のPythonコードがあるとします。

>>>

>>> x = 1000
>>> y = 1000
>>> x is y
True

上記のように、xyはどちらも、同じPythonオブジェクトを指す名前です。 ただし、値1000を保持するPythonオブジェクトは、常に同じメモリアドレスを持つことが保証されているわけではありません。 たとえば、2つの数値を足して1000を取得すると、メモリアドレスが異なります。

>>>

>>> x = 1000
>>> y = 499 + 501
>>> x is y
False

今回は、行x is yFalseを返します。 これが混乱する場合は、心配しないでください。 このコードが実行されるときに発生するステップは次のとおりです。

  1. Pythonオブジェクトを作成します(1000

  2. そのオブジェクトに名前xを割り当てます

  3. Pythonオブジェクトを作成します(499

  4. Pythonオブジェクトを作成します(501

  5. これら2つのオブジェクトを一緒に追加します

  6. 新しいPythonオブジェクトを作成します(1000

  7. そのオブジェクトに名前yを割り当てます

Technical Note:上記の手順は、このコードがREPL内で実行された場合にのみ発生します。 上記の例を使用してファイルに貼り付け、ファイルを実行すると、x is y行がTrueを返すことがわかります。

これは、コンパイラが賢いために発生します。 CPythonコンパイラは、peephole optimizationsと呼ばれる最適化を試みます。これは、可能な限り実行ステップを節約するのに役立ちます。 詳細については、CPython’s peephole optimizer source codeを確認してください。

これは無駄ではないですか? ええ、そうです。しかし、それはあなたがPythonのすべての大きな利点に支払う代価です。 これらの中間オブジェクトのクリーンアップについて心配する必要はありません。また、それらが存在することを知る必要さえありません! 喜びは、これらの操作が比較的高速であり、これまでこれらの詳細を知る必要がなかったことです。

コアPython開発者は、賢明にもこの無駄に気付き、いくつかの最適化を行うことにしました。 これらの最適化は、新規参入者にとって驚くべき動作をもたらします。

>>>

>>> x = 20
>>> y = 19 + 1
>>> x is y
True

この例では、結果がTrueであることを除いて、以前とほぼ同じコードが表示されます。 これは、インターンオブジェクトの結果です。 Pythonは、メモリ内のオブジェクトの特定のサブセットを事前に作成し、それらを日常的な使用のためにグローバル名前空間に保持します。

Pythonの実装に依存するオブジェクト。 CPython 3.7は以下をインターンします。

  1. -5256の間の整数

  2. ASCII文字、数字、またはアンダースコアのみを含む文字列

この背後にある理由は、これらの変数が多くのプログラムで使用される可能性が非常に高いことです。 これらのオブジェクトをインターンすることにより、Pythonは一貫して使用されるオブジェクトのメモリ割り当て呼び出しを防ぎます。

20文字未満で、ASCII文字、数字、またはアンダースコアを含む文字列はインターンされます。 この背後にある理由は、これらが何らかの種類のアイデンティティであると想定されていることです。

>>>

>>> s1 = "realpython"
>>> id(s1)
140696485006960
>>> s2 = "realpython"
>>> id(s2)
140696485006960
>>> s1 is s2
True

ここでは、s1s2の両方がメモリ内の同じアドレスを指していることがわかります。 ASCII以外の文字、数字、またはアンダースコアを使用すると、異なる結果が得られます。

>>>

>>> s1 = "Real Python!"
>>> s2 = "Real Python!"
>>> s1 is s2
False

この例には感嘆符(!)が含まれているため、これらの文字列はインターンされておらず、メモリ内の異なるオブジェクトです。

Bonus:これらのオブジェクトが本当に同じ内部オブジェクトを参照するようにしたい場合は、sys.intern()をチェックアウトすることをお勧めします。 この機能の使用例の1つは、ドキュメントで概説されています。

文字列のインターンは、ディクショナリルックアップで少しパフォーマンスを上げるのに役立ちます。ディクショナリのキーがインターンされ、ルックアップキーがインターンされる場合、キー比較(ハッシュ後)は、文字列比較の代わりにポインター比較によって実行できます。 (Source

インターンされたオブジェクトはしばしば混乱の原因になります。 疑問がある場合は、いつでもid()isを使用してオブジェクトの同等性を判断できることを覚えておいてください。

Pythonでポインタをシミュレートする

Pythonのポインターがネイティブに存在しないからといって、ポインターを使用してもメリットが得られないわけではありません。 実際、Pythonでポインターをシミュレートする方法は複数あります。 このセクションでは2つのことを学びます。

  1. 可変型をポインターとして使用する

  2. カスタムPythonオブジェクトの使用

さて、要点を説明しましょう。

可変型をポインターとして使用する

可変型についてはすでに学習しました。 これらのオブジェクトは可変なので、あたかもポインタであるかのように扱って、ポインタの動作をシミュレートできます。 次のcコードを複製したいとします。

void add_one(int *x) {
    *x += 1;
}

このコードは、整数(*x)へのポインターを受け取り、値を1つインクリメントします。 コードを実行する主な機能は次のとおりです。

#include 

int main(void) {
    int y = 2337;
    printf("y = %d\n", y);
    add_one(&y);
    printf("y = %d\n", y);
    return 0;
}

上記のコードでは、2337yに割り当て、現在の値を出力し、値を1つインクリメントしてから、変更された値を出力します。 このコードを実行すると、次の出力が得られます。

y = 2337
y = 2338

Pythonでこのタイプの動作を複製する1つの方法は、可変タイプを使用することです。 リストを使用して最初の要素を変更することを検討してください。

>>>

>>> def add_one(x):
...     x[0] += 1
...
>>> y = [2337]
>>> add_one(y)
>>> y[0]
2338

ここで、add_one(x)は最初の要素にアクセスし、その値を1つインクリメントします。 listを使用すると、最終結果で値が変更されたように見えます。 Pythonのポインターは存在しますか? うーん、ダメ。 これが可能なのは、listが可変型であるためです。 tupleを使用しようとすると、エラーが発生します。

>>>

>>> z = (2337,)
>>> add_one(z)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in add_one
TypeError: 'tuple' object does not support item assignment

上記のコードは、tupleが不変であることを示しています。 したがって、アイテムの割り当てはサポートされていません。 listだけが可変タイプではありません。 Pythonでポインタを模倣するもう1つの一般的なアプローチは、dictを使用することです。

興味深いイベントが発生するたびに追跡したいアプリケーションがあるとします。 これを実現する1つの方法は、dictを作成し、アイテムの1つをカウンターとして使用することです。

>>>

>>> counters = {"func_calls": 0}
>>> def bar():
...     counters["func_calls"] += 1
...
>>> def foo():
...     counters["func_calls"] += 1
...     bar()
...
>>> foo()
>>> counters["func_calls"]
2

この例では、countersディクショナリを使用して、関数呼び出しの数を追跡しています。 foo()を呼び出すと、期待どおりにカウンターが2に増加しました。 dictは可変であるためです。

これはsimulatesのポインターの動作にすぎず、CまたはCの真のポインターに直接マップされないことに注意してください。 つまり、これらの操作は、CまたはCの場合よりもコストがかかります。

Pythonオブジェクトの使用

dictオプションは、Pythonでポインターをエミュレートするための優れた方法ですが、使用したキー名を覚えるのが面倒になる場合があります。 これは、アプリケーションのさまざまな部分で辞書を使用している場合に特に当てはまります。 これは、カスタムPythonクラスが本当に役立つ場所です。

最後の例を基に、アプリケーションのメトリックを追跡すると仮定します。 クラスの作成は、厄介な詳細を抽象化するための優れた方法です。

class Metrics(object):
    def __init__(self):
        self._metrics = {
            "func_calls": 0,
            "cat_pictures_served": 0,
        }

このコードは、Metricsクラスを定義します。 このクラスは、_metricsメンバー変数にある実際のデータを保持するためにdictを引き続き使用します。 これにより、必要な可変性が得られます。 ここで、これらの値にアクセスできる必要があります。 これを行う良い方法の1つは、プロパティを使用することです。

class Metrics(object):
    # ...

    @property
    def func_calls(self):
        return self._metrics["func_calls"]

    @property
    def cat_pictures_served(self):
        return self._metrics["cat_pictures_served"]

このコードは@propertyを利用します。 デコレータに慣れていない場合は、このPrimer on Python Decoratorsを確認できます。 ここでの@propertyデコレータを使用すると、func_callscat_pictures_servedに属性であるかのようにアクセスできます。

>>>

>>> metrics = Metrics()
>>> metrics.func_calls
0
>>> metrics.cat_pictures_served
0

これらの名前に属性としてアクセスできるという事実は、これらの値がdictにあるという事実を抽象化したことを意味します。 また、属性の名前をより明確にします。 もちろん、これらの値を増分できる必要があります。

class Metrics(object):
    # ...

    def inc_func_calls(self):
        self._metrics["func_calls"] += 1

    def inc_cat_pics(self):
        self._metrics["cat_pictures_served"] += 1

次の2つの新しいメソッドを導入しました。

  1. inc_func_calls()

  2. inc_cat_pics()

これらのメソッドは、メトリックdictの値を変更します。 これで、ポインタを変更するかのように変更するクラスができました。

>>>

>>> metrics = Metrics()
>>> metrics.inc_func_calls()
>>> metrics.inc_func_calls()
>>> metrics.func_calls
2

ここでは、アプリケーションのさまざまな場所でfunc_callsにアクセスし、inc_func_calls()を呼び出して、Pythonでポインターをシミュレートできます。 これは、アプリケーションのさまざまな部分で頻繁に使用および更新する必要があるメトリックのようなものがある場合に役立ちます。

Note:特にこのクラスでは、@property.setterを使用する代わりにinc_func_calls()inc_cat_pics()を明示的にすると、ユーザーがこれらの値を任意のintまたは無効な値に設定できなくなります。 dictのように。

Metricsクラスの完全なソースは次のとおりです。

class Metrics(object):
    def __init__(self):
        self._metrics = {
            "func_calls": 0,
            "cat_pictures_served": 0,
        }

    @property
    def func_calls(self):
        return self._metrics["func_calls"]

    @property
    def cat_pictures_served(self):
        return self._metrics["cat_pictures_served"]

    def inc_func_calls(self):
        self._metrics["func_calls"] += 1

    def inc_cat_pics(self):
        self._metrics["cat_pictures_served"] += 1

ctypesの実際のポインタ

さて、多分Python、特にCPythonにはポインターがあります。 組み込みのctypesモジュールを使用すると、Pythonで実際のCスタイルのポインターを作成できます。 ctypesに慣れていない場合は、Extending Python With C Libraries and the “ctypes” Moduleを確認できます。

これを使用する本当の理由は、ポインターを必要とするCライブラリーに対して関数呼び出しを行う必要がある場合です。 前のadd_one()C関数に戻りましょう。

void add_one(int *x) {
    *x += 1;
}

ここでも、このコードはxの値を1つインクリメントしています。 これを使用するには、まず共有オブジェクトにコンパイルします。 上記のファイルがadd.cに格納されているとすると、gccを使用してこれを実行できます。

$ gcc -c -Wall -Werror -fpic add.c
$ gcc -shared -o libadd1.so add.o

最初のコマンドは、Cソースファイルをadd.oというオブジェクトにコンパイルします。 2番目のコマンドは、そのリンクされていないオブジェクトファイルを取得し、libadd1.soという共有オブジェクトを生成します。

libadd1.soは現在のディレクトリにあるはずです。 ctypesを使用してPythonにロードできます。

>>>

>>> import ctypes
>>> add_lib = ctypes.CDLL("./libadd1.so")
>>> add_lib.add_one
<_FuncPtr object at 0x7f9f3b8852a0>

ctypes.CDLLコードは、libadd1共有オブジェクトを表すオブジェクトを返します。 この共有オブジェクトでadd_one()を定義したので、他のPythonオブジェクトであるかのようにアクセスできます。 ただし、関数を呼び出す前に、関数のシグネチャを指定する必要があります。 これにより、Pythonは関数に正しい型を確実に渡すことができます。

この場合、関数シグネチャは整数へのポインターです。 ctypesを使用すると、次のコードを使用してこれを指定できます。

>>>

>>> add_one = add_lib.add_one
>>> add_one.argtypes = [ctypes.POINTER(ctypes.c_int)]

このコードでは、Cが期待するものと一致するように関数シグネチャを設定しています。 ここで、間違った型でこのコードを呼び出そうとすると、未定義の動作の代わりに素晴らしい警告が表示されます:

>>>

>>> add_one(1)
Traceback (most recent call last):
  File "", line 1, in 
ctypes.ArgumentError: argument 1: : \
expected LP_c_int instance instead of int

Pythonはエラーをスローし、add_one()が単なる整数ではなくポインタを必要としていることを説明します。 幸い、ctypesには、これらの関数へのポインターを渡す方法があります。 最初に、Cスタイルの整数を宣言します。

>>>

>>> x = ctypes.c_int()
>>> x
c_int(0)

上記のコードは、値が0のCスタイルの整数xを作成します。 ctypesは、参照によって変数を渡すことができる便利なbyref()を提供します。

Note:用語by referenceは、変数by valueを渡すこととは反対です。

参照渡しの場合、元の変数への参照を渡すため、変更は元の変数に反映されます。 値を渡すと元の変数のコピーが生成され、変更は元の変数に反映されません。

これを使用して、add_one()を呼び出すことができます。

>>>

>>> add_one(ctypes.byref(x))
998793640
>>> x
c_int(1)

いいね! 整数が1増加しました。 おめでとうございます。Pythonで実際のポインターを使用できました。

結論

これで、Pythonオブジェクトとポインターの交差点についての理解が深まりました。 名前と変数の区別のいくつかは重要であるように見えますが、これらの重要な用語を根本的に理解することで、Pythonが変数を処理する方法の理解が深まります。

また、Pythonでポインターをシミュレートする優れた方法をいくつか学びました。

  • 低オーバーヘッドポインターとして可変オブジェクトを利用する

  • 使いやすいカスタムPythonオブジェクトの作成

  • ctypesモジュールで実際のポインタのロックを解除する

これらのメソッドを使用すると、Pythonが提供するメモリの安全性を犠牲にすることなく、Pythonでポインターをシミュレートできます。

読んでくれてありがとう。 まだ質問がある場合は、コメントセクションまたはTwitterでお気軽にお問い合わせください。