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

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

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

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

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

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

  • Узнайте, почему не существует указателей в Python

  • Исследуйте разницу между переменными C и именами Python

  • Имитация указателей в Python

  • Поэкспериментируйте с реальными указателями, используяctypes

Note: В этой статье «Python» будет относиться к эталонной реализации Python на C, также известной как 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 действительно является объектом. Каждый объект содержит как минимум три фрагмента данных:

  • Счетчик ссылок

  • Type

  • Значение

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

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

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

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

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

  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 возвращаетTrue тогда и только тогда, когда два объекта имеют одинаковый адрес памяти.

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

>>>

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

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

>>>

>>> 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 и вернет тот же идентификатор. Однакоstr иint не имеют этих методов и приводят к вызовам__add__() вместо__iadd__().

Для получения более подробной информации ознакомьтесь с Pythonhttps://docs.python.org/3/reference/datamodel.html#object.iadd [документы модели данных.]

Попытка напрямую изменить строку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

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

Еще один способ продемонстрировать, что список изменчив, это с помощью присваивания:

>>>

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

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

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

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

Это может показаться педантичным, и по большей части это так. В большинстве случаев вполне приемлемо рассматривать имена 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, тем самымoverwriting предыдущее значение. Это означает, что переменнаяx равнаmutable. Обновленный макет памяти показывает новое значение:

New In-Memory representation of X (2338)

Обратите внимание, что расположениеx не изменилось, только само значение. Это важный момент. Это означает, чтоxis the memory location, а не просто название для него.

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

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

int y = x;

Этот код создает блокnew с именем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, ноnot его местоположение. Кроме того, вы вообще не повлияли на исходную переменнуюx. Это резко контрастирует с тем, как работают имена Python.

Имена в Python

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

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

>>>

>>> x = 2337

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

  1. СоздайтеPyObject

  2. Установите код типа на целое число дляPyObject

  3. Установите значение2337 дляPyObject

  4. Создайте имя под названиемx

  5. Укажитеx на новыйPyObject

  6. Увеличьте количество ссылокPyObject на 1

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

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

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

Python In-Memory representation of X (2337)

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

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

>>>

>>> x = 2338

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

Этот код:

  • Создает новыйPyObject

  • Устанавливает код типа в целое число дляPyObject

  • Устанавливает значение2338 дляPyObject

  • Указываетx на новыйPyObject

  • Увеличивает счетчик новыхPyObject на 1

  • Уменьшает пересчет старогоPyObject на 1

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

Python Name Pointing to new object (2338)

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

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

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

>>>

>>> y = x

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

X and Y Names pointing to 2338

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

>>>

>>> 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 вы не назначаете переменные. Вместо этого вы привязываете имена к ссылкам.

Замечание о внутренних объектах в Python

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

Предположим, у вас есть следующий код 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

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. Целые числа от-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

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

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

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

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

Имитация указателей в Python

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

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

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

Хорошо, давайте перейдем к делу.

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

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

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

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

#include 

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

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

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 "", line 1, in 
  File "", 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 изменчив.

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

Note: В этом классе, в частности, создание явных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 "", 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)

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

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

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

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

>>>

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

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

Заключение

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

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

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

  • Создание пользовательских объектов Python для простоты использования

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

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

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