05.06.2019       Выпуск 285 (03.06.2019 - 09.06.2019)       Статьи

Указатели в Python: в чём суть?

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

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

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

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

обзор

.

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

С помощью этой статьи вы:

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

: Здесь термин «Python» применяется к реализации Python на C, которая известна под названием CPython. Все обсуждения устройства языка справедливы для CPython 3.7, но могут не соответствовать последующим итерациям.

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

Не знаю. Могут ли указатели существовать в Python нативно? Вероятно, но судя по всему, указатели противоречат понятию

Zen of 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 — на самом деле объекты. Каждый объект содержит как минимум три вида данных:

Счётчик ссылок

используется для управления памятью. Подробно об этом управлении написано в

Memory Management in Python

. Тип используется на уровне CPython для обеспечения типобезопасности в ходе исполнения (runtime). А значение — это фактическое значение, ассоциированное с объектом.

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

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

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

  1. Неизменяемые объекты (не могут быть изменены);

  2. Изменяемые объекты (могут быть изменены).

Осознание этой разницы — первый ключ к путешествию по миру указателей в Python. Вот характеристика неизменяемости некоторых популярных типов:

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

  1. id() возвращает адрес памяти объекта;

  2. 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;

Исполнение это короткой строки проходит через несколько различных этапов:

  1. Выделение достаточного количества памяти для числа.

  2. Присвоение этому месту в памяти значения 2337.

  3. Отображение, что 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, в ходе исполнения этого код проходит несколько отдельных этапов:

  1. Создаётся PyObject.

  2. Числу для PyObject’а присваивается typecode.

  3. 2337 присваивается значение для PyObject’а.

  4. Создаётся имя x.
  5. x указывает на новый PyObject.
  6. Счётчик ссылок 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

. Если вас это смутило, не беспокойтесь. Вот что происходит при исполнении этого кода:

  1. Создаётся Python-объект (1000).

  2. Ему присваивается имя x.

  3. Создаётся Python-объект (499).

  4. Создаётся Python-объект (501).

  5. Эти два объекта складываются.

  6. Создаётся новый Python-объект (1000).

  7. Ему присваивается имя 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 интернированными являются:

  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()

. Один из способов применения этой функции описан в документации:

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

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

id()

и

is

для определения эквивалентности объектов.

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

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

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

  2. Применение специально подготовленных 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

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

  1. inc_func_calls()
  2. 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 без необходимости жертвовать предоставляемой языком безопасностью памяти.






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus