返回博客

08. 变量与常量:动态类型与内存模型

08. 变量与常量:动态类型与内存模型

第八篇:变量与常量——动态类型、引用模型与数值运算

前言

在深入 Python 编程的过程中,理解变量和常量的本质至关重要。与其他静态类型语言不同,Python 的变量机制有独特之处。本文将彻底讲解变量命名规则、赋值语句的本质、动态语言的类型推断机制、变量在内存中的引用模型,以及整数除法 / 与 // 和取模 % 运算的区别与用法。

变量的本质:命名而非容器

什么是变量?

在很多教学资料中,变量被描述为"盒子"或"容器"。这个比喻是错误的! 正确的理解是:

# ❌ 错误比喻:变量是一个盒子,里面装了数字 42
var = 42    # 把 42 放进 var 这个盒子

# ✅ 正确理解:变量是一个标签/别名,贴在某个对象上
var = 42    # 给 int(42) 这个对象贴上"var"的标签

Python 变量声明的本质

# Python 没有传统意义上的"变量声明"
# 赋值语句 = 名称绑定操作

x = 42          # 创建 int 对象 42,让 x 指向它
print(x)        # 42
print(type(x))  # <class 'int'>

y = x           # y 也指向同一个 int 对象 42
print(y)        # 42

x = 100         # 创建新的 int 对象 100,让 x 重新指向它
                # y 仍然指向原来的 42
print(x)        # 100
print(y)        # 42 — y 不受影响!

这与 Java、C++ 等静态语言完全不同。Java 先在栈上分配空间存储值,而 Python 创建一个对象,x 成为它的名字。

id() 函数验证内存模型

a = 42
b = a
c = 42

print(id(a))      # 例如 140234867153584
print(id(b))      # 同上 — a 和 b 指向同一对象
print(id(c))      # 同上 — Python 优化了相同小整数的缓存

# 证明它们指向同一个对象
print(a is b)     # True
print(a is c)     # True

# 重新赋值后
b = 100
print(id(a))      # 仍然是原来那个对象的 id
print(id(b))      # 新对象的 id,不同
print(a is b)     # False — 现在指向不同对象

关键概念:id() 返回对象的唯一标识(通常是内存地址)

变量命名规则

合法命名规则

规则 ✅ 正确 ❌ 错误
只能含字母、数字、下划线 user_name user-name
不能以数字开头 name2 2name
不能使用关键字 data class, if, for
区分大小写 Name ≠ name —

命名约定(PEP 8)

# 变量和函数名:snake_case(小写 + 下划线)
user_count = 42
calculate_total()
is_active = True

# 类名:CamelCase(每个单词首字母大写)
class UserProfile:
    pass

# 常量:全大写 + 下划线
MAX_RETRY_COUNT = 3
DEFAULT_TIMEOUT = 30
API_KEY = "abc123"

# 私有属性/方法:前缀单下划线
_internal_data = "内部使用"
def _helper_function():
    pass

# 特殊属性:前后双下划线(魔术方法)
__init__
__str__
__len__

避免的命名陷阱

# ❌ 不要用内置函数名作为变量名
list = [1, 2, 3]       # 覆盖了内置的 list()
str = "hello"          # 覆盖了内置的 str()
type = int             # 覆盖了内置的 type()

# 修复后可以正常使用
del list
my_list = [1, 2, 3]    # 正确做法

# ❌ 不要用容易混淆的名称
l = 1                  # 小写字母 l 看起来像数字 1
O = 0                  # 大写字母 O 看起来像零
I = 1                  # 大写字母 I 看起来像数字 1

# ✅ 使用清晰有意义的名称
length = 1
zero = 0
one = 1

Python 的关键字

import keyword
print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
#  'break', 'class', 'continue', 'def', 'del', 'elif', 'else',
#  'except', 'finally', 'for', 'from', 'global', 'if', 'import',
#  'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass',
#  'raise', 'return', 'try', 'while', 'with', 'yield']

# 检查一个名称是否可用
def is_valid_identifier(name):
    return name.isidentifier() and not keyword.iskeyword(name)

print(is_valid_identifier("user"))    # True
print(is_valid_identifier("for"))     # False — 关键字
print(is_valid_identifier("123abc"))  # False — 以数字开头

动态类型 vs 静态类型

核心区别

# Python 是动态类型语言
x = 42           # x 是 int
x = "hello"      # x 变成 str
x = [1, 2, 3]    # x 变成 list
x = True         # x 变成 bool

# 同一变量可以随时变类型 —— 这是动态类型的特征
print(type(x))   # <class 'bool'>

对比 Java(静态类型):

// Java 编译时就要确定类型,之后不能改变
int x = 42;
x = "hello"; // ❌ 编译错误!Cannot assign String to int

动态类型的优势

# 优势一:灵活多态
def process(data):
    if isinstance(data, list):
        return sum(data)
    elif isinstance(data, str):
        return len(data)
    elif isinstance(data, dict):
        return len(data.keys())
    return 0

process([1, 2, 3])     # 6
process("Hello")       # 5
process({"a": 1})      # 1

动态类型的代价

# 代价一:运行时错误(编译期无法发现)
def divide(a, b):
    return a / b

divide(10, 2)    # 5.0 — 正常工作
divide("10", 2)  # TypeError! — 运行时才发现

赋值的高级用法

多重赋值

# 同时给多个变量赋值
x, y, z = 1, 2, 3
print(x, y, z)  # 1 2 3

# 交换两个变量的值(最优雅的方式)
a, b = 10, 20
a, b = b, a
print(a, b)     # 20 10

解包赋值

# 列表解包
first, second, *rest = [1, 2, 3, 4, 5]
print(first)   # 1
print(second)  # 2
print(rest)    # [3, 4, 5]

# 嵌套解包
coords = [[1, 2], [3, 4]]
[x1, y1], [x2, y2] = coords
print(x1, y1)  # 1 2
print(x2, y2)  # 3 4

链式赋值

# 多个变量指向同一个对象
x = y = z = 0
print(x, y, z)  # 0 0 0

# 注意:可变对象需要小心!
a = []
b = a
a.append(1)
print(b)  # [1] — b 也受影响了

# 如果要独立副本,需要显式复制
a = []
b = a.copy()  # 或使用 list(a)
a.append(1)
print(b)  # [] — 互不影响

Python 的整数除法和取模

/ — 真除法(总是返回浮点数)

print(10 / 3)       # 3.3333333333333335
print(10 / 2)       # 5.0 — 即使整除也是 float
print(-10 / 3)      # -3.3333333333333335
print(10 / 0)       # ZeroDivisionError!

// — 地板除法(向下取整)

print(10 // 3)       # 3
print(10 // 2)       # 5
print(-10 // 3)      # -4 — 向负无穷方向舍入
print(10.5 // 3)     # 3.0

# floor(-3.333) = -4 — 向负无穷舍入
# 而不是 trunc(-3.333) = -3 — 向零截断

% — 取模(求余数)

# % 运算符返回除法的余数
# 关系式:a = (a // b) * b + (a % b)
print(10 % 3)        # 1 — 10 = 3×3 + 1
print(10 % 2)        # 0
print(-10 % 3)       # 2

# ⚠️ Python 的 % 保证结果与除数同号
# 公式保证:(a // b) * b + (a % b) == a

实际应用场景

# 场景一:时间转换
seconds = 3665
hours = seconds // 3600
remaining_seconds = seconds % 3600
minutes = remaining_seconds // 60
secs = remaining_seconds % 60
print(f"{hours}h {minutes}m {secs}s")
# 输出:1h 1m 5s

# 场景二:判断奇偶
print(42 % 2 == 0)   # True
print(13 % 2 == 0)   # False

# 场景三:周期性排列
names = ["Alice", "Bob", "Charlie"]
for i in range(10):
    speaker = names[i % len(names)]
    print(f"发言人{i+1}: {speaker}")

常量:约定俗成

Python 没有真正的常量(编译期不可修改的值),但有约定俗成的写法:

# 惯例:大写命名表示"不应该被修改"
PI = 3.14159265358979
MAX_CONNECTIONS = 100
VERSION = "3.12.5"

# 但它们本质上仍然可以被修改
PI = 3  # 不会报错,只是违反约定
print(PI)  # 3

# 真正不可变的替代方案
from types import SimpleNamespace
config = SimpleNamespace(host="localhost", port=8080)

作用域与命名空间

LEGB 规则

Python 查找变量的顺序:

  • L — Local(局部作用域:函数内部)
  • E — Enclosing(外层函数作用域)
  • G — Global(模块级全局作用域)
  • B — Built-in(Python 内置作用域)
x = "global"  # G

def outer():
    x = "enclosing"  # E
    
    def inner():
        x = "local"  # L
        print(x)     # 找到 local,停止查找
    
    inner()

outer()  # 输出:local

nonlocal 关键字

def outer():
    count = 0
    
    def inner():
        nonlocal count  # 引用外层函数的变量
        count += 1
        return count
    
    return inner

counter = outer()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

总结

本文讲解了变量与常量的核心知识:

  1. 变量的本质 — 变量是对象上的"标签",不是容器
  2. 引用模型 — 通过 id() 可验证多个变量指向同一对象
  3. 命名规则 — 遵循 PEP 8 命名约定(snake_case/CamelCase)
  4. 动态类型 — 变量可随时改变类型,灵活但有运行时风险
  5. 高级赋值 — 多重赋值、解包、链式赋值
  6. 除法运算 — /(真除法)、//(地板除法)、%(取模)
  7. 常量约定 — 大写下划线命名,无编译期保护
  8. 作用域规则 — LEGB 查找顺序

下一篇文章将深入讲解列表 list 的增删改查、切片、排序等完整操作。


本文是 Python 教程系列的第八篇。