凌晨两点,一段巡检脚本突然报错:
import os
timeout = os.getenv("TIMEOUT", "30")
next_check = timeout + 5# 运行到这里触发 TypeError
配置里明明写的是30,为什么不能加5?
因为环境变量读出来的是字符串"30",不是整数30。Python 不会在timeout第一次出现时,要求你提前声明它的类型;等程序真正执行timeout + 5,解释器才根据两个对象当时的类型判断这次运算是否成立。
这正是很多人从 Java、C 或 C++ 转到 Python 时最不习惯的地方:
int age = 18;
age = 18
Python 少写了一个int,计算机怎么还知道age是整数?
答案先放在这里:「Python 没有取消类型。18是一个int对象,age是绑定到这个对象的名称。」
01 少写了int,类型并没有消失
Java 和 Python 都有类型系统,差别在于类型约束放在哪里、什么时候检查。
在 Java 中:
int age = 18;
变量age的声明类型在编译期已经确定。后面如果写:
age = "18";
编译器会拒绝这次赋值,因为字符串与int不兼容。
在 Python 中:
age = 18
名称age没有被永久声明成int。类型信息在它当前绑定的对象18上。程序执行加法、比较或方法调用等具体操作时,再依据对象的实际类型处理。
所以,“Python 变量没有类型”这句话容易造成误解。更准确的说法是:
❝Python 的对象有类型,名称通常没有被固定为某一种声明类型。
❞
02 Java 的“盒子”模型,要加一个边界
为了入门,我们常把 Java 的基本类型变量画成一个带门禁的盒子:
名称:age
声明类型:int
当前值:18
这个盒子只接收与int兼容的值。它很好地表达了语言规则,但不是 JVM 真实内存布局的截图。实际运行时,局部变量可能使用虚拟机槽位,也可能被优化到寄存器里。
Java 的引用类型变量也能改绑到另一个对象,只是新对象仍要满足已经声明的类型约束。即使使用类型推断,规则也没有改变:
var age = 18; // 编译器推断为 int
age = "18"; // 仍然无法通过编译
Python 采用的是另一套语言模型:名称与对象分开,名称通过引用关系绑定对象。图里的箭头表示“绑定”,不等同于一个可以在 Python 代码中直接操作的原始内存地址。
03age = 18到底做了什么
执行下面这行代码时:
age = 18
从语言层面,可以拆成两个动作:
- 先对等号右侧求值,得到一个值为
18、类型为int的对象;
这里说“得到对象”,可能是创建新对象,也可能是复用已有对象;那属于具体 Python 实现的细节,不影响赋值语义。
现在看这段完整代码:
age = 18
print(age)
print(type(age).__name__)
输出:
18
int
type(age)不是在查询“名称age被声明成了什么类型”,而是在查询:age当前绑定的对象是什么类型。
04x为什么能先是整数,再是字符串
来看一段更能说明问题的代码:
x = 10
y = x
x = "hello"
print(x)
print(y)
输出:
hello
10
执行y = x后,x和y都绑定着整数对象10。随后执行x = "hello",只是让x改绑到字符串对象"hello";y仍然绑定原来的整数对象。
整数10没有变成字符串。变化的是名称x与对象之间的引用关系。
这就是动态类型最核心的机制:
❝同一个名称可以在不同时间绑定不同类型的对象,对象自身的类型不会被这次改绑改变。
❞
05 动态类型,不等于随便混用
名称可以动态改绑,不代表不同类型可以任意混算。
下面的示例可以直接运行:
print(10 + 1)
try:
print(10 + "1")
except TypeError as error:
print(type(error).__name__)
print(10 + int("1"))
输出:
11
TypeError
11
第一行执行整数加法;第二次运算遇到int + str,Python 不会偷偷把字符串"1"猜成整数,于是抛出TypeError;显式执行int("1")后,运算才成立。
Python 因此通常被称为动态、较强类型语言:
- 动态,指类型信息跟随对象,相关检查主要发生在运行时;
- 较强类型,指不相关的类型通常不会为了完成运算而被随意、悄悄地转换。
“强类型”并没有一套适用于所有语言的严格统一定义,Python 也存在有规则的数值兼容,例如1 + 2.5会得到3.5。这里真正需要记住的是:动态类型和没有类型不是一回事。
06 灵活从哪里来,代价又在哪里
动态类型让 Python 代码简洁、组合灵活,写自动化脚本、运维工具和数据处理程序时,开发节奏通常很快。
但它也把一部分问题推迟到了具体代码路径真正运行的时候。没有被测试覆盖的异常分支,可能直到线上收到特殊配置或接口数据才报错。
开头的巡检脚本,应该在外部数据进入程序的边界完成转换:
import os
timeout: int = int(os.getenv("TIMEOUT", "30"))
next_check = timeout + 5
print(next_check)
常用的工程护栏包括:
- 环境变量、命令行参数、配置文件和接口响应进入程序时,立即校验并转换;
类型注解很有价值,但它不等于 Java 式的变量声明。Python 解释器默认不会仅凭注解阻止后续赋值:
age: int = 18
age = "eighteen"
print(age) # 仍会输出 eighteen
编辑器和静态检查器可以提前报告这类不一致;运行时若需要强制校验,则要显式检查,或使用会执行校验的框架。
07 一张图总结
现在再回答开头的问题:
age = 18
Python 之所以不要求先写int,不是因为它不关心类型,而是因为:
- 类型注解、静态检查与测试,可以帮助部分问题更早暴露。
到这里,一个新问题已经出现:如果18是对象,它为什么还能携带方法?
number = 10
print(number.bit_length())
数字有方法,字符串有方法,函数甚至也能被赋值、传递和返回。
接下来的第 2 篇《图解 Python:为什么 Python 一切皆对象?》,会沿着这一篇的“名称绑定对象”继续往里走:对象究竟由什么组成,为什么数字、函数和类都遵守同一套模型。
❝觉得这篇文章对你有帮助吗?
点个 「在看」,「关注」 我们,加个 「星标」,我们一起用最有趣的方式,读懂最硬核的技术!
你的 「支持」 对我非常重要,是我坚持更新的 「唯一动力」。
关注我们,回复 C++ 免费获取 243 节高质量C++课程。
❞