Что такое переменные в Python?

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

Как осуществляется хранение объектов в Python?

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

В Python все является объектом. Когда мы работаем с объектами, то используем имена и ссылки, а не традиционные переменные, как в некоторых других языках. 

  • Каждое имя - просто метка для объекта. У объекта может быть несколько имен. 

  • Ссылка - это связь между именем и объектом. Имя, которое мы используем для обращения к объекту, на самом деле является ссылкой на этот объект. 

Итак, объект в Python может иметь несколько имен (ссылок). Все они будут указывать на один и тот же объект в памяти. Каждый объект в Python содержит в себе три элемента тип (type), подсчет ссылок (reference count) и значение (value).

 

Как переменные хранятся в памяти. Изображение автора.
Как переменные хранятся в памяти. Изображение автора.

Ссылки. Введение

Далее, в приведенном ниже примере, переменной num присваивается значение 10

num = 10

Python создает новый объект целого числа типа int (который подобен цифровому представлению целого числа). Переменная num ссылается на этот адрес памяти.

Чтобы найти адрес памяти объекта, на который ссылается переменная, можно воспользоваться встроенной функцией id().

Функция id() возвращает адрес памяти в виде числа в десятичной системе. Мы можем преобразовать его в шестнадцатеричную с помощью встроенной функции hex().

print(hex(id(num)))

--> 0x7ffdb446d448
Шестнадцатеричное представление адреса памяти ссылки. Изображение автора.
Шестнадцатеричное представление адреса памяти ссылки. Изображение автора.

Передача аргументов в функциях Python

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

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

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

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

Пример: Неизменяемый (иммутабельный) параметр 

К неизменяемым относятся объекты встроенных типов: int (целые числа), float (числа с плавающей точкой), complex (комплексные числа), bool (логические значения), strings (строки), bytes (последовательности отдельных байтов) и tuple (кортежи).

def f(name):
    name = 'John'

new_name = 'Mary'
f(new_name)
print(new_name)

Вывод:

Mary

В приведенном выше примере и name, и new_name одновременно указывают на Mary. Но когда name = 'John', воссоздается новый объект со значением John, и name продолжает указывать на него, а new_name по-прежнему указывает на Mary. Следовательно, значение new_name не изменяется.

Пример: Изменяемый (мутабельный) параметр

В Python к изменяемым относятся объекты встроенных типов: list (список), dict (словарь) и set (множество). 

def f(students):
    students.append(3)

students = [0,1,2]
f(students)
print(students)

Вывод:

[0,1,2,3]

В приведенном примере, поскольку students является списком, если вы измените значение students, это приведет к изменению значений всех переменных, которые на него ссылаются. Таким образом, students становится [0,1,2,3].

Сборка мусора

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

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

Существует две основные стратегии сборки мусора:

  • подсчет ссылок

  • сборка мусора на основе поколений

1. Подсчет ссылок

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

Чтобы получить количество ссылок на объект, вы можете воспользоваться встроенным модулем ctypes

import ctypes

def count_references(address):
    """
    Count the number of references to the object at the given address.
    """
    return ctypes.c_long.from_address(address).value

students = 15
print(count_references(id(students)))

# Step 1
toppers = students 
print(count_references(id(students)))

# Step 2
toppers = 2
print(count_references(id(students))) 

# Step 3
students = 1
print(count_references(id(students))) 
Шаг 1: количество ссылок students = 2. Изображение автора.
Шаг 1: количество ссылок students = 2. Изображение автора.
Шаг 2: количество ссылок students = 1. Изображение автора.
Шаг 2: количество ссылок students = 1. Изображение автора.
Шаг 3: Количество ссылок на целочисленный объект со значением 15 будет равно 0. Изображение автора.
Шаг 3: Количество ссылок на целочисленный объект со значением 15 будет равно 0. Изображение автора.

Но подсчет ссылок не может решить проблему циклической ссылки.

Что такое циклическая ссылка?

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

Базовый пример циклической ссылки:

x = []
x.append(x)
print(x)

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

2. Сборка мусора на основе поколений

Сборка мусора на основе поколений (Generational Garbage Collection) использует метод трассировки для сборки мусора.

Сборка мусора на основе трассировки - это техника, используемая в некоторых алгоритмах сборки мусора для выявления и удаления недостижимых объектов. Она работает путем отслеживания выполнения программы и выявления активных объектов на основе их доступности из корневых ссылок.

Generational Garbage Collection разделяет объекты на поколения в зависимости от их возраста, исходя из того, что большинство объектов становятся мусором относительно быстро после создания.

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

Поколенческая сборка мусора помогает решить проблему циклических ссылок, периодически проверяя объекты в разных поколениях и удаляя те из них, которые больше не достижимы. Она обнаруживает и прерывает циклические ссылки, выявляя недостижимые объекты через процесс, известный как "mark and sweep" (“пометка и уборка”).

Таким образом, сборка мусора на основе поколений обеспечивает:

  • отсутствие утечек памяти;

  • правильное использование системных ресурсов;

  • эффективную сборку мусора.

Программное взаимодействие со сборщиком мусора Python

В приведенном ниже примере мы создаем два класса “Students” и “Boys”, ссылающихся друг на друга, и выполняем сборку мусора с помощью встроенного модуля gc (интерфейс сборщика мусора).

Никогда не следует отключать сборщик мусора, если это не требуется.

import gc
import ctypes

def count_references(address):
    """
    Count the number of references to the object at the given address.
    """
    return ctypes.c_long.from_address(address).value

def object_exists(obj_id):
    """
    Return True if the object with the given id exists.
    """
    for obj in gc.get_objects():
        if id(obj) == obj_id:
            return True
    return False

class Students:
    def __init__(self):
        self.boys = Boys(self)
        print(f'Students: {hex(id(self))}, Boys: {hex(id(self.boys))}')

class Boys:
    def __init__(self, students):
        self.students = students
        print(f'Boys: {hex(id(self))}, Students: {hex(id(self.students))}')

gc.disable()

students = Students()

students_id = id(students)
boys_id = id(students.boys)

print(f'Number of references to students: {count_references(students_id)}') # 2

print(f'Number of references to boys: {count_references(boys_id)}') # 1

print(f'Does students exist? {object_exists(students_id)}') # True
print(f'Does boys exist? {object_exists(boys_id)}') # True

students = None

print(f'Number of references to students: {count_references(students_id)}') # 1

print(f'Number of references to boys: {count_references(boys_id)}') # 1

print(f'Does students exist? {object_exists(students_id)}') # True
print(f'Does boys exist? {object_exists(boys_id)}') # True

print('Collecting garbage...')
gc.collect()

print(f'Does students exist? {object_exists(students_id)}') # False
print(f'Does boys exist? {object_exists(boys_id)}') # False

print(f'Number of references to students: {count_references(students_id)}') # 0

print(f'Number of references to boys: {count_references(boys_id)}') # 0

Вывод:

Boys: 0x1e18b68c6d0, Students: 0x1e18b698510
Students: 0x1e18b698510, Boys: 0x1e18b68c6d0
Number of references to students: 2
Number of references to boys: 1
Does students exist? True
Does boys exist? True
Number of references to students: 1
Number of references to boys: 1
Does students exist? True
Does boys exist? True
Collecting garbage...
Does students exist? False
Does boys exist? False
Number of references to students: 0
Number of references to boys: 0

Заключение

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

Перевод статьи подготовлен в рамках запуска специализации Python Developer. На странице вы можете узнать подробнее о специализации, а также зарегистрироваться на бесплатные мероприятия.

Комментарии (2)


  1. bodyawm
    09.08.2023 13:24
    +2

    @

    PYTHON

    @

    ЭФФЕКТИВНОЕ УПРАВЛЕНИЕ ПАМЯТЬЮ

    Анекдот дня просто. Извините, я эмбедщик


  1. dimitrii_z
    09.08.2023 13:24

    На самом деле есть статья тут же, хоть и 2018го но полнее и полезнее:

    https://habr.com/ru/articles/417215/

    За эту конечно тоже плюс, что напомнили про gc, но таки надо проверять а не повтор ли...