Pythonz中一切皆对象

你大概在各种教程、面试题、知乎回答里都见过这句话:

Python 中一切皆对象。

而本章主要讲解的内容,包括下面几点:

  • 「对象」到底是个什么东西

  • 为什么数字、字符串、函数、甚至 print 本身,都是对象

  • 这句话给你写代码带来了什么实际好处

  • 顺便看懂几个之前觉得「奇怪」的 Python 现象

一、先看一个「奇怪」的现象

打开你的终端,进入 Python 交互环境(在命令行输入 python 回车):

1
2
3
4
5
┌─────────────────────────────────────────┐
│$ python │
│Python 3.13.0 │
│>>> _ │
└─────────────────────────────────────────┘

然后敲下面这几行,一行一行来:

1
2
3
4
>>> 10
10
>>> type(10)
<class 'int'>

到目前为止没毛病,10 是个整数,类型是 int

接着来:

1
2
>>> type(int)
<class 'type'>

有点意思了。int 本身,也是个对象,它的类型是 type

再往下挖:

1
2
3
4
5
6
7
8
>>> type(type)
<class 'type'>
>>> type(print)
<class 'builtin_function_or_method'>
>>> type(None)
<class 'NoneType'>
>>> type(True)
<class 'bool'>

你发现没有——intprintNoneTrue,这些东西你平时当「关键字」「内置函数」「特殊值」在用,但它们每一个,都能被 type() 查出类型。

能被查类型,就说明它们都是「某个类」造出来的实例。 这就是「一切皆对象」的第一次露脸。

但这还不够具体。我们得先搞清楚一个根本问题:什么是「对象」?

二、「对象」到底是什么——用快递柜来理解

先把编程书里那些「封装、继承、多态」扔一边,那些是面向对象编程技法,不是「对象」本身。

对象,可以用一个生活类比一次讲透:快递柜

你去快递柜取件,柜子上有编号,比如 A-12。你跟系统说「我要取 A-12」,柜子门弹开,里面是个具体的包裹。

在 Python 的世界里:

  • 对象 = 柜子里的那个包裹。它是真实存在的一坨数据,住在内存里某个位置。

  • 变量名 = 柜子编号 A-12。它只是个标签,贴在某个包裹上,方便你找。

  • 柜子编号不是包裹本身。撕掉标签,包裹还在;换个标签贴上去,也能指到同一个包裹。

这个类比非常关键,因为很多人卡在「一切皆对象」上,根本原因是把变量当成了主角。一旦你意识到「变量只是个标签,真正的主角是对象」,后面的一切都通了。

来看代码验证这个类比:

1
2
3
4
5
>>> a = [1, 2, 3]
>>> b = a
>>> b.append(4)
>>> a
[1, 2, 3, 4]

如果 ab 是两个独立的「盒子」,往 b 里塞 4,不应该影响 a。但 a 变了。

为什么?因为 ab贴在同一个包裹上的两个标签

1
2
3
标签 a ──┐
├──> [ 1, 2, 3, 4 ] ← 内存里真实存在的列表对象
标签 b ──┘

b = a 不是「复制一份」,而是「给同一个对象再贴个标签」。所以动 b 就是动那个对象,a 当然也跟着变。

id() 函数能证明这一点——它返回对象在内存里的地址,相当于包裹的「物理位置」:

1
2
3
4
5
6
7
8
>>> a = [1, 2, 3]
>>> b = a
>>> id(a)
4378912384
>>> id(b)
4378912384 # 跟 a 一模一样,说明是同一个对象
>>> a is b
True

is 运算符就是在比 id,比的是「是不是同一个包裹」。

这就是「对象」的本质:一块有自己身份(id)、有自己类型(type)、有自己数据(value)的内存实体。Python 里你能碰到的任何「值」,本质上都是这种东西。

三、所以「一切皆对象」到底是指什么

现在可以给这句话一个精确的翻译了:

Python 里你能叫得出名字、能拿来用的每一个「东西」——数字、字符串、列表、函数、类、模块、甚至 NoneTrue——背后都是一个真实的对象。它们都有 id,都有类型,都能被赋值给变量、当参数传、当返回值拿回来。

注意这个「一切」是真的「一切」,不是夸张。我们来挨个验证。每一类你都在终端里敲一遍,眼见为实。

3.1 数字和字符串是对象

1
2
3
4
5
6
7
>>> x = 42
>>> id(x)
4378129072
>>> type(x)
<class 'int'>
>>> x.bit_length()
6

42 是个 int 对象,有自己的 id、自己的类型,还能调用方法 .bit_length()(返回这个整数用二进制表示需要多少位,42 的二进制是 101010,6 位)。

你以前可能以为「方法」是 class 才有的东西。但在 Python 里,连 42 这种数字都带方法。字符串也一样:

1
2
3
4
5
>>> s = "hello"
>>> s.upper()
'HELLO'
>>> type(s)
<class 'str'>

"hello" 是个 str 对象,.upper() 是它的方法。

3.2 函数是对象

这是「一切皆对象」最颠覆新手认知的一条。先定义一个最普通的函数:

1
2
3
>>> def greet(name):
... return f"Hello, {name}"
...

然后把它当变量看:

1
2
3
4
>>> type(greet)
<class 'function'>
>>> id(greet)
4378921984

greet 的类型是 function,它有 id。它就是个对象。

既然是对象,就能干对象能干的所有事。比如——把它塞进变量

1
2
3
>>> say_hi = greet      # 不是调用,没加括号
>>> say_hi("Tom")
'Hello, Tom'

say_hi = greet 没有加括号,所以不是「调用 greet」,而是「给 greet 这个对象再贴个标签 say_hi」。跟前面列表那个例子一模一样。

能塞进变量,就能当参数传给别的函数

1
2
3
4
5
>>> def apply(func, value):
... return func(value)
...
>>> apply(greet, "Jerry")
'Hello, Jerry'

apply 接收一个函数当参数,然后调用它。这在 Python 里叫「高阶函数」,听起来很高级,本质就是「函数是对象,能传来传去」。

还能当返回值

1
2
3
4
5
6
7
8
>>> def make_greeter(prefix):
... def inner(name):
... return f"{prefix}, {name}"
... return inner # 注意,返回的是函数本身,没调用
...
>>> shout = make_greeter("HEY")
>>> shout("bob")
'HEY, bob'

make_greeter 吐出来一个新函数,你把它接住、起个名、之后再调用。这就是装饰器的原理——装饰器本质就是个「吃函数、吐函数」的函数。能这么玩,全靠函数是对象。

函数对象甚至还带「属性」,你可以往它身上挂东西:

1
2
3
4
5
6
>>> greet.calls = 0           # 给函数对象加个属性
>>> greet.calls += 1
>>> greet.calls
1
>>> dir(greet) # 看看它身上都有啥
['__call__', '__class__', '__code__', '__defaults__', '__dict__', ...]

dir() 列出一个对象所有的属性和方法。看到没,函数对象身上挂着一堆 __xxx__,这些都是 Python 给它准备好的「内置零件」。

3.3 类本身是对象

这一条最容易把人绕进去,我们慢慢来。

你定义一个类:

1
2
3
4
>>> class Dog:
... def bark(self):
... return "Woof"
...

Dog 是个类,对吧?用它造对象:

1
2
3
>>> d = Dog()
>>> type(d)
<class '__main__.Dog'>

d 的类型是 Dog,这很正常——dDog 类造出来的实例。

Dog 自己呢?

1
2
>>> type(Dog)
<class 'type'>

Dog** 这个类本身,也是个对象,它的类型是 type。**

这句话得多咂摸两遍。在 Java、C++ 这些语言里,「类」是编译期的概念,是图纸,不是运行时存在的东西。但在 Python 里,class Dog: 这行代码执行的时候,Python 真的在内存里造了一个对象,这个对象的名字叫 Dog,类型是 type,它的职责是「用来生产 Dog 实例」。

1
2
3
4
5
6
7
8
9
type(造类机器)

│ 生产出

Dog(类对象,同时也是造 Dog 实例的机器)

│ 生产出

d(Dog 实例对象)

type 是「造类的类」,行话叫「元类」(metaclass)。typeDogDogd,一层套一层。你现在不用深究元类,只要记住:连「类」这种东西,在 Python 里也是对象

验证一下它能当对象用:

1
2
3
4
5
6
7
8
>>> id(Dog)
4378923408
>>> Dog.legs = 4 # 给类对象挂属性
>>> Dog.legs
4
>>> animals = [Dog] # 塞进列表
>>> animals[0]
<class '__main__.Dog'>

类有 id、能挂属性、能进列表——它就是个对象。

3.4 模块是对象

你写 import math,以为只是「引入了一些函数」?其实 Python 做的是:把 math 模块加载进来,在内存里造一个模块对象,然后给它贴个标签 math

1
2
3
4
5
6
7
8
9
>>> import math
>>> type(math)
<class 'module'>
>>> id(math)
4378901234
>>> math.__name__
'math'
>>> math.pi
3.141592653589793

math 是个 module 类型的对象,pi 是它身上的属性。你自己写的 .py 文件被 import 时,同样会变成一个 module 对象。

3.5 连 NoneTrue... 都是对象

1
2
3
4
5
6
7
8
9
10
>>> type(None)
<class 'NoneType'>
>>> type(True)
<class 'bool'>
>>> type(...) # ... 是个省略号字面量,叫 Ellipsis
<class 'ellipsis'>
>>> id(True)
4376543210
>>> True.__class__
<class 'bool'>

NoneNoneType 类型的对象,Truebool 类型的对象。True 甚至有 __class__ 属性——这是每个对象都标配的「我是谁造的」属性。

甚至,那些你以为「不是对象」的「关键字」,比如 importifdef——它们确实不是对象,它们是语法关键字,是语言结构的一部分,不归对象管。「一切皆对象」指的是「所有值和定义出来的东西」,不包括语法本身。这个边界要分清,免得有人拿 type(if) 来杠你。

四、所有对象都有「三件套」

到这里你应该有感觉了。我们给「对象」做一个更精确的定义:

在 Python 里,对象就是一块内存实体,它天生自带三样东西:

  1. 身份(identity)——id() 返回的内存地址,每个对象独一无二。

  2. 类型(type)——type() 返回的类,决定这个对象能干什么、有什么方法。

  3. 值(value)——对象实际承载的数据。

这三样东西,你可以对任何一个 Python 对象去查:

1
2
3
4
5
6
7
8
9
10
11
>>> for obj in [42, "hi", [1,2], greet, Dog, math, None, True]:
... print(f"id={id(obj):>11} type={type(obj).__name__:<20}")
...
id= 4378129072 type=int
id= 4378911216 type=str
id= 4378912384 type=list
id= 4378921984 type=function
id= 4378923408 type=type
id= 4378901234 type=module
id= 4376542992 type=NoneType
id= 4376543210 type=bool

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 就是类的类型)、模块、NoneTrue——这一锅全是对象,每一个都有 id 和 type。这就是「一切皆对象」的硬证据。

is 比的是身份,== 比的是值,这是新手最容易混的地方,正好借着对象三件套讲清楚:

1
2
3
4
5
6
7
8
9
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b # 值相等
True
>>> a is b # 但不是同一个对象
False
>>> c = a
>>> a is c # 这才是同一个对象
True

== 问的是「你俩长得一样吗」,is 问的是「你俩是同一个包裹吗」。

五、那「类型」本身又是谁造的——type 的尾巴

你可能会问一个要命的问题:

既然 intstrDog 都是 type 造出来的对象,那 type 自己是谁造的?

答案是:type** 是自己造的自己**。

1
2
3
4
5
6
7
8
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(type)
<class 'type'> # type 的类型还是 type
>>> type(type) is type
True

别慌,这不是 bug,这是 Python 设计的一个「自举」(bootstrap)小把戏。type 是整个对象宇宙的「根类型」,所有的类(包括它自己)最终都是 type 的实例。

你只需要记住这条链子,所有 Python 对象的「身世」都能查到底:

1
2
3
type  ──造──>  int / str / list / Dog / 你的类  ──造──>  具体对象

└── 也是 type 自己造的(自举)

举个例子顺着链子查:

1
2
3
4
5
6
7
>>> d = Dog()
>>> d.__class__ # d 是谁造的
<class '__main__.Dog'>
>>> d.__class__.__class__ # Dog 是谁造的
<class 'type'>
>>> d.__class__.__class__.__class__ # type 是谁造的
<class 'type'> # 到顶了,type 自己

type 就到顶了,再往上还是 type。这就是对象世界的「天花板」。

所有对象最终的祖宗object 这个类:

1
2
3
4
5
6
>>> int.__bases__          # int 的父类
(<class 'object'>,)
>>> Dog.__bases__
(<class 'object'>,)
>>> object.__bases__ # object 没有父类了
()

object 是所有类的「终极祖先」,任何类的继承链往上追,最后都会追到 objecttypeobject 互相缠绕,撑起了整个 Python 对象宇宙。这块你现在知道有这回事就行,不用背。

所谓的「缠绕」,精确说是这样:objecttype 的实例(isinstance(object, type)True),而 type 又继承自 objecttype.__bases__(object,))。一个造了另一个,另一个又是它的爹--互为因果,这就是「自举」这个词的真正含义。Python 启动时先把这两个根对象建好,整个对象宇宙才有了地基。

六、这句话到底给你带来了什么实际好处

讲了这么多「是什么」,你可能会想:知道这些,我写代码能咋样?

这才是重点。来几个真正能让你代码变好的后果。

6.1 函数能传来传去——告别又臭又长的 if-else

假设你要写个简易计算器,支持加减乘除。新手写法:

1
2
3
4
5
6
7
8
9
10
11
def calc(op, a, b):
if op == "+":
return a + b
elif op == "-":
return a - b
elif op == "*":
return a * b
elif op == "/":
return a / b
else:
raise ValueError("不支持的运算")

每加一个运算符,就得改这个函数,加一个 elif。运算符一多,又长又难维护。

因为函数是对象,你可以这么做——把运算符对应的函数存进字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import operator

ops = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
}

def calc(op, a, b):
func = ops.get(op)
if func is None:
raise ValueError(f"不支持的运算: {op}")
return func(a, b)

operator.add 就是 lambda a, b: a + b 的内置版,它是个函数对象,被存进字典当值。要加新运算符,往字典里塞一项就行,calc 函数一行都不用动。

这就是「函数是对象」最日常的用法。你看很多成熟库的代码,到处都是「用字典当分发器」,原理就是这个。

6.2 装饰器变得理所当然

你大概见过 @xxx 这种语法:

1
2
3
@app.route("/home")
def home():
return "welcome"

@app.route("/home") 到底干了啥?把它翻译成不带 @ 的等价写法,你就懂了:

1
2
3
def home():
return "welcome"
home = app.route("/home")(home)

app.route("/home") 返回一个函数,这个函数吃掉 home、吐出一个新版的 home,再贴回 home 这个标签上。

这一串操作能成立,前提就是函数是对象——能被传进去、能被返回、能被重新赋值。如果函数不是对象,装饰器这个语法根本没法定义。所以下次看到 @,心里默念一句「感谢函数是对象」就行。

6.3 鸭子类型——不查户口,只看能力

「一切皆对象」还带出一个 Python 的核心风格:鸭子类型

一句话:「如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。」

意思是:Python 不关心一个对象是什么类型,只关心它能做什么。你只要实现了需要的方法,就能用,不用非得继承某个类。

看个例子。你想写个函数,把任何「能迭代」的东西打印出来:

1
2
3
def show_all(items):
for x in items:
print(x)

这个函数不在乎 items 是列表、元组、字符串、集合、还是你自定义的类——只要它能被 for 遍历(也就是实现了 __iter__ 方法),就能传进来。

1
2
3
4
5
6
7
8
9
10
11
>>> show_all([1, 2, 3])
1
2
3
>>> show_all("abc")
a
b
c
>>> show_all({10, 20})
10
20

列表、字符串、集合,类型完全不同,但都能用。这在 Java 里得靠「实现同一个接口」或「继承同一个父类」才能做到,Python 不care,只要你「长得像」。

背后的根基是什么?因为所有这些都是对象,而对象的「能力」由它的类型(也就是它身上有哪些方法)决定。Python 在运行时动态查「你有没有这个方法」,而不是编译时查「你属不属于这个类」。

6.4 一切都能被「反射」——dir / getattr / hasattr

既然所有东西都是对象、都有属性,Python 就提供了一组工具让你「在运行时偷看对象身上有什么」:

1
2
3
4
>>> hasattr(str, "upper")
True
>>> getattr("hello", "upper")()
'HELLO'

hasattr 问「这个对象有没有某属性」,getattr 直接把那个属性拿出来。这让你能写出非常灵活的代码——比如根据字符串名字调用方法,配置驱动代码逻辑。

很多 web 框架(比如 Django 的 ORM、FastAPI 的路由注册)就是靠这种「运行时扫描对象属性」的机制工作的。底层还是那句老话:因为一切都是对象,对象都有属性,属性都能被动态访问

七、几个新手最容易踩的坑

道理讲完了,给你排几个「因为一切皆对象」而出现的、会让你一脸懵的现象。

坑 1:可变对象当默认参数

1
2
3
4
5
6
7
8
>>> def add_item(item, lst=[]):
... lst.append(item)
... return lst
...
>>> add_item(1)
[1]
>>> add_item(2)
[1, 2] # ??不是说默认是空列表吗

新手必踩。你以为每次调用 lst 都重新变空,结果它在「记住」上一次的内容。

原因:def 定义函数时,那个默认值 [] 就被造出来一次,存到函数对象身上。之后每次调用都复用同一个列表对象。这恰恰是「函数是对象、默认参数是对象身上的属性」的副产物。

正确写法用 None 占位:

1
2
3
4
5
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

坑 2:小整数缓存,is 乱用

1
2
3
4
5
6
7
8
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False # ???明明都是 257

Python 为了省内存,把 -5256 这些常用小整数预先造好缓存,所有用到这些值的地方都指向同一个对象。所以 256 is 256True——它们真的是同一个包裹。

257 不在缓存范围,每次写 257 都新造一个对象,两个 257 是两个不同的包裹,isFalse 了。

教训:**比较值永远用 ==,只有当你真的想确认「是不是同一个对象」时才用 ****is**(比如跟 None 比,永远用 x is None)。

Note:上面这个实验必须在交互式 >>> 里逐行敲才会看到 False。如果你把这几行写进 .py 文件用 python a.py 运行,或者用 python -c 一次性执行,CPython 编译器会做常量折叠--把两个字面量 257 优化成同一个对象,结果反而是 True。读者照搬到文件里跑,会得到和文档相反的结果,别怀疑人生,这是实现细节的差异。

这恰恰说明了为什么前面反复强调「is 用来比身份」是个实现细节敏感的操作:缓存哪些整数、要不要做常量折叠,都是 CPython 内部优化策略,不同版本、不同运行方式结果可能不同。所以记住结论就好,日常写代码别用 is 比整数,永远用 ==

坑 3:以为赋值是复制

1
2
3
4
5
>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 99
>>> a
[99, 2, 3] # a 也被改了

这就是前面快递柜那个例子的翻版。b = a 是贴标签,不是复制。要真复制:

1
2
3
4
>>> b = a.copy()          # 浅拷贝
>>> b = list(a) # 等价写法
>>> import copy
>>> b = copy.deepcopy(a) # 深拷贝,连嵌套对象一起复制

记住:在 Python 里,赋值永远只是「让一个名字指向一个对象」,从来不复制对象本身。要复制,得显式调用 .copy()copy.deepcopy()

坑 4:+ 创建新对象,原对象不变

1
2
3
4
5
>>> s = "hello"
>>> s + " world"
'hello world'
>>> s
'hello' # s 没变

字符串是不可变对象。s + " world" 不是「修改 s」,而是造了一个全新的字符串对象s 还指着原来那个 "hello"

要更新 s,得把新对象贴回去:

1
2
3
>>> s = s + " world"
>>> s
'hello world'

这也解释了为什么在循环里用 s = s + x 拼字符串很慢——每次都造新对象。大量拼接用 "".join(list) 更好。

八、一张图收尾:Python 对象宇宙

把整篇文章浓缩成一张图,存在脑子里就够了。

从上往下读:object 是终极祖先;type 是元类,造出所有类(包括它自己,所以叫「自举」);内置类型和你的类都是 type 的实例,又各自造出具体的对象实例;最底层是结论--这一切都是对象。

ASCII 简版:

1
2
3
4
5
6
7
8
9
10
11
12
object(终极祖先)
│ 所有类的父类
├──> type(元类)──自举──┐
│ │ 造类 │
│ ├──> 内置类型(int/str/list/function/module/NoneType/bool...)
│ └──> 你的类(Dog)
│ │ 造实例 │ 造实例
│ ▼ ▼
└──> 对象实例:42 "hi" [1,2] greet Dog() None True


一切皆对象:每个都有 id · type · value

记住三句话:

  1. 对象是主角,变量是标签。 你写的 x = 10x 是标签,10 那个对象才是主角。

  2. 所有东西都是对象。 数字、字符串、函数、类、模块、NoneTrue,全都有 id 和 type。

  3. *类型也是对象,由 type 造;所有类的祖先是 object* 这是对象宇宙的根。

小结

  • 「一切皆对象」不是口号,是 Python 的设计事实:你能碰到的每个值——数字、字符串、列表、函数、类、模块、NoneTrue——都是对象,都有 id、type、value。

  • 对象是内存里真实存在的实体,变量只是贴在它上面的标签。赋值是贴标签,不是复制。

  • 函数是对象,所以能当参数传、当返回值拿、能塞进字典——这是装饰器、高阶函数、策略模式的根基。

  • 类本身也是对象,由 type 造;所有类的祖先是 objecttype 自举,是对象宇宙的根。

  • 鸭子类型、getattr/hasattr 反射、可变默认参数坑、is vs == 的区别——这些 Python 特色,全都是「一切皆对象」的副产物。

  • 比较值用 ==,比身份用 is,跟 None 比永远用 is None

下次再有人跟你说「Python 一切皆对象」,别再觉得是句空话了。它是 Python 所有奇妙特性的地基。