Указатели в Python: в чем смысл?

Оглавление

Смотреть сейчас К этому учебнику прилагается видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным учебником, чтобы углубить свое понимание: Пункты и объекты в Python

Если вы когда-нибудь работали с языками нижнего уровня, такими как C или C++, то вы наверняка слышали об указателях. Указатели позволяют добиться большой эффективности в некоторых частях вашего кода. Они также вызывают путаницу у новичков и могут привести к различным ошибкам в управлении памятью даже у экспертов. Так где же они находятся в Python и как можно имитировать указатели в Python?

Указатели широко используются в языках C и C++. По сути, это переменные, которые хранят в памяти адрес другой переменной. Чтобы получить более подробную информацию об указателях, вы можете ознакомиться с этим обзором по указателям в C.

В этой статье вы лучше поймете объектную модель Python и узнаете, почему указатели в Python на самом деле не существуют. Для случаев, когда вам нужно имитировать поведение указателей, вы узнаете, как имитировать указатели в Python без кошмара управления памятью.

В этой статье вы узнаете:

  • Узнайте, почему указатели в Python не существуют
  • Исследуйте разницу между переменными C и именами Python
  • Симулируйте указатели в Python
  • Экспериментируйте с реальными указателями, используя ctypes

Примечание: В этой статье под "Python" будет подразумеваться эталонная реализация Python на C, известная как CPython. Поскольку в статье обсуждаются некоторые внутренние аспекты языка, эти заметки справедливы для CPython 3.7, но могут быть не справедливы для будущих или прошлых итераций языка.

Бесплатная загрузка: Получите пример главы из книги Python Tricks: The Book, которая покажет вам лучшие практики Python на простых примерах, которые вы сможете мгновенно применить, чтобы писать более красивый + Pythonic код.

Почему в Python нет указателей?

Правда в том, что я не знаю. Могут ли указатели в Python существовать нативно? Возможно, но указатели, кажется, противоречат дзену Python. Указатели поощряют неявные изменения, а не явные. Часто они сложны, а не просты, особенно для новичков. Хуже того, в них можно найти способ прострелить себе ногу или сделать что-то действительно опасное, например, прочитать из участка памяти, который вы не должны были читать

Python имеет тенденцию пытаться абстрагироваться от деталей реализации, таких как адреса памяти, от своих пользователей. Python часто фокусируется на удобстве использования, а не на скорости. В результате указатели в Python не имеют смысла. Однако не стоит бояться, Python по умолчанию предоставляет вам некоторые преимущества использования указателей.

Понимание указателей в Python требует небольшого экскурса в детали реализации Python. В частности, вам нужно будет понять:

  1. Взаимозаменяемые и взаимозаменяемые объекты
  2. Пайтон переменные/имена

Держите свои адреса в памяти, и давайте начнем.

Объекты в Python

В Python все является объектом. Для доказательства вы можете открыть REPL и изучить использование isinstance():

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

Этот код показывает, что все в Python действительно является объектом. Каждый объект содержит как минимум три части данных:

  • Количество ссылок
  • Тип
  • Значение

Счетчик ссылок предназначен для управления памятью. Для более подробного рассмотрения внутренних аспектов управления памятью в Python вы можете прочитать Memory Management in Python.

Тип используется на уровне CPython для обеспечения безопасности типов во время выполнения программы. Наконец, есть значение, которое является фактическим значением, связанным с объектом.

Однако не все объекты одинаковы. Есть еще одно важное различие, которое вам необходимо понять: неизменяемые и изменяемые объекты. Понимание разницы между типами объектов действительно помогает прояснить первый слой лука, которым являются указатели в Python.

Неизменяемые и изменяемые объекты

В Python существует два типа объектов:

  1. Взаимозаменяемые объекты не могут быть изменены.
  2. Взаимозаменяемые объекты могут быть изменены.

Понимание этой разницы - первый ключ к навигации по ландшафту указателей в Python. Вот описание распространенных типов и того, являются ли они изменяемыми или неизменяемыми:

Тип Неизменяемый?
int Да
float Да
bool Да
complex Да
tuple Да
frozenset Да
str Да
list Нет
set Нет
dict Нет

Как видите, многие часто используемые примитивные типы являются неизменяемыми. Вы можете доказать это сами, написав немного на Python. Вам понадобится несколько инструментов из стандартной библиотеки Python:

  1. id() возвращает адрес памяти объекта.
  2. is возвращает True тогда и только тогда, когда два объекта имеют одинаковый адрес памяти.

Опять же, вы можете использовать их в среде REPL:

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

В приведенном выше коде вы присвоили значение 5 объекту x. Если бы вы попытались изменить это значение с помощью сложения, то получили бы новый объект:

>>> 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 в итоге после операции += имеет разные адреса памяти.

Бонус: Оператор += переводит в различные вызовы методов.

Для некоторых объектов, таких как list, += будет преобразовано в __iadd__() (in-place add). Это изменит self и вернет тот же идентификатор. Однако str и int не имеют таких методов и приводят к вызову __add__() вместо __iadd__().

Для получения более подробной информации ознакомьтесь с документацией по модели данных в Python .

Попытка напрямую мутировать строку s приводит к ошибке:

>>> s[0] = "R"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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

Этот код показывает основное различие в двух типах объектов. Изначально у my_list есть идентификатор. Даже после добавления 4 в список, my_list имеет тот же самый идентификатор. Это происходит потому, что тип list является мутабельным.

Другой способ продемонстрировать, что список является изменяемым, - это присваивание:

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

В этом коде вы мутируете my_list и устанавливаете его первый элемент в 0. Однако даже после этого присвоения он сохраняет тот же идентификатор. После того как вы разобрались с мутабельными и неизменяемыми объектами, следующим шагом на пути к просвещению в Python станет понимание экосистемы переменных Python.

Понимание переменных

Переменные в Python принципиально отличаются от переменных в C или C++. На самом деле, в Python даже нет переменных. В Python есть имена, а не переменные.

Это может показаться педантичным, и по большей части так оно и есть. В большинстве случаев вполне допустимо воспринимать имена в Python как переменные, но понимание разницы очень важно. Это особенно актуально, когда вы изучаете сложную тему указателей в Python.

Чтобы наглядно показать разницу, вы можете посмотреть, как работают переменные в C, что они представляют, а затем сравнить это с тем, как работают имена в Python.

Переменные в C

Допустим, у вас есть следующий код, определяющий переменную x:

int x = 2337;

При выполнении этой строки кода происходит несколько отдельных шагов:

  1. Выделите достаточно памяти для целого числа
  2. Присвойте значение 2337 этому участку памяти
  3. Укажите, что x указывает на это значение

В упрощенном виде память может выглядеть так:

In-Memory representation of X (2337)

Здесь видно, что переменная x имеет фальшивую ячейку памяти 0x7f1 и значение 2337. Если позже в программе вы захотите изменить значение x, вы можете сделать следующее:

x = 2338;

Приведенный выше код присваивает новое значение (2338) переменной x, тем самым перезаписывая предыдущее значение. Это означает, что переменная x является изменяемой. Обновленная схема памяти показывает новое значение:

New In-Memory representation of X (2338)

Обратите внимание, что расположение x не изменилось, изменилось только само значение. Это очень важный момент. Он означает, что x - это место в памяти , а не просто его имя.

Другой способ представить эту концепцию в терминах владения. В одном смысле x владеет ячейкой памяти. x поначалу является пустым ящиком, в который может поместиться ровно одно целое число и в котором могут храниться целочисленные значения.

Когда вы присваиваете значение x, вы помещаете его в поле, которое принадлежит x. Если бы вы хотели ввести новую переменную (y), вы могли бы добавить эту строку кода:

int y = x;

Этот код создает новый бокс с именем y и копирует в него значение из x. Теперь схема памяти будет выглядеть так:

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

Обратите внимание на новое расположение 0x7f5 в y. Хотя значение x было скопировано в y, переменной y принадлежит некоторый новый адрес в памяти. Поэтому вы можете перезаписать значение y, не затрагивая x:

y = 2339;

Теперь расположение памяти будет выглядеть так:

Updated representation of Y (2339)

Опять же, вы изменили значение в y, но не его местоположение. Кроме того, вы никак не повлияли на исходную переменную x. Это резко контрастирует с тем, как работают имена в Python.

Имена в Python

В Python нет переменных. В нем есть имена. Да, это педантичный момент, и вы можете использовать термин "переменные" сколько угодно. Важно знать, что существует разница между переменными и именами.

Давайте возьмем эквивалентный код из приведенного выше примера на C и напишем его на Python:

>>> x = 2337

Как и в языке C, приведенный выше код разбивается на несколько отдельных шагов во время выполнения:

  1. Создайте PyObject
  2. Установите код типа на integer для PyObject
  3. Установите значение 2337 для PyObject
  4. Создайте имя x
  5. Укажите x на новое PyObject
  6. Увеличьте счетчик ссылок PyObject на 1

Примечание: PyObject - это не то же самое, что object в Python. Он специфичен для CPython и представляет собой базовую структуру для всех объектов Python.

PyObject определяется как структура C, поэтому если вы задаетесь вопросом, почему вы не можете вызвать typecode или refcount напрямую, то это потому, что у вас нет доступа к структурам напрямую. Вызовы методов, таких как sys.getrefcount(), могут помочь получить некоторые внутренние сведения.

В памяти это может выглядеть примерно так:

Python In-Memory representation of X (2337)

Вы видите, что расположение памяти значительно отличается от предыдущего расположения на языке C. Вместо того чтобы x владеть блоком памяти, в котором находится значение 2337, вновь созданный объект Python владеет памятью, в которой находится 2337. Имя Python x не владеет напрямую любым адресом памяти, как переменная C x владела статическим слотом в памяти.

Если бы вы попытались присвоить новое значение x, вы могли бы попробовать следующее:

>>> x = 2338

То, что происходит здесь, отличается от эквивалента на C, но не слишком отличается от оригинальной привязки в Python.

Этот код:

  • Создает новый PyObject
  • Устанавливает код типа на integer для PyObject
  • Устанавливает значение 2338 для PyObject
  • Указывает x на новый PyObject
  • Увеличивает счетчик ссылок нового PyObject на 1
  • Уменьшает счетчик ссылок старого PyObject на 1

Теперь в памяти это будет выглядеть примерно так:

Python Name Pointing to new object (2338)

Эта диаграмма помогает проиллюстрировать, что x указывает на ссылку на объект и не владеет пространством памяти, как раньше. Она также показывает, что команда x = 2338 не является присваиванием, а скорее привязывает имя x к ссылке.

Кроме того, предыдущий объект (который содержал значение 2337) теперь находится в памяти с количеством ссылок 0 и будет очищен сборщиком мусора.

Вы можете ввести новое имя, y, как в примере на C:

>>> y = x

В памяти у вас будет новое имя, но не обязательно новый объект:

X and Y Names pointing to 2338

Теперь вы видите, что новый объект Python не был создан, просто было создано новое имя, указывающее на тот же объект. Кроме того, счетчик ссылок объекта увеличился на единицу. Можно проверить равенство идентичности объектов, чтобы убедиться, что они одинаковые:

>>> y is x
True

Приведенный выше код показывает, что x и y являются одним и тем же объектом. Однако не заблуждайтесь: y по-прежнему неизменяем.

Например, вы можете выполнить сложение на y:

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

После вызова добавления вы получаете новый объект Python. Теперь память выглядит следующим образом:

x name and y name different objects

Был создан новый объект, и y теперь указывает на новый объект. Интересно, что это то же самое конечное состояние, если бы вы привязали y к 2339 напрямую:

>>> y = 2339

Приведенное выше утверждение приводит к тому же состоянию конечной памяти, что и дополнение. Напомним, что в Python вы не присваиваете переменные. Вместо этого вы связываете имена со ссылками.

Заметка о Intern-объектах в Python

Теперь, когда вы поняли, как создаются объекты Python и как имена привязываются к этим объектам, пришло время бросить гаечный ключ в этот механизм. Этот ключ называется interned objects.

Предположим, что у вас есть следующий код на языке Python:

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

Как указано выше, x и y - это имена, которые указывают на один и тот же объект Python. Но не всегда гарантируется, что объект Python, который содержит значение 1000, имеет тот же адрес памяти. Например, если вы сложите два числа вместе, чтобы получить 1000, то в итоге получите другой адрес памяти:

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

На этот раз строка x is y возвращает False. Если это сбивает с толку, то не волнуйтесь. Вот шаги, которые происходят при выполнении этого кода:

  1. Создайте объект Python(1000)
  2. Присвойте имя x этому объекту
  3. Создать объект Python (499)
  4. Создайте объект Python (501)
  5. Соедините эти два объекта вместе
  6. Создайте новый объект Python (1000)
  7. Присвойте имя y этому объекту

Техническое примечание: Описанные выше действия происходят только при выполнении этого кода внутри REPL. Если бы вы взяли приведенный выше пример, вставили его в файл и запустили его, то обнаружили бы, что строка x is y возвращает True.

Это происходит потому, что компиляторы умны. Компилятор CPython пытается сделать оптимизации, называемые peephole optimizations, которые помогают сэкономить шаги выполнения, когда это возможно. Для получения подробной информации вы можете ознакомиться с исходным кодом оптимизатора peephole в CPython.

Разве это не расточительство? Да, это так, но это цена, которую вы платите за все замечательные преимущества Python. Вам никогда не придется беспокоиться об очистке этих промежуточных объектов или даже знать, что они существуют! Радость заключается в том, что эти операции выполняются относительно быстро, и до сих пор вам не нужно было знать ни одной из этих деталей.

Разработчики ядра Python в своей мудрости также заметили это расточительство и решили сделать несколько оптимизаций. Эти оптимизации приводят к поведению, которое может быть неожиданным для новичков:

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

В этом примере вы видите почти тот же код, что и раньше, только на этот раз результат True. Это результат интернированных объектов. Python предварительно создает определенное подмножество объектов в памяти и хранит их в глобальном пространстве имен для повседневного использования.

Какие объекты зависят от реализации Python. В CPython 3.7 интерпритируются следующие:

  1. Целочисленные числа между -5 и 256
  2. Строки, содержащие только буквы ASCII, цифры или символы подчеркивания

Причина этого в том, что эти переменные с большой вероятностью будут использоваться во многих программах. Интернируя эти объекты, Python предотвращает вызовы выделения памяти для постоянно используемых объектов.

Строки, содержащие менее 20 символов и содержащие буквы ASCII, цифры или символы подчеркивания, будут интернированы. Это объясняется тем, что предполагается, что они являются некими тождествами:

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

Здесь видно, что s1 и s2 указывают на один и тот же адрес в памяти. Если бы вы ввели букву, цифру или знак подчеркивания, отличные от ASCII, то получили бы другой результат:

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

Поскольку в этом примере присутствует восклицательный знак (!), эти строки не интернированы и являются разными объектами в памяти.

Бонус: Если вы действительно хотите, чтобы эти объекты ссылались на один и тот же внутренний объект, то вам стоит обратить внимание на sys.intern(). Один из вариантов использования этой функции описан в документации:

Интернирование строк полезно для увеличения производительности при поиске в словаре - если ключи в словаре интернированы, а ключ поиска интернирован, то сравнение ключей (после хэширования) может быть выполнено сравнением указателей вместо сравнения строк. (Source)

Несовместимые объекты часто становятся источником путаницы. Просто помните, если вы сомневаетесь, что всегда можно использовать id() и is для определения равенства объектов.

Симулирование указателей в Python

То, что указатели в Python не существуют изначально, не означает, что вы не можете получить преимущества использования указателей. На самом деле, в Python существует множество способов имитации указателей. В этом разделе вы узнаете о двух из них:

  1. Использование мутабельных типов в качестве указателей
  2. Использование пользовательских объектов Python

Ладно, перейдем к делу.

Использование изменяемых типов в качестве указателей

Вы уже узнали о мутабельных типах. Поскольку эти объекты являются мутабельными, вы можете обращаться с ними, как с указателями, чтобы имитировать поведение указателей. Предположим, вы хотите воспроизвести следующий код на языке c:

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

Этот код принимает указатель на целое число (*x) и затем увеличивает его значение на единицу. Вот главная функция для отработки этого кода:

#include <stdio.h>

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

В приведенном выше коде вы присваиваете 2337 значение y, выводите текущее значение, увеличиваете его на единицу, а затем выводите измененное значение. Результатом выполнения этого кода будет следующее:

y = 2337
y = 2338

Один из способов повторить такое поведение в Python - использовать мутабельный тип. Рассмотрим использование списка и модификацию первого элемента:

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

Здесь add_one(x) получает доступ к первому элементу и увеличивает его значение на единицу. Использование list означает, что в конечном результате значение будет изменено. Значит, указатели в Python все-таки существуют? Ну, нет. Это возможно только потому, что list является мутабельным типом. Если бы вы попытались использовать tuple, вы бы получили ошибку:

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

Приведенный выше код демонстрирует, что tuple является неизменяемым. Поэтому он не поддерживает присваивание элементов. list - не единственный мутабельный тип. Другим распространенным подходом к имитации указателей в Python является использование dict.

Допустим, у вас есть приложение, в котором вы хотите отслеживать каждый раз, когда происходит интересное событие. Одним из способов добиться этого было бы создание dict и использование одного из элементов в качестве счетчика:

>>> 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 является мутабельным.

Имейте в виду, что это только имитирует поведение указателей и не сопоставляется напрямую с истинными указателями в C или C++. То есть эти операции более дорогостоящие, чем в C или C++.

Использование объектов Python

Опция dict - отличный способ эмулировать указатели в Python, но иногда становится утомительно запоминать имя ключа, который вы использовали. Это особенно актуально, если вы используете словарь в разных частях вашего приложения. Именно здесь может помочь пользовательский класс Python.

В развитие предыдущего примера предположим, что вы хотите отслеживать метрики в своем приложении. Создание класса - отличный способ абстрагироваться от надоедливых деталей:

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

Этот код определяет класс Metrics. Этот класс по-прежнему использует dict для хранения фактических данных, которые находятся в переменной-члене _metrics. Это обеспечит вам необходимую мутабельность. Теперь вам просто нужно иметь доступ к этим значениям. Один из хороших способов сделать это - использовать свойства:

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. Если вы не знакомы с декораторами, вы можете ознакомиться с этим Праймером по декораторам Python. Декоратор @property здесь позволяет вам получить доступ к func_calls и cat_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

Вы ввели два новых метода:

  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. Это полезно, когда у вас есть что-то вроде метрик, которые должны часто использоваться и обновляться в различных частях ваших приложений.

Примечание: В этом классе, в частности, явное использование inc_func_calls() и inc_cat_pics() вместо использования @property.setter предотвращает установку этих значений в произвольное 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, вы можете создавать настоящие указатели в стиле C в Python. Если вы не знакомы с ctypes, то вы можете взглянуть на Extending Python With C Libraries and the "ctypes" Module.

Реальная причина, по которой вы могли бы использовать это, заключается в том, что вам нужно сделать вызов функции в библиотеке C, которая требует указатель. Давайте вернемся к предыдущей C-функции add_one():

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

Здесь этот код снова увеличивает значение x на единицу. Чтобы использовать этот код, сначала скомпилируйте его в общий объект. Если предположить, что вышеупомянутый файл хранится в add.c, вы можете сделать это с помощью gcc:

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

Первая команда компилирует исходный файл на языке C в объект под названием add.o. Вторая команда берет этот несвязанный объектный файл и создает общий объект libadd1.so.

libadd1.so должен находиться в вашем текущем каталоге. Вы можете загрузить его в Python, используя ctypes:

>>> 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 "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 1: <class 'TypeError'>: \
expected LP_c_int instance instead of int

Python выдает ошибку, объясняя, что add_one() хочет получить указатель, а не просто целое число. К счастью, ctypes имеет возможность передавать указатели в эти функции. Сначала объявите целое число в стиле C:

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

Приведенный выше код создает целое число x в стиле C со значением 0. ctypes предоставляет удобную функцию byref(), позволяющую передавать переменную по ссылке.

Примечание: Термин by reference противоположен передаче переменной by value.

При передаче по ссылке вы передаете ссылку на исходную переменную, и поэтому изменения будут отражены в исходной переменной. При передаче по значению создается копия исходной переменной, и изменения не отражаются в оригинале.

Для получения дополнительной информации о передаче по ссылке в Python, ознакомьтесь с Pass by Reference in Python: Background and Best Practices.

Вы можете использовать это для вызова add_one():

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

Отлично! Ваше целое число увеличилось на единицу. Поздравляем, вы успешно использовали вещественные указатели в Python.

Заключение

Теперь вы лучше понимаете, как пересекаются объекты Python и указатели. Даже если некоторые различия между именами и переменными кажутся педантичными, фундаментальное понимание этих ключевых терминов расширяет ваше представление о том, как Python работает с переменными.

Вы также узнали несколько отличных способов моделирования указателей в Python:

  • Использование мутабельных объектов в качестве малозатратных указателей
  • Создание пользовательских объектов Python для удобства использования
  • Разблокировка реальных указателей с помощью модуля ctypes

Эти методы позволяют моделировать указатели в Python, не жертвуя безопасностью памяти, которую обеспечивает Python.

Спасибо, что прочитали. Если у вас остались вопросы, обращайтесь к нам в разделе комментариев или в Twitter.

Back to Top