- PVSM.RU - https://www.pvsm.ru -
Если вы когда-нибудь работали с такими низкоуровневыми языками, как С или С++, то наверняка слышали про указатели. Они позволяют сильно повышать эффективность разных кусков кода. Но также они могут запутывать новичков — и даже опытных разработчиков — и приводить к багам управления памятью. А есть ли указатели в Python, можно их как-то эмулировать?
Указатели широко применяются в С и С++. По сути, это переменные, которые содержат адреса памяти, по которым находятся другие переменные. Чтобы освежить знания об указателях, почитайте этот обзор [1].
Благодаря этой статье вы лучше поймёте модель объектов в Python и узнаете, почему в этом языке на самом деле не существуют указатели. На случай, если вам понадобится сымитировать поведение указателей, вы научитесь эмулировать их без сопутствующего кошмара управления памятью.
С помощью этой статьи вы:
ctypes
поэкспериментируете с настоящими указателями. Примечание: Здесь термин «Python» применяется к реализации Python на C, которая известна под названием CPython. Все обсуждения устройства языка справедливы для CPython 3.7, но могут не соответствовать последующим итерациям.
Не знаю. Могут ли указатели существовать в Python нативно? Вероятно, но судя по всему, указатели противоречат понятию Zen of Python [2], потому что провоцируют неявные изменения вместо явных. Нередко указатели довольно сложны, особенно для новичков. Более того, они подталкивают вас к неудачным решениям или к тому, чтобы сделать что-нибудь действительно опасное, вроде чтения из области памяти, откуда вам не следовало считывать.
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 — на самом деле объекты. Каждый объект содержит как минимум три вида данных:
Счётчик ссылок [3] используется для управления памятью. Подробно об этом управлении написано в Memory Management in Python [4]. Тип используется на уровне 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__()
.
Подробнее об этом рассказывается в документации по моделям данных [5] Python. [5]
При попытке напрямую изменить строковое значение 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 [6] будет исследование его экосистемы.
Переменные в Python в корне отличаются от переменных в C и C++. По сути, их просто нет в Python. Вместо переменных здесь имена.
Это может звучать педантично, и по большей части так оно и есть. Чаще всего можно воспринимать имена в Python в качестве переменных, но необходимо понимать разницу. Это особенно важно, когда изучаешь такую непростую тему, как указатели.
Чтобы вам было проще разобраться, давайте посмотрим, как работают переменные в С, что они представляют, а затем сравним с работой имён в Python.
Возьмём код, который определяет переменную 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:
>>> x = 2337
Как и в C, в ходе исполнения этого код проходит несколько отдельных этапов:
2337
присваивается значение для PyObject’а.
x
.x
указывает на новый PyObject.Примечание: PyObject [7] — не то же самое, что объект в Python, эта сущность характерна для CPython и представляет базовую структуру всех объектов Python.
PyObject определяется как C-структура, так что если вы удивляетесь, почему нельзя напрямую вызвать typecode или счётчик ссылок, то причина в том, что у вас нет прямого доступа к структурам. Вызовы методов вроде sys.getrefcount() [3] могут помочь получить какие-то внутренние вещи.
Если говорить о памяти, то это может выглядеть таким образом:
Здесь схема памяти сильно отличается от схемы в С, показанной выше. Вместо того, чтобы x
владел блоком памяти, в котором хранится значение 2337
, свежесозданный объект Python владеет памятью, в которой живёт 2337
. Python-имя x
не владеет напрямую каким-либо адресом в памяти, как С-переменная владеет статической ячейкой.
Если хотите присвоить x
новое значение, попробуйте такой код:
>>> x = 2338
Поведение системы будет отличаться от того, что происходит в С, но будет не слишком сильно отличаться от исходной привязки (bind) в Python.
В этом коде:
2
присваивается значение для PyObject’а.
x
указывает на новый PyObject.
Теперь схема памяти выглядит так:
Эта иллюстрация демонстрирует, что x
указывает на ссылку на объект и не владеет областью памяти, как раньше. Также вы видите, что команда x = 2338
является не присваиванием, а, скорее, привязкой (binding) имени x
к ссылке.
Кроме того, предыдущий объект (содержавший значение 2337
) теперь находится в памяти со счётчиком ссылок, равным 0, и будет убран сборщиком мусора [8].
Вы можете ввести новое имя 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 вы не присваиваете переменные, а привязываете имена к ссылкам.
Теперь вы понимаете, как создаются новые объекты в 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
. Если вас это смутило, не беспокойтесь. Вот что происходит при исполнении этого кода:
1000
).
x
.
499
).
501
).
1000
).
y
.
Технические пояснения: описанные шаги имеют место только в том случае, когда этот код исполняется внутри REPL. Если вы возьмёте приведённый пример, вставите в файл и запустите его, то строка x is y
вернёт True
.
Причина в сообразительности компилятора CPython, который старается выполнить peephole-оптимизации [9], помогающие по мере возможности экономить шаги исполнения кода. Подробности вы можете найти в исходном коде peephole-оптимизатора CPython [10].
Но разве это не расточительно? Ну да, но эту цену вы платите за все замечательные преимущества Python. Вам не нужно думать об удалении подобных промежуточных объектов, и даже не нужно знать об их существовании! Прикол в том, что эти операции выполняются относительно быстро, и вы бы о них не узнали до этого момента.
Создатели Python мудро подметили эти накладные расходы и решили сделать несколько оптимизаций. Их результатом является поведение, которое может удивить новичков:
>>> x = 20
>>> y = 19 + 1
>>> x is y
True
В этом примере почти такой же код, как и выше, за исключением того, что мы получаем True
. Всё дело в интернированных (interned) объектах. Python предварительно создаёт в памяти определённое подмножество объектов и хранит их в глобальном пространстве имён для повседневного использования.
Какие объекты зависят от реализации Python? В CPython 3.7 интернированными являются:
-5
до 256
.
Так сделано потому, что эти переменные очень часто используются во многих программах. Интернируя, 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()
. Один из способов применения этой функции описан в документации:
Интернирование строк полезно для небольшого повышения производительности при поиске по словарю: если ключи в словаре и искомый ключ интернированы, то сравнение ключей (после хэширования) может выполняться с помощью сравнения указателей, а не строк. (Источник [11])
Интернированные объекты часто путают программистов. Просто запомните, что если начнёте сомневаться, то всегда можете воспользоваться id()
и is
для определения эквивалентности объектов.
Тот факт, что указатели в Python отсутствуют нативно, не означает, что вы не можете воспользоваться преимуществами применения указателей. На самом деле есть несколько способов эмулирования указателей в Python. Здесь мы рассмотрим два из них:
Вы уже знаете, что такое изменяемые типы. Именно благодаря их изменяемости мы можем эмулировать поведение указателей. Допустим, нужно реплицировать этот код:
void add_one(int *x) {
*x += 1;
}
Этот код берёт указатель на число (*x
) и инкрементирует значение на 1. Вот основная функция для исполнения кода:
#include <stdiо.h>
int main(void) {
int y = 2337;
printf("y = %dn", y);
add_one(&y);
printf("y = %dn", 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++.
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 [12]. Если вы не знакомы с декораторами, то почитайте статью Primer on Python Decorators [13]. В данном случае декоратор @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
Может быть, всё-таки есть указатели в Python, особенно в CPython? С помощью встроенного модуля ctypes можно создать настоящие указатели, как в C. Если вы не знакомы с ctypes, можете почитать статью Extending Python With C Libraries and the «ctypes» Module [14].
Вам это может понадобиться в тех случаях, когда нужно вызвать библиотеку С, которой необходимы указатели. Вернёмся к упомянутой выше С-функции 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 без необходимости жертвовать предоставляемой языком безопасностью памяти.
Автор: Макс
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/319702
Ссылки в тексте:
[1] обзор: https://www.tutorialspoint.com/cprogramming/c_pointers.htm
[2] Zen of Python: https://www.python.org/dev/peps/pep-0020/#id3
[3] Счётчик ссылок: https://docs.python.org/3/library/sys.html#sys.getrefcount
[4] Memory Management in Python: https://realpython.com/python-memory-management/
[5] документации по моделям данных: https://docs.python.org/3/reference/datamodel.html#object.__iadd__
[6] познанию Python: https://realpython.com/pycon-guide/#what-to-do-at-pycon
[7] PyObject: https://github.com/python/cpython/blob/v3.7.3/Include/object.h#L101
[8] сборщиком мусора: https://docs.python.org/3/faq/design.html?highlight=garbage%20collect#how-does-python-manage-memory
[9] peephole-оптимизации: https://en.wikipedia.org/wiki/Peephole_optimization
[10] исходном коде peephole-оптимизатора CPython: https://github.com/python/cpython/blob/master/Python/peephole.c
[11] Источник: https://docs.python.org/3/library/sys.html#sys.intern
[12] @property: https://docs.python.org/3/library/functions.html#property
[13] Primer on Python Decorators: https://realpython.com/primer-on-python-decorators/
[14] Extending Python With C Libraries and the «ctypes» Module: https://dbader.org/blog/python-ctypes-tutorial
[15] Источник: https://habr.com/ru/post/454324/?utm_source=habrahabr&utm_medium=rss&utm_campaign=454324
Нажмите здесь для печати.