你大概在各种教程、面试题、知乎回答里都见过这句话:
Python 中一切皆对象。
而本章主要讲解的内容,包括下面几点:
「对象」到底是个什么东西
为什么数字、字符串、函数、甚至
print本身,都是对象这句话给你写代码带来了什么实际好处
顺便看懂几个之前觉得「奇怪」的 Python 现象
一、先看一个「奇怪」的现象
打开你的终端,进入 Python 交互环境(在命令行输入 python 回车):
1 | ┌─────────────────────────────────────────┐ |
然后敲下面这几行,一行一行来:
1 | 10 |
到目前为止没毛病,10 是个整数,类型是 int。
接着来:
1 | type(int) |
有点意思了。int 本身,也是个对象,它的类型是 type。
再往下挖:
1 | type(type) |
你发现没有——int、print、None、True,这些东西你平时当「关键字」「内置函数」「特殊值」在用,但它们每一个,都能被 type() 查出类型。
能被查类型,就说明它们都是「某个类」造出来的实例。 这就是「一切皆对象」的第一次露脸。
但这还不够具体。我们得先搞清楚一个根本问题:什么是「对象」?
二、「对象」到底是什么——用快递柜来理解
先把编程书里那些「封装、继承、多态」扔一边,那些是面向对象编程技法,不是「对象」本身。
对象,可以用一个生活类比一次讲透:快递柜。
你去快递柜取件,柜子上有编号,比如 A-12。你跟系统说「我要取 A-12」,柜子门弹开,里面是个具体的包裹。
在 Python 的世界里:
对象 = 柜子里的那个包裹。它是真实存在的一坨数据,住在内存里某个位置。
变量名 = 柜子编号 A-12。它只是个标签,贴在某个包裹上,方便你找。
柜子编号不是包裹本身。撕掉标签,包裹还在;换个标签贴上去,也能指到同一个包裹。
这个类比非常关键,因为很多人卡在「一切皆对象」上,根本原因是把变量当成了主角。一旦你意识到「变量只是个标签,真正的主角是对象」,后面的一切都通了。
来看代码验证这个类比:
1 | a = [1, 2, 3] |
如果 a 和 b 是两个独立的「盒子」,往 b 里塞 4,不应该影响 a。但 a 变了。
为什么?因为 a 和 b 是贴在同一个包裹上的两个标签:
1 | 标签 a ──┐ |
b = a 不是「复制一份」,而是「给同一个对象再贴个标签」。所以动 b 就是动那个对象,a 当然也跟着变。
id() 函数能证明这一点——它返回对象在内存里的地址,相当于包裹的「物理位置」:
1 | a = [1, 2, 3] |
is 运算符就是在比 id,比的是「是不是同一个包裹」。
这就是「对象」的本质:一块有自己身份(id)、有自己类型(type)、有自己数据(value)的内存实体。Python 里你能碰到的任何「值」,本质上都是这种东西。
三、所以「一切皆对象」到底是指什么
现在可以给这句话一个精确的翻译了:
Python 里你能叫得出名字、能拿来用的每一个「东西」——数字、字符串、列表、函数、类、模块、甚至
None和True——背后都是一个真实的对象。它们都有 id,都有类型,都能被赋值给变量、当参数传、当返回值拿回来。
注意这个「一切」是真的「一切」,不是夸张。我们来挨个验证。每一类你都在终端里敲一遍,眼见为实。
3.1 数字和字符串是对象
1 | x = 42 |
42 是个 int 对象,有自己的 id、自己的类型,还能调用方法 .bit_length()(返回这个整数用二进制表示需要多少位,42 的二进制是 101010,6 位)。
你以前可能以为「方法」是 class 才有的东西。但在 Python 里,连 42 这种数字都带方法。字符串也一样:
1 | s = "hello" |
"hello" 是个 str 对象,.upper() 是它的方法。
3.2 函数是对象
这是「一切皆对象」最颠覆新手认知的一条。先定义一个最普通的函数:
1 | def greet(name): |
然后把它当变量看:
1 | type(greet) |
greet 的类型是 function,它有 id。它就是个对象。
既然是对象,就能干对象能干的所有事。比如——把它塞进变量:
1 | say_hi = greet # 不是调用,没加括号 |
say_hi = greet 没有加括号,所以不是「调用 greet」,而是「给 greet 这个对象再贴个标签 say_hi」。跟前面列表那个例子一模一样。
能塞进变量,就能当参数传给别的函数:
1 | def apply(func, value): |
apply 接收一个函数当参数,然后调用它。这在 Python 里叫「高阶函数」,听起来很高级,本质就是「函数是对象,能传来传去」。
还能当返回值:
1 | def make_greeter(prefix): |
make_greeter 吐出来一个新函数,你把它接住、起个名、之后再调用。这就是装饰器的原理——装饰器本质就是个「吃函数、吐函数」的函数。能这么玩,全靠函数是对象。
函数对象甚至还带「属性」,你可以往它身上挂东西:
1 | greet.calls = 0 # 给函数对象加个属性 |
dir() 列出一个对象所有的属性和方法。看到没,函数对象身上挂着一堆 __xxx__,这些都是 Python 给它准备好的「内置零件」。
3.3 类本身是对象
这一条最容易把人绕进去,我们慢慢来。
你定义一个类:
1 | class Dog: |
Dog 是个类,对吧?用它造对象:
1 | d = Dog() |
d 的类型是 Dog,这很正常——d 是 Dog 类造出来的实例。
但 Dog 自己呢?
1 | type(Dog) |
Dog** 这个类本身,也是个对象,它的类型是 type。**
这句话得多咂摸两遍。在 Java、C++ 这些语言里,「类」是编译期的概念,是图纸,不是运行时存在的东西。但在 Python 里,class Dog: 这行代码执行的时候,Python 真的在内存里造了一个对象,这个对象的名字叫 Dog,类型是 type,它的职责是「用来生产 Dog 实例」。
1 | type(造类机器) |
type 是「造类的类」,行话叫「元类」(metaclass)。type 造 Dog,Dog 造 d,一层套一层。你现在不用深究元类,只要记住:连「类」这种东西,在 Python 里也是对象。
验证一下它能当对象用:
1 | id(Dog) |
类有 id、能挂属性、能进列表——它就是个对象。
3.4 模块是对象
你写 import math,以为只是「引入了一些函数」?其实 Python 做的是:把 math 模块加载进来,在内存里造一个模块对象,然后给它贴个标签 math。
1 | import math |
math 是个 module 类型的对象,pi 是它身上的属性。你自己写的 .py 文件被 import 时,同样会变成一个 module 对象。
3.5 连 None、True、... 都是对象
1 | type(None) |
None 是 NoneType 类型的对象,True 是 bool 类型的对象。True 甚至有 __class__ 属性——这是每个对象都标配的「我是谁造的」属性。
甚至,那些你以为「不是对象」的「关键字」,比如 import、if、def——它们确实不是对象,它们是语法关键字,是语言结构的一部分,不归对象管。「一切皆对象」指的是「所有值和定义出来的东西」,不包括语法本身。这个边界要分清,免得有人拿 type(if) 来杠你。
四、所有对象都有「三件套」
到这里你应该有感觉了。我们给「对象」做一个更精确的定义:
在 Python 里,对象就是一块内存实体,它天生自带三样东西:
身份(identity)——
id()返回的内存地址,每个对象独一无二。类型(type)——
type()返回的类,决定这个对象能干什么、有什么方法。值(value)——对象实际承载的数据。
这三样东西,你可以对任何一个 Python 对象去查:
1 | for obj in [42, "hi", [1,2], greet, Dog, math, None, True]: |
Note:上面这行 print 里用了个你可能没见过的 f-string 写法,拆开讲一下。f-string 的每个占位符可以用冒号分成两半:{表达式:格式说明符},左边是要显示的值,右边控制怎么显示。{id(obj):>11} 里的 >11 表示「放在 11 格宽的区域里右对齐,左边补空格」;{type(obj).__name__:<20} 里的 <20 表示「占 20 格左对齐,右边补空格」。纯粹是为了让多行输出的两列对齐好看,跟对象本身无关。对齐符就三个:< 左对齐、> 右对齐、^ 居中,宽度数字写在后面。想用别的字符补位就写在符号前面,比如 0>11 用 0 补、_>11 用下划线补。你完全可以用普通的 f"id={id(obj)} type={type(obj).__name__}" 替换它,功能一样,只是输出没那么齐整。
看到了吧。整数、字符串、列表、函数、类(type 就是类的类型)、模块、None、True——这一锅全是对象,每一个都有 id 和 type。这就是「一切皆对象」的硬证据。
is 比的是身份,== 比的是值,这是新手最容易混的地方,正好借着对象三件套讲清楚:
1 | a = [1, 2, 3] |
== 问的是「你俩长得一样吗」,is 问的是「你俩是同一个包裹吗」。
五、那「类型」本身又是谁造的——type 的尾巴
你可能会问一个要命的问题:
既然
int、str、Dog都是type造出来的对象,那type自己是谁造的?
答案是:type** 是自己造的自己**。
1 | type(int) |
别慌,这不是 bug,这是 Python 设计的一个「自举」(bootstrap)小把戏。type 是整个对象宇宙的「根类型」,所有的类(包括它自己)最终都是 type 的实例。
你只需要记住这条链子,所有 Python 对象的「身世」都能查到底:
1 | type ──造──> int / str / list / Dog / 你的类 ──造──> 具体对象 |
举个例子顺着链子查:
1 | d = Dog() |
到 type 就到顶了,再往上还是 type。这就是对象世界的「天花板」。
而所有对象最终的祖宗是 object 这个类:
1 | int.__bases__ # int 的父类 |
object 是所有类的「终极祖先」,任何类的继承链往上追,最后都会追到 object。type 和 object 互相缠绕,撑起了整个 Python 对象宇宙。这块你现在知道有这回事就行,不用背。
所谓的「缠绕」,精确说是这样:object 是 type 的实例(isinstance(object, type) 为 True),而 type 又继承自 object(type.__bases__ 是 (object,))。一个造了另一个,另一个又是它的爹--互为因果,这就是「自举」这个词的真正含义。Python 启动时先把这两个根对象建好,整个对象宇宙才有了地基。
六、这句话到底给你带来了什么实际好处
讲了这么多「是什么」,你可能会想:知道这些,我写代码能咋样?
这才是重点。来几个真正能让你代码变好的后果。
6.1 函数能传来传去——告别又臭又长的 if-else
假设你要写个简易计算器,支持加减乘除。新手写法:
1 | def calc(op, a, b): |
每加一个运算符,就得改这个函数,加一个 elif。运算符一多,又长又难维护。
因为函数是对象,你可以这么做——把运算符对应的函数存进字典:
1 | import operator |
operator.add 就是 lambda a, b: a + b 的内置版,它是个函数对象,被存进字典当值。要加新运算符,往字典里塞一项就行,calc 函数一行都不用动。
这就是「函数是对象」最日常的用法。你看很多成熟库的代码,到处都是「用字典当分发器」,原理就是这个。
6.2 装饰器变得理所当然
你大概见过 @xxx 这种语法:
1 |
|
@app.route("/home") 到底干了啥?把它翻译成不带 @ 的等价写法,你就懂了:
1 | def home(): |
app.route("/home") 返回一个函数,这个函数吃掉 home、吐出一个新版的 home,再贴回 home 这个标签上。
这一串操作能成立,前提就是函数是对象——能被传进去、能被返回、能被重新赋值。如果函数不是对象,装饰器这个语法根本没法定义。所以下次看到 @,心里默念一句「感谢函数是对象」就行。
6.3 鸭子类型——不查户口,只看能力
「一切皆对象」还带出一个 Python 的核心风格:鸭子类型。
一句话:「如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。」
意思是:Python 不关心一个对象是什么类型,只关心它能做什么。你只要实现了需要的方法,就能用,不用非得继承某个类。
看个例子。你想写个函数,把任何「能迭代」的东西打印出来:
1 | def show_all(items): |
这个函数不在乎 items 是列表、元组、字符串、集合、还是你自定义的类——只要它能被 for 遍历(也就是实现了 __iter__ 方法),就能传进来。
1 | show_all([1, 2, 3]) |
列表、字符串、集合,类型完全不同,但都能用。这在 Java 里得靠「实现同一个接口」或「继承同一个父类」才能做到,Python 不care,只要你「长得像」。
背后的根基是什么?因为所有这些都是对象,而对象的「能力」由它的类型(也就是它身上有哪些方法)决定。Python 在运行时动态查「你有没有这个方法」,而不是编译时查「你属不属于这个类」。
6.4 一切都能被「反射」——dir / getattr / hasattr
既然所有东西都是对象、都有属性,Python 就提供了一组工具让你「在运行时偷看对象身上有什么」:
1 | hasattr(str, "upper") |
hasattr 问「这个对象有没有某属性」,getattr 直接把那个属性拿出来。这让你能写出非常灵活的代码——比如根据字符串名字调用方法,配置驱动代码逻辑。
很多 web 框架(比如 Django 的 ORM、FastAPI 的路由注册)就是靠这种「运行时扫描对象属性」的机制工作的。底层还是那句老话:因为一切都是对象,对象都有属性,属性都能被动态访问。
七、几个新手最容易踩的坑
道理讲完了,给你排几个「因为一切皆对象」而出现的、会让你一脸懵的现象。
坑 1:可变对象当默认参数
1 | def add_item(item, lst=[]): |
新手必踩。你以为每次调用 lst 都重新变空,结果它在「记住」上一次的内容。
原因:def 定义函数时,那个默认值 [] 就被造出来一次,存到函数对象身上。之后每次调用都复用同一个列表对象。这恰恰是「函数是对象、默认参数是对象身上的属性」的副产物。
正确写法用 None 占位:
1 | def add_item(item, lst=None): |
坑 2:小整数缓存,is 乱用
1 | a = 256 |
Python 为了省内存,把 -5 到 256 这些常用小整数预先造好缓存,所有用到这些值的地方都指向同一个对象。所以 256 is 256 是 True——它们真的是同一个包裹。
但 257 不在缓存范围,每次写 257 都新造一个对象,两个 257 是两个不同的包裹,is 就 False 了。
教训:**比较值永远用 ==,只有当你真的想确认「是不是同一个对象」时才用 ****is**(比如跟 None 比,永远用 x is None)。
Note:上面这个实验必须在交互式 >>> 里逐行敲才会看到 False。如果你把这几行写进 .py 文件用 python a.py 运行,或者用 python -c 一次性执行,CPython 编译器会做常量折叠--把两个字面量 257 优化成同一个对象,结果反而是 True。读者照搬到文件里跑,会得到和文档相反的结果,别怀疑人生,这是实现细节的差异。
这恰恰说明了为什么前面反复强调「is 用来比身份」是个实现细节敏感的操作:缓存哪些整数、要不要做常量折叠,都是 CPython 内部优化策略,不同版本、不同运行方式结果可能不同。所以记住结论就好,日常写代码别用 is 比整数,永远用 ==。
坑 3:以为赋值是复制
1 | a = [1, 2, 3] |
这就是前面快递柜那个例子的翻版。b = a 是贴标签,不是复制。要真复制:
1 | b = a.copy() # 浅拷贝 |
记住:在 Python 里,赋值永远只是「让一个名字指向一个对象」,从来不复制对象本身。要复制,得显式调用 .copy() 或 copy.deepcopy()。
坑 4:+ 创建新对象,原对象不变
1 | s = "hello" |
字符串是不可变对象。s + " world" 不是「修改 s」,而是造了一个全新的字符串对象,s 还指着原来那个 "hello"。
要更新 s,得把新对象贴回去:
1 | s = s + " world" |
这也解释了为什么在循环里用 s = s + x 拼字符串很慢——每次都造新对象。大量拼接用 "".join(list) 更好。
八、一张图收尾:Python 对象宇宙
把整篇文章浓缩成一张图,存在脑子里就够了。
从上往下读:object 是终极祖先;type 是元类,造出所有类(包括它自己,所以叫「自举」);内置类型和你的类都是 type 的实例,又各自造出具体的对象实例;最底层是结论--这一切都是对象。
ASCII 简版:
1 | object(终极祖先) |
记住三句话:
对象是主角,变量是标签。 你写的
x = 10,x是标签,10那个对象才是主角。所有东西都是对象。 数字、字符串、函数、类、模块、
None、True,全都有 id 和 type。*类型也是对象,由
type造;所有类的祖先是object。* 这是对象宇宙的根。
小结
「一切皆对象」不是口号,是 Python 的设计事实:你能碰到的每个值——数字、字符串、列表、函数、类、模块、
None、True——都是对象,都有 id、type、value。对象是内存里真实存在的实体,变量只是贴在它上面的标签。赋值是贴标签,不是复制。
函数是对象,所以能当参数传、当返回值拿、能塞进字典——这是装饰器、高阶函数、策略模式的根基。
类本身也是对象,由
type造;所有类的祖先是object。type自举,是对象宇宙的根。鸭子类型、
getattr/hasattr反射、可变默认参数坑、isvs==的区别——这些 Python 特色,全都是「一切皆对象」的副产物。比较值用
==,比身份用is,跟None比永远用is None。
下次再有人跟你说「Python 一切皆对象」,别再觉得是句空话了。它是 Python 所有奇妙特性的地基。