Если вы когда-нибудь работали с такими низкоуровневыми языками, как С или С++, то наверняка слышали про указатели. Они позволяют сильно повышать эффективность разных кусков кода. Но также они могут запутывать новичков — и даже опытных разработчиков — и приводить к багам управления памятью. А есть ли указатели в Python, можно их как-то эмулировать?
Указатели широко применяются в С и С++. По сути, это переменные, которые содержат адреса памяти, по которым находятся другие переменные. Чтобы освежить знания об указателях, почитайте этот обзор.
Благодаря этой статье вы лучше поймёте модель объектов в Python и узнаете, почему в этом языке на самом деле не существуют указатели. На случай, если вам понадобится сымитировать поведение указателей, вы научитесь эмулировать их без сопутствующего кошмара управления памятью.
С помощью этой статьи вы:
- Узнаете, почему в Python нет указателей.
- Узнаете разницу между переменными C и именами в Python.
- Научитесь эмулировать указатели в Python.
- С помощью
ctypes
поэкспериментируете с настоящими указателями.
Примечание: Здесь термин «Python» применяется к реализации Python на C, которая известна под названием CPython. Все обсуждения устройства языка справедливы для CPython 3.7, но могут не соответствовать последующим итерациям.
Почему в Python нет указателей?
Не знаю. Могут ли указатели существовать в Python нативно? Вероятно, но судя по всему, указатели противоречат понятию Zen of Python, потому что провоцируют неявные изменения вместо явных. Нередко указатели довольно сложны, особенно для новичков. Более того, они подталкивают вас к неудачным решениям или к тому, чтобы сделать что-нибудь действительно опасное, вроде чтения из области памяти, откуда вам не следовало считывать.
Python старается абстрагировать от пользователя подробности реализации, например адреса памяти. Часто в этом языке упор делается на удобство использования, а не на скорость. Поэтому указатели в Python не имеют особого смысла. Но не переживайте, по умолчанию язык предоставляет вам некоторые преимущества использования указателей.
Чтобы разобраться с указателями в Python, давайте кратко пройдёмся по особенностями реализации языка. В частности, вам нужно понять:
- Что такое изменяемые и неизменяемые объекты.
- Как устроены переменные/имена в 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 — на самом деле объекты. Каждый объект содержит как минимум три вида данных:
- Счётчик ссылок.
- Тип.
- Значение.
Счётчик ссылок используется для управления памятью. Подробно об этом управлении написано в Memory Management in Python. Тип используется на уровне CPython для обеспечения типобезопасности в ходе исполнения (runtime). А значение — это фактическое значение, ассоциированное с объектом.
Но не все объекты одинаковы. Есть одно важное отличие: объекты бывают изменяемые и неизменяемые. Понимание этого различия между типами объектов поможет вам лучше осознать первый слой луковицы, которая называется «указатели в Python».
Изменяемые и неизменяемые объекты
В Python есть два типа объектов:
- Неизменяемые объекты (не могут быть изменены);
- Изменяемые объекты (могут быть изменены).
Осознание этой разницы — первый ключ к путешествию по миру указателей в Python. Вот характеристика неизменяемости некоторых популярных типов:
Тип |
Неизменяемый? |
---|---|
int |
Да |
float |
Да |
bool |
Да |
complex |
Да |
tuple |
Да |
frozenset |
Да |
str |
Да |
list |
Нет |
set |
Нет |
dict |
Нет |
Как видите, многие из часто используемых примитивных типов являются неизменяемыми. Проверить это можно, написав кое-какой код на Python. Вам понадобится два инструмента из стандартной библиотеки:
id()
возвращает адрес памяти объекта;
is
возвращаетTrue
, если и только если два объекта имеют одинаковый адрес памяти.
Можете прогнать этот код в REPL-окружении:
>>> x = 5
>>> id(x)
94529957049376
Здесь мы присвоили переменной
x
значение 5
. Если вы попробуете изменить значение с помощью сложения, то получите новый объект:>>> x += 1
>>> x
6
>>> id(x)
94529957049408
Хотя может показаться, что этот код просто меняет значение
x
, но на самом деле вы получаете в качестве ответа новый объект.Тип
str
тоже неизменяем:>>> s = "real_python"
>>> id(s)
140637819584048
>>> s += "_rocks"
>>> s
'real_python_rocks'
>>> id(s)
140637819609424
И в этом случае
s
после операции +=
получает другой адрес памяти.Бонус: Оператор
+=
преобразовывается в различные вызовы методов.Для некоторых объектов, таких как список,
+=
преобразует в __iadd__()
(локальное добавление). Оно изменит себя и вернёт тот же ID. Однако у str
и int
нет этих методов, и в результате будет вызываться __add__()
вместо __iadd__()
.Подробнее об этом рассказывается в документации по моделям данных Python.
При попытке напрямую изменить строковое значение
s
мы получим ошибку:>>> s[0] = "R"
Обратная трассировка (последними отображаются самые свежие вызовы):
File "<stdin>", line 1, in <mоdule>
TypeError: 'str' object does not support item assignment
Приведённый выше код сбоит и Python сообщает, что
str
не поддерживает это изменение, что соответствует определению неизменяемости типа str
.Сравните с изменяемым объектом, например, со списком:
>>> 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
есть ID. Даже после добавления к списку 4
, my_list
всё ещё имеет тот же ID. Причина в том, что тип list
является изменяемым.Вот ещё одна демонстрация изменяемости списка с помощью присваивания:
>>> my_list[0] = 0
>>> my_list
[0, 2, 3, 4]
>>> id(my_list)
140637819575368
В этом коде мы изменили
my_list
и задали ему в качестве первого элемента 0
. Однако список сохранил тот же ID после этой операции. Следующим шагом на нашем пути к познанию Python будет исследование его экосистемы.Разбираемся с переменными
Переменные в Python в корне отличаются от переменных в C и C++. По сути, их просто нет в Python. Вместо переменных здесь имена.
Это может звучать педантично, и по большей части так оно и есть. Чаще всего можно воспринимать имена в Python в качестве переменных, но необходимо понимать разницу. Это особенно важно, когда изучаешь такую непростую тему, как указатели.
Чтобы вам было проще разобраться, давайте посмотрим, как работают переменные в С, что они представляют, а затем сравним с работой имён в Python.
Переменные в C
Возьмём код, который определяет переменную
x
:int x = 2337;
Исполнение это короткой строки проходит через несколько различных этапов:
- Выделение достаточного количества памяти для числа.
- Присвоение этому месту в памяти значения
2337
.
- Отображение, что
x
указывает на это значение.
Упрощённо память может выглядеть так:
Здесь переменная
x
имеет фальшивый адрес 0x7f1
и значение 2337
. Если позднее вам захочется изменить значение x
, можете сделать так:x = 2338;
Этот код присваивает переменной
x
новое значение 2338
, тем самым перезаписывая предыдущее значение. Это означает, что переменная x
изменяема. Обновлённая схема памяти для нового значения:Обратите внимание, что расположение
x
не поменялось, только само значение. Это важно. Нам это говорит о том, что x
— это место в памяти, а не просто имя.Можно также рассматривать этот вопрос в рамках концепции владения. С одной стороны,
x
владеет местом в памяти. Во-первых, x
— это пустая коробка, которая может содержать лишь одно число (integer), в котором могут храниться целочисленные значения.Когда вы присваиваете
x
какое-то значение, вы тем самым помещаете значение в коробку, принадлежащую x
. Если вы хотите представить новую переменную y
, то можете добавить такую строку:int y = x;
Этот код создаёт новую коробку под названием
y
и копирует в неё значение из x
. Теперь схема памяти выглядит так:Обратите внимание на новое местоположение
y
— 0x7f5
. Хотя в y
и было скопировано значение x
, однако переменная y
владеет новым адресом в памяти. Следовательно, вы можете перезаписывать значение y
, не влияя на x
:y = 2339;
Теперь схема памяти выглядит так:
Повторюсь: вы изменили значение
y
, но не местоположение. Кроме того, вы никак не повлияли на исходную переменную x
. С именами в Python совершенно иная ситуация.
Имена в Python
В Python нет переменных, вместо них имена. Вы можете на своё усмотрение использовать термин «переменные», однако важно знать разницу между переменными и именами.
Давайте возьмём эквивалентный код из вышеприведённого примера на С и напишем его на Python:
>>> x = 2337
Как и в C, в ходе исполнения этого код проходит несколько отдельных этапов:
- Создаётся PyObject.
- Числу для PyObject’а присваивается typecode.
2337
присваивается значение для PyObject’а.
- Создаётся имя
x
. x
указывает на новый PyObject.- Счётчик ссылок PyObject’а увеличивается на 1.
Примечание: PyObject — не то же самое, что объект в Python, эта сущность характерна для CPython и представляет базовую структуру всех объектов Python.
PyObject определяется как C-структура, так что если вы удивляетесь, почему нельзя напрямую вызвать typecode или счётчик ссылок, то причина в том, что у вас нет прямого доступа к структурам. Вызовы методов вроде sys.getrefcount() могут помочь получить какие-то внутренние вещи.
Если говорить о памяти, то это может выглядеть таким образом:
Здесь схема памяти сильно отличается от схемы в С, показанной выше. Вместо того, чтобы
x
владел блоком памяти, в котором хранится значение 2337
, свежесозданный объект Python владеет памятью, в которой живёт 2337
. Python-имя x
не владеет напрямую каким-либо адресом в памяти, как С-переменная владеет статической ячейкой.Если хотите присвоить
x
новое значение, попробуйте такой код:>>> x = 2338
Поведение системы будет отличаться от того, что происходит в С, но будет не слишком сильно отличаться от исходной привязки (bind) в Python.
В этом коде:
- Создаётся новый PyObject.
- Числу для PyObject’а присваивается typecode.
2
присваивается значение для PyObject’а.
x
указывает на новый PyObject.
- Счётчик ссылок нового PyObject увеличивается на 1.
- Счётчик ссылок старого PyObject уменьшается на 1.
Теперь схема памяти выглядит так:
Эта иллюстрация демонстрирует, что
x
указывает на ссылку на объект и не владеет областью памяти, как раньше. Также вы видите, что команда x = 2338
является не присваиванием, а, скорее, привязкой (binding) имени x
к ссылке.Кроме того, предыдущий объект (содержавший значение
2337
) теперь находится в памяти со счётчиком ссылок, равным 0, и будет убран сборщиком мусора.Вы можете ввести новое имя
y
, как в примере на С:>>> y = x
В памяти появится новое имя, но не обязательно новый объект:
Теперь вы видите, что новый Python-объект не создан, создано только новое имя, которое указывает на тот же объект. Кроме того, счётчик ссылок объекта увеличился на 1. Можете проверить эквивалентность идентичности объектов, чтобы подтвердить их одинаковость:
>>> y is x
True
Этот код показывает, что
x
и y
являются одним объектом. Но не ошибитесь: y
всё ещё является неизменяемым. Например, вы можете выполнить с y
операцию сложения:>>> y += 1
>>> y is x
False
После вызова сложения, вам вернётся новый Python-объект. Теперь память выглядит так:
Был создан новый объект, и
y
теперь указывает на него. Любопытно, что точно такое же конечное состояние мы получили бы, если напрямую привязали y
к 2339
:>>> y = 2339
После этого выражения мы получим такое конечное состояние памяти, как и при операции сложения. Напомню, что в Python вы не присваиваете переменные, а привязываете имена к ссылкам.
Об интернированных (intern) объектах в Python
Теперь вы понимаете, как создаются новые объекты в Python и как к ним привязываются имена. Пришло время поговорить об интернированных (interned) объектах.
У нас есть такой Python-код:
>>> x = 1000
>>> y = 1000
>>> x is y
True
Как и раньше,
x
и y
являются именами, указывающими на один и тот же Python-объект. Но это объект, содержащий значение 1000
, не может всегда иметь одинаковый адрес памяти. Например, если вы сложили два числа и получили 1000, то получите другой адрес:>>> x = 1000
>>> y = 499 + 501
>>> x is y
False
На этот раз строка
x is y
возвращает False
. Если вас это смутило, не беспокойтесь. Вот что происходит при исполнении этого кода:- Создаётся Python-объект (
1000
).
- Ему присваивается имя
x
.
- Создаётся Python-объект (
499
).
- Создаётся Python-объект (
501
).
- Эти два объекта складываются.
- Создаётся новый Python-объект (
1000
).
- Ему присваивается имя
y
.
Технические пояснения: описанные шаги имеют место только в том случае, когда этот код исполняется внутри REPL. Если вы возьмёте приведённый пример, вставите в файл и запустите его, то строка
x is y
вернёт True
.Причина в сообразительности компилятора CPython, который старается выполнить peephole-оптимизации, помогающие по мере возможности экономить шаги исполнения кода. Подробности вы можете найти в исходном коде peephole-оптимизатора CPython.
Но разве это не расточительно? Ну да, но эту цену вы платите за все замечательные преимущества Python. Вам не нужно думать об удалении подобных промежуточных объектов, и даже не нужно знать об их существовании! Прикол в том, что эти операции выполняются относительно быстро, и вы бы о них не узнали до этого момента.
Создатели Python мудро подметили эти накладные расходы и решили сделать несколько оптимизаций. Их результатом является поведение, которое может удивить новичков:
>>> x = 20
>>> y = 19 + 1
>>> x is y
True
В этом примере почти такой же код, как и выше, за исключением того, что мы получаем
True
. Всё дело в интернированных (interned) объектах. Python предварительно создаёт в памяти определённое подмножество объектов и хранит их в глобальном пространстве имён для повседневного использования.Какие объекты зависят от реализации Python? В CPython 3.7 интернированными являются:
- Целые числа в диапазоне от
-5
до256
.
- Строки, содержащие только 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()
. Один из способов применения этой функции описан в документации:Интернирование строк полезно для небольшого повышения производительности при поиске по словарю: если ключи в словаре и искомый ключ интернированы, то сравнение ключей (после хэширования) может выполняться с помощью сравнения указателей, а не строк. (Источник)
Интернированные объекты часто путают программистов. Просто запомните, что если начнёте сомневаться, то всегда можете воспользоваться
id()
и is
для определения эквивалентности объектов.Эмулирование указателей в Python
Тот факт, что указатели в Python отсутствуют нативно, не означает, что вы не можете воспользоваться преимуществами применения указателей. На самом деле есть несколько способов эмулирования указателей в Python. Здесь мы рассмотрим два из них:
- Применение в качестве указателей изменяемых типов.
- Применение специально подготовленных Python-объектов.
Применение в качестве указателей изменяемых типов
Вы уже знаете, что такое изменяемые типы. Именно благодаря их изменяемости мы можем эмулировать поведение указателей. Допустим, нужно реплицировать этот код:
void add_one(int *x) {
*x += 1;
}
Этот код берёт указатель на число (
*x
) и инкрементирует значение на 1. Вот основная функция для исполнения кода:#include <stdiо.h>
int main(void) {
int y = 2337;
printf("y = %d\n", y);
add_one(&y);
printf("y = %d\n", y);
return 0;
}
В приведённом фрагменте мы присвоили
y
значение 2337
, вывели на экран текущее значение, увеличили его на 1, а затем вывели новое значение. На экране появляется:y = 2337
y = 2338
Один из способов репликации этого поведения в Python — использовать изменяемый тип. Например, применить список и изменить первый элемент:
>>> def add_one(x):
... x[0] += 1
...
>>> y = [2337]
>>> add_one(y)
>>> y[0]
2338
Здесь
add_one(x)
обращается к первому элементу и увеличивает его значение на 1. Применение списка означает, что в результате мы получим изменённое значение. Так значит в Python существуют указатели? Нет. Описанное поведение стало возможным потому, что список — это изменяемый тип. Если вы попытаетесь использовать кортеж, то получите ошибку:>>> z = (2337,)
>>> add_one(z)
Обратная трассировка (последними идут самые свежие вызовы):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in add_one
TypeError: 'tuple' object does not support item assignment
Этот код демонстрирует неизменяемость кортежа, поэтому он не поддерживает присваивание элементов.
list
не единственный изменяемый тип, указатели части эмулируются и с помощью dict
.Допустим, у вас есть приложение, которое должно отслеживать возникновение интересных событий. Это можно сделать с помощью создания словаря и использования одного из его элементов в качестве счётчика:
>>> counters = {"func_calls": 0}
>>> def bar():
... counters["func_calls"] += 1
...
>>> def foo():
... counters["func_calls"] += 1
... bar()
...
>>> foo()
>>> counters["func_calls"]
2
В этом примере словарь использует счётчики для отслеживания количества вызовов функции. После вызова
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
. Он всё ещё использует словарь для хранения актуальных данных, которые лежат в переменной члена _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
То, что вы можете обратиться к этим именам как к атрибутам, означает, что вы абстрагированы от факта, что эти значения хранятся в словаре. К тому же вы делаете имена атрибутов более явными. Конечно, у вас должна быть возможность увеличивать значения:
class Metrics(object):
# ...
def inc_func_calls(self):
self._metrics["func_calls"] += 1
def inc_cat_pics(self):
self._metrics["cat_pictures_served"] += 1
Мы ввели два новых метода:
inc_func_calls()
inc_cat_pics()
Они меняют значения в словаре
metrics
. Теперь у вас есть класс, который можно изменить так же, как и указатель:>>> metrics = Metrics()
>>> metrics.inc_func_calls()
>>> metrics.inc_func_calls()
>>> metrics.func_calls
2
Вы можете обращаться к
func_calls
и вызывать inc_func_calls()
в разных частях приложений и эмулировать указатели в Python. Это полезно в ситуациях, когда у вас есть что-то вроде metrics
, что нужно часто использовать и обновлять в разных частях приложений.Примечание: В данном случае, явное создание
inc_func_calls()
и inc_cat_pics()
вместо использования @property.setter
не даёт пользователям задавать эти значения произвольному int
, или неправильное значение вроде словаря.Вот полный исходный код класса
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. Если вы не знакомы с ctypes, можете почитать статью Extending Python With C Libraries and the «ctypes» Module.
Вам это может понадобиться в тех случаях, когда нужно вызвать библиотеку С, которой необходимы указатели. Вернёмся к упомянутой выше С-функции
add_one()
:void add_one(int *x) {
*x += 1;
}
Напомню, что этот код увеличивает значение
x
на 1. Чтобы им воспользоваться, сначала скомпилируем код в общий (shared) объект. Будем считать, что наш файл хранится в add.c
, сделать это можно с помощью gcc:$ gcc -c -Wall -Werror -fpic add.c
$ gcc -shared -o libadd1.so add.o
Первая команда компилирует исходный файл C в объект
add.o
. Вторая команда берёт этот несвязанный объект и создаёт общий объект 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 "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 1: <class 'TypeError'>: \
expected LP_c_int instance instead of int
Python бросает ошибку и объясняет, что
add_one()
хочет получить указатель, а не просто целое число. К счастью, в ctypes есть способ передавать указатели таким функциям. Сначала объявим целое число в стиле С:>>> x = ctypes.c_int()
>>> x
c_int(0)
Здесь мы создали целое число
x
со значением 0
. ctypes предоставляет удобную функцию byref()
, которая позволяет передавать переменную по ссылке.Примечание: Словосочетание по ссылке является антонимом передаче переменной по значению.
При передаче по ссылке вы передаёте ссылку на исходную переменную, поэтому изменения будут отражены и на ней. При передаче по значению вы получаете копию исходной переменной, и изменения эту исходную переменную уже не затрагивают.
Для вызова
add_one()
можете использовать этот код:>>> add_one(ctypes.byref(x))
998793640
>>> x
c_int(1)
Отлично! Ваше число увеличилось на 1. Поздравляю, вы успешно использовали в Python настоящие указатели.
Заключение
Теперь вы лучше понимаете взаимосвязь между объектами Python и указателями. Хотя некоторые уточнения касательно имён и переменных выглядят проявлениями педантизма, однако понимание сути эти ключевых терминов улучшает ваше понимание механизма обработки переменных в Python.
Также мы узнали некоторые способы эмулирования указателей в Python:
- Использование изменяемых объектов в качестве указателей с низкими накладными расходами.
- Создание настраиваемых Python-объектов для простоты использования.
- Разлочивание настоящих указателей с помощью модуля ctypes.
Эти методы позволяют эмулировать указатели в Python без необходимости жертвовать предоставляемой языком безопасностью памяти.