Python 与 Java:模块、包、对象、反射核心差异总结
适用口径:Python 3.3+(含命名空间包)/ Java 9+(含 JPMS)。 本文在原始复盘稿基础上做了两处官方口径修正,分别见第二节 2.3 与第四节 4.2。
一、最核心总纲(全文精髓)
| # | 结论 | 精确表述 |
|---|---|---|
| 1 | Python 一切皆对象 | 文件、包、库导入后都是 module 对象,可赋值、可传参、可反射 |
| 2 | Java 的模块对象 | Java 9+ 有 java.lang.Module 运行时对象;但它只是"声明载体",不是命名空间(详见 2.3) |
| 3 | Python 反射本质 | getattr(obj, name) 走 object.__getattribute__,是一条带优先级的查找链 ,不是简单查 __dict__(详见 4.2) |
| 4 | Java 反射本质 | 通过全限定类名字符串加载 Class 对象,再从 Class 上取构造器/字段/方法 |
| 5 | 模块/包的运行时地位 | Python 的模块与包是 运行时对象;Java 的包(package)始终只是静态路径结构 ,只有 Module 才是运行时对象 |
二、Python 模块 & 包 完整定义
2.1 模块(module)
模块 = 被导入后在 sys.modules 中生成 module 对象的一切。
- 文件模块 :单个
xxx.py - 包模块 :带
__init__.py的文件夹
2.2 包(package)
- 包是文件夹形式的特殊模块 :包本身是顶层
module对象,包内部的.py是子module对象 - 不管是文件还是文件夹,
import拿到的全是module对象 - 因此
getattr既可以操作单文件模块,也可以操作包模块
Python 3.3+ 补充 :无
__init__.py也能导入(PEP 420 命名空间包),但企业开发业务包必须保留__init__.py------它决定了包的初始化时机、导出控制与相对导入作用域。
2.3 【修正 1】Java 不是"没有模块对象",而是"模块对象不是命名空间"
这是原始复盘稿最需要更正的一条。原稿写的是:
~~Java 无库对象/模块对象:只有独立 Class 对象,没有"整个 jar 的顶层对象"~~
这个表述只对 Java 8 及以前成立。 Java 9 引入 JPMS(Java Platform Module System,JEP 261)后:
java
Module m = User.class.getModule(); // 每个类都属于一个 Module
m.isNamed(); // true=命名模块(模块化 JAR);false=未命名模块(classpath)
m.getName(); // 模块名,未命名模块返回 null
m.getPackages(); // 该模块导出的包集合
m.getClassLoader();
m.getLayer(); // 所属 ModuleLayer
ModuleLayer.boot(); // 启动层(运行时容器)
关键事实:
java.lang.Module是运行时对象 ,每个被加载的类型都归属某个Module- 类路径(classpath)上的所有类型,统一归入同一个未命名模块------这就是"感觉不到模块对象"的原因
- 模块化 JAR(带
module-info.class)场景下,一个 jar 就是一个Module对象,可以反射拿到它的包、依赖、导出关系
所以准确的分界线不是"有无对象",而是对象性质不同:
| 维度 | Python module 对象 |
Java Module 对象 |
|---|---|---|
| 是否运行时对象 | 是 | 是(Java 9+) |
| 能否当命名空间用 | 能 (mod.User、mod.func()) |
不能 (不可 Module.xxx 取成员) |
| 能否动态增删属性 | 能 (mod.x = 1) |
不能(只读描述) |
| 性质 | 可执行的命名空间 | 只读的声明载体 |
| 获取方式 | import x / importlib |
Class.getModule() / ModuleLayer |
| classpath 场景 | 不适用 | 全部归入同一个未命名模块 |
一句话 :Java 9+ 有 Module,但它是"描述一个 jar 的元数据",不是"装成员的名字空间";Python 的 module 恰恰相反,主要用途就是装成员。
三、Python __dict__ 三层名字空间
Python 有三层相互独立的名字字典,结构统一为:字符串变量名 → 对象引用。
| 层级 | 存储内容 |
|---|---|
模块 __dict__ |
整个 .py 文件/包的全局命名空间(class A → key A 存类对象;a = A() → key a 存实例对象) |
类 __dict__ |
类属性、类方法、@staticmethod、@classmethod |
实例 __dict__ |
该实例独有的属性 |
核心纠正 :不是"实例化覆盖类"。类与实例是字典中两个不同 key,只有对同一个 key 重新赋值才会发生覆盖。
python
class A:
kind = "base" # 写进 类 __dict__
a = A()
a.kind = "child" # 写进 实例 __dict__(不是覆盖类属性!)
A.kind # "base" ← 类字典没动
a.__dict__ # {"kind": "child"}
四、Python getattr 反射原理
4.1 结论先行
- 是不是反射:是,Python 标准反射机制
- 能拿到什么:取决于目标对象的名字空间里存的是什么
python
# test.py
class User: pass
user_obj = User()
getattr(test, "User") # 拿到【类】,需要 () 才能实例化
getattr(test, "user_obj") # 拿到【实例】,直接可用
getattr(test, "some_fn") # 拿到【函数对象】,可直接调用
关键点 :getattr 找的是变量名,不是文件名、也不是类名。查找依据是模块内的变量名字符串。
4.2 【修正 2】"getattr 本质是查 __dict__" ------ 不完整
原稿多处表述为:
~~Python 反射本质:操作对象的
__dict__名字典,通过字符串拿成员~~ ~~getattr 底层本质:运行时通过字符串名称查找任意对象的__dict__属性字典~~
在"对 module 对象做 getattr"这个特定场景下基本成立(普通 module 没有数据描述符这一层)。但把它当成通用原理,会在两个地方翻车:
- 类变量 / 父类属性 :
getattr(obj, 'kind')能拿到类属性,但它不在实例__dict__里 - 数据描述符 :
property的优先级高于 实例__dict__,实例字典里的同名 key 会被直接遮挡
Python 官方 Descriptor HowTo 给出的 object.__getattribute__ 等价实现:
python
def __getattribute__(obj, name):
null = object()
objtype = type(obj)
# ① 沿 type(obj).__mro__ 找类变量
cls_var = find_name_in_mro(objtype, name, null)
if cls_var is not null:
# ② 若有 __set__ / __delete__ → 数据描述符,优先级最高
if hasattr(type(cls_var), '__set__') or hasattr(type(cls_var), '__delete__'):
return type(cls_var).__get__(cls_var, obj, objtype)
# ③ 实例 __dict__
obj_dict = getattr(obj, '__dict__', null)
if obj_dict is not null and name in obj_dict:
return obj_dict[name]
# ④ 非数据描述符 / 普通类变量
if cls_var is not null:
if hasattr(type(cls_var), '__get__'):
return type(cls_var).__get__(cls_var, obj, objtype)
return cls_var
raise AttributeError(name) # ⑤ 抛错 → getattr() 捕获 → __getattr__ 兜底
完整优先级链(从高到低):
① 数据描述符(property / 定义了 __set__ 或 __delete__ 的对象)
↓
② 实例 __dict__
↓
③ 非数据描述符 / 类变量(沿 MRO 查找)
↓
④ type(obj) 的元类属性
↓
⑤ 类中定义的 __getattr__(兜底)
↓
⑥ AttributeError
一句话修正 :getattr(obj, name) 调的是 obj.__getattribute__(name);这是一条描述符优先于实例字典 、沿 MRO 逐级回退 、最后交给 __getattr__ 兜底的查找链,而非一次 __dict__ 哈希查表。
面试速记版:"描述符先开口,实例字典排后头,类变量 MRO 兜,都失手
__getattr__收。"
五、Java vs Python 终极对标
5.1 包 / 文件夹
| 维度 | Java | Python |
|---|---|---|
| 包/文件夹本质 | 纯静态路径结构,运行时不是对象 | 运行时 module 对象 |
| 能否赋值 / 传参 | 不能 | 能 |
| Java 9+ 的例外 | Module(只读元数据对象)存在,但不可作命名空间 |
--- |
5.2 导入机制
| 维度 | Java | Python |
|---|---|---|
import 时机 |
编译期语法糖,不加载类、不执行代码 | 运行时 执行,加载并生成 module 对象 |
| 产物 | 无运行时产物(只是省全称) | sys.modules 中留下一个 module 对象 |
| 副作用 | 无 | 会执行模块顶层代码 |
5.3 反射机制
| 维度 | Java | Python |
|---|---|---|
| 入口 | 全限定类名字符串 → Class.forName() |
先拿到模块/对象 → getattr(obj, "name") |
| 中间层 | 无"库对象"这一层 | 有 module 对象这一层 |
| 取成员 | Class 上取 Constructor / Field / Method |
直接从对象上按变量名取,拿到即可用 |
六、Spring Bean 单例 ↔ Python 模块全局
| 场景 | 行为 |
|---|---|
@Component 类 |
Spring IOC 单例 Bean,容器内全局只 new 一次 |
成员变量位置 new 对象 |
随 Bean 初始化只 new 一次,但不归 Spring 管理------无注入、无 AOP |
方法内 new 对象 |
每次调用方法都新建对象 |
对应到 Python :模块全局变量天然单例 ------因为模块只加载一次,sys.modules 会缓存,重复 import 不重复执行。这是两者在"单例"语义上最干净的对照点。
七、面试速记口诀
7.1 总纲(4 句)
Py 一切皆对象,模块包库全上场; Java 只有 Class,包是路径不发光。 Py 靠字典存名字,getattr 字符串取; Java 反射靠全名,forName 把类举。
7.2 导入(2 句)
import 在 Py 是执行,sys.modules 里留对象; import 在 Java 是糖,编译期就化没了。
7.3 getattr 查找链(6 句)
getattr(obj, "x"),五步走: 描述符,先开口; 实例字典,排后头; 类变量、MRO 兜; 都失手,
__getattr__收。
7.4 __dict__ 三层(4 句)
模块一层,类一层,实例自己再一层; 同名不是覆盖,是三个不同 key 的门。
7.5 单例(2 句)
单例 Bean:容器里一次 new; 模块全局:导一次就够。
7.6 Java 9+ 加分句(2 句)
Java 9 起有 Module,
isNamed辨真假; 但它是"声明",不是"命名空间"。
八、全文终极一句话背诵版
- Python 侧 :模块、包都是运行时
module对象,靠__dict__存所有类、实例、函数;getattr按"描述符 → 实例字典 → MRO →__getattr__"的优先级链,通过字符串反射取值。 - Java 侧 :包是静态路径,运行时只有
Class对象;Java 9+ 另有只读的Module元数据对象,但不可当命名空间;反射只能从全限定类名加载Class起步。