Перейти к основному содержимому

Переменные и присваивание

Разработчику Архитектору

Переменные в Python

Теория

В императивной модели переменная — именованная память; в Python имя — ссылка на объект, тип принадлежит объекту (динамическая типизация).

Области видимости, LEGB, модули как пространства имён — 4.03, типизация, builtins и типизация.

Особенности переменной в Python

На первых шагах переменную удобно представить как коробочку с наклейкой-именем, куда кладут значение. Так проще понять присваивание name = "Анна". На более глубоком уровне в Python всё устроено иначе: переменная — это имя (ссылка) на объект в памяти, а не "ящик" с типом.

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

Когда мы пишем:

x = 42

…то мы не сохраняем 42 в x, а говорим интерпретатору создать объект типа int со значением 42 и связать с ним имя x. Пошагово:

  • сначала создается объект int(42);
  • затем имя x в текущем namespace начинает ссылаться на этот объект;
  • при следующем присваивании ссылка x перенаправится на другой объект.

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

a = [1, 2, 3]
b = a # b теперь ссылается на тот же список
b.append(4)
print(a) # [1, 2, 3, 4] — изменился и a!

a и b - две ссылки на один и тот же изменяемый объект. Разбор ключевых элементов:

  • b = a создает второе имя для того же списка;
  • append(4) меняет содержимое объекта list на месте;
  • print(a) показывает изменение, потому что a и b разделяют один объект.

Констант в Python нет, существуют лишь общие соглашения об именовании, когда переменные называют верхним регистром:

<ИМЯ_КОНСТАНТЫ> = <значение>

Пример:

MAX_CONNECTIONS = 100
DEFAULT_ENCODING = "utf-8"

Объявление переменных

В Python нет ключевых слов для объявления переменных. Нет ничего вроде var, let, const, int, str и т.п.

В других языках нам пришлось бы писать сложные конструкции:

let x = 1;
int x = 1;
local x = 1

Python отказывается от такого подхода и подразумевает обычное написание названия:

x = 1

Объявление переменной происходит в момент первого присваивания:

name = "Алексей" # переменная создана
age = 30 # ещё одна
is_student = False # и ещё одна

Никаких предварительных объявлений — интерпретатор сам добавляет имя в текущее пространство имён.

<имя> = <значение>

Если присвоить значение уже существующей переменной, то значение перезапишется.

x = 1
x = 2
x = 3
print(x) # выведет 3

Пример:

counter = 0
name = "Анна"
is_valid = True

Допустимо множественное присваивание:

<имя1>, <имя2>, ..., <имяN> = <значение1>, <значение2>, ..., <значениеN>

Пример:

x, y = 10, 20
a, b, c = "A", "B", "C"

Разбор:

  • справа формируется кортеж значений;
  • Python выполняет распаковку по позициям;
  • количество имен слева и значений справа должно совпадать.

Имеется возможность распаковки последовательности:

<первый>, *<остальные> = <последовательность>

Пример:

first, *rest = [1, 2, 3, 4]
# first = 1, rest = [2, 3, 4]

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

<имя1>, <имя2> = <имя2>, <имя1>

Пример:

a, b = b, a

Литералы

Литерал — это способ записи значения прямо в коде. Они используются при присвоении переменных:

number = 42 # 42 — целочисленный литерал
pi = 3.14159 # литерал float
text = "Hello" # строковый литерал
flag = True # булев литерал
data = [1, 2, 3] # литерал списка
config = {"db": "prod"} # литерал словаря
point = (10, 20) # литерал кортежа
binary = b"hello" # байтовый литерал

Литералы — это "сырые" значения, которые создаются как объекты в памяти, и на них можно ссылаться через переменные.

<имя> = <литерал>

Равенство значений и идентичность объекта

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

value = 100 # int
value = "текст" # str
value = [1, 2] # list

Поскольку переменные — это ссылки, важно различать равенство значений (==) и идентичность объекта (is).

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b) # True — значения одинаковые
print(a is b) # False — разные объекты в памяти
print(a is c) # True — ссылаются на один объект

Разбор:

  • == сравнивает содержимое коллекций.
  • is сравнивает идентичность объектов, то есть совпадение ссылки.
  • c = a связывает имена с одним объектом, поэтому a is c дает True.

Функция id() показывает уникальный идентификатор объекта:

print(id(a)) # например, 140234567890123
print(id(b)) # другой адрес
print(id(c)) # такой же, как у a
<переменная1> == <переменная2> # сравнение значений
<переменная1> is <переменная2> # сравнение объектов в памяти

Пример:

a = [1, 2]
b = [1, 2]
c = a

print(a == b) # True
print(a is b) # False
print(a is c) # True
id(<переменная>)

Пример:

x = [1, 2, 3]
print(id(x)) # уникальный адрес объекта в памяти

Области видимости

Области видимости переменных (Scopes).

Python следует правилу LEGB, определяющему, где искать переменную при обращении к ней:

  • L - Local, внутри функции;
  • E - Enclosing - в обрамляющих функциях (замыканиях);
  • G - Global - на уровне модуля;
  • B - Built-in - встроенные имена (len, print, True).

Пример:

Код ITЗагрузка примера кода…

Локальные переменные создаются внутри функции и доступны только внутри неё.

def greet():
message = "Привет!" # локальная переменная
print(message)

greet()
# print(message) # Ошибка! NameError

Глобальные переменные объявлены на уровне модуля (вне функций/классов):

counter = 0

def increment():
global counter # говорим, что хотим использовать глобальную переменную
counter += 1

increment()
print(counter) # 1

Говоря global, мы как бы говорим Python-у, что "если что, имеем в виду именно глобальную counter".

global <имя>
<имя> = <новое_значение>

Пример:

counter = 0

def increment():
global counter
counter += 1

Без global попытка изменить counter создаст локальную переменную с тем же именем.

Аналогично — nonlocal для доступа к переменным из обрамляющей области:

def outer():
x = "внешний"

def inner():
nonlocal x
x = "изменили изнутри"

inner()
print(x) # "изменили изнутри"

Разбор:

  • outer() создает обрамляющую область видимости.
  • nonlocal x внутри inner() связывает присваивание с x из outer, а не с новой локальной переменной.
  • после вызова inner() значение x в outer уже обновлено.

Поэтому в Python, как и в JavaScript, есть лексическое окружение и замыкания (closures).

Нелокальная переменная (замыкание):

nonlocal <имя>
<имя> = <новое_значение>

Пример:

def outer():
x = "внешний"
def inner():
nonlocal x
x = "изменённый"
inner()
return x

Для проверки существования глобальной переменной можно использовать условный оператор:

if '<имя>' in globals():
...

Пример:

if 'DEBUG_MODE' in globals():
print("Режим отладки активен")

Удаление переменной

Допустимо также и удаление переменной:

del <имя>

Пример:

temp = 42
del temp
# temp больше не существует в текущем пространстве имён

Замыкание

Замыкание — функция, которая запоминает значения из обрамляющей области, даже после завершения этой области.

def make_multiplier(n):
def multiplier(x):
return x * n # n — из обрамляющей области
return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5)) # 10
print(triple(5)) # 15

Разбор:

  • make_multiplier(n) возвращает функцию multiplier, которая "захватывает" n.
  • double = make_multiplier(2) создает версию функции с фиксированным множителем 2.
  • при вызове double(5) используется x=5 и захваченное n=2, результат 10.

Здесь multiplier — замыкание, которое "захватывает" значение n. Атрибут .__closure__ позволяет посмотреть захваченные переменные:

print(double.__closure__[0].cell_contents) # 2

Это мощный механизм, используемый в декораторах, каррировании и фабриках функций. Но о функциях позже.


Правила имён переменных

Отдельной команды "объявить переменную" в Python нет: имя появляется при первом присваивании. Зато есть жёсткие правила для самого имени:

  • начинается с буквы (латиница или кириллица) или с _, но не с цифры;
  • дальше — только буквы, цифры и _ (пробелы и дефисы недопустимы);
  • регистр важен: age, Age и AGEтри разные переменные;
  • нельзя использовать ключевые слова (if, class, for и т.д.).
user_name = "Анна" # понятное имя
total_volume = 1000 # несколько слов через _
x = 5 # короткое имя допустимо в учебных примерах

# 2name = "ошибка" # SyntaxError — имя не может начинаться с цифры

Для вывода в консоль print() часто склеивает строки через +. Для текста это удобно, для чисел + — обычное сложение; смешивать типы без преобразования нельзя:

print("Python " + "удобен") # Python удобен
print(5 + 10) # 15
# print(5 + "10") # TypeError — int + str
print("Мне " + str(10) + " лет")

Соглашения об именах

Python поддерживает соглашения об именах, влияющих на поведение. Мы уже о них говорили - это _ для временной переменной, __name__ для специальных переменных модуля.

Кроме этого, в Python нет настоящих констант, но есть соглашение - имена в верхнем регистре считаются константами:

PI = 3.14159
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30

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

NUMBER = 123
MAIN_NAME = "TEST"

Это не защищает от изменения, но сигнализирует, что значение не должно меняться.

PI = 3 # технически возможно, но плохо по стилю

Для настоящей защиты можно использовать модуль types.MappingProxyType (неизменяемый словарь), классы с @property или сторонние библиотеки вроде const.

from types import MappingProxyType

CONFIG = {
'API_URL': 'https://api.example.com',
'TIMEOUT': 30
}

READONLY_CONFIG = MappingProxyType(CONFIG)
# READONLY_CONFIG['API_URL'] = '...' # Ошибка!

И разумеется, нельзя использовать ключевые слова как имена:

# if = 5 # SyntaxError
# class = "A" # SyntaxError

Список ключевых слов можно получить:


import keyword

print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', ...]

В именах разрешены буквы, цифры, подчёркивание. Начинать следует с буквы или нижнего подчёркивания (_), но не с цифры. А юникод-символы лучше избегать, хотя можно.

Стиль написания в Python именно snake_case, стандарт PEP 8, а имена описывать лучше так — user_name, total_price, is_active.

Имеется множественное присваивание:

a, b = 1, 2
x, y, z = "X", "Y", "Z"

С распаковкой:

first, *rest = [1, 2, 3, 4]
# first = 1, rest = [2, 3, 4]

Обмен значениями:

a, b = b, a # без временной переменной

Удаление переменной:

x = 100
del x # имя x удаляется из пространства имён
# print(x) # NameError

del удаляет ссылку, а не объект. Объект удалится сборщиком мусора, когда на него не останется ссылок.

Проверка существования переменной:

if 'my_var' in globals():
print("Есть такая глобальная переменная")

# Или через getattr, hasattr — особенно для объектов

Пространства имён

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

Виды:

  • Локальное — locals()
  • Глобальное — globals()
  • Встроенное — dir(builtins)
x = 10

def func():
y = 20
print(locals()) # {'y': 20}
print(globals()['x']) # 10

func()

Эти функции полезны для метапрограммирования, но редко нужны в обычном коде.

locals() # локальное пространство имён
globals() # глобальное пространство имён

Пример:

value = 100

def func():
local_var = 200
print(globals()['value']) # 100
print(locals()) # {'local_var': 200}