
写运维脚本时,我们经常会见到两类很“反常”的代码。
数字居然能调用方法:
number = 10
print(number.bit_length())
函数居然能放进字典,再像普通数据一样取出来:
def restart(service):
return f"restart {service}"
commands = {"restart": restart}
print(commands["restart"]("nginx"))
为什么 10 不是一块只能参与计算的裸数据?为什么 restart 可以被保存、传递,取出来以后还能执行?
答案是 Python 常说的那句话:「一切皆对象。」
不过,这句话并不是说 if、for 或赋值语句本身都是普通对象值。更准确的范围是:在 Python 程序运行时,数字、字符串、容器、函数、类、模块等,都以对象的形式存在。
01 从“名称绑定对象”继续往里看
上一篇我们拆解了这行代码:
age = 18
它不是创建一个“只能装整数的 age 盒子”,而是让名称 age 绑定到整数对象 18。
名称 age
│
│ 绑定
▼
int 对象 18
这解释了为什么 Python 变量不用提前声明固定类型,却也留下了一个新问题:这个“对象”到底是什么?
先看几个运行时的值:
age = 18
service = "nginx"
ports = [80, 443]
print(type(age).__name__)
print(type(service).__name__)
print(type(ports).__name__)
输出:
int
str
list
它们的值不同,支持的操作也不同,但都具有对象的共同特征。名称没有被固定成某一种类型,名称当前绑定的对象却一定有类型。
02 对象到底由什么构成
从 Python 的数据模型看,每个对象都有三项核心信息:身份、类型和值。
身份:是不是同一个对象
对象被创建后,在它的生命周期内会保持自己的身份。is 比较的就是两个名称是否绑定到同一个对象:
a = []
b = []
c = a
print(a == b)
print(a is b)
print(a is c)
输出:
True
False
True
a 和 b 的内容相等,却是两个不同的列表对象;a 和 c 才绑定着同一个对象。
id() 可以返回对象的身份标识,但不要把它当成跨 Python 实现都成立的原始内存地址接口。
类型:对象支持什么操作
类型决定对象可以参与哪些运算、如何查找属性,以及它支持哪些协议。
print(10 + 5)
print("10" + "5")
同一个 +,遇到 int 对象时执行数值相加,遇到 str 对象时执行字符串拼接。
值:对象当前承载的内容或状态
列表对象的内容可以原地变化:
services = ["nginx"]
services.append("redis")
print(services)
整数、字符串等不可变对象则不能原地改变自身的值。相关计算会得到另一个对象。
入门时可以把对象概括为“身份 + 类型 + 值”。方法不宜简单地画成每个对象内部各存一份代码;通常是对象的类型提供相应能力,属性查找时再找到它。
03 整数为什么也能调用方法
回到开头的代码:
number = 10
print(number.bit_length())
输出 4,因为十进制整数 10 的二进制形式是 1010,需要四个有效二进制位。
number 绑定的是一个 int 对象,而 int 本身是 Python 的内置类。bit_length 是 int 类型提供的方法。
通过实例访问它时,可以把查找过程理解为:
找到 number 绑定的对象
↓
确认对象类型是 int
↓
在 int 上找到 bit_length
↓
得到绑定到 number 的方法
↓
加括号执行
下面两种写法得到相同结果:
number = 10
print(number.bit_length())
print(int.bit_length(number))
输出:
4
4
还可以先保存这个绑定方法,再调用它:
method = number.bit_length
print(method())
print(method.__self__ is number)
输出:
4
True
所以,整数有方法,不是每个整数对象都单独复制了一套方法代码,而是对象通过自己的类型获得相应操作。
04 def创建的是函数对象
Python 执行 def 语句时,会创建一个函数对象,再把函数名绑定到它。函数体不会在定义时运行,只有函数对象被调用时才会执行。
def check_disk():
return "ok"
关系可以表示为:
名称 check_disk
│
│ 绑定
▼
function 对象
既然函数是对象,它就能被赋给另一个名称:
task = check_disk
print(task is check_disk)
print(task())
输出:
True
ok
这里没有复制函数代码,task 和 check_disk 绑定着同一个函数对象。
函数也可以保存到容器、作为参数传递,或者作为返回值返回:
jobs = {"disk": check_disk}
def run(job):
return job()
def choose_job():
return check_disk
print(jobs["disk"]())
print(run(check_disk))
print(choose_job()())
三个结果都是 ok。
这里最容易混淆的是括号:
task = check_disk,保存的是函数对象;
task = check_disk(),先执行函数,再保存返回值;
return check_disk,返回函数对象;
return check_disk(),返回函数的执行结果。
05 class创建的也是对象
函数是对象,类也一样。
class Server:
def __init__(self, name):
self.name = name
执行 class 语句时,Python 会准备类的命名空间、执行类体,再创建类对象并把名称 Server 绑定到它。
对于这个使用默认元类的普通类:
print(type(Server) is type)
print(callable(Server))
输出:
True
True
Server 本身是一个类对象。把它赋给另一个名称,不会创建新类:
factory = Server
print(factory is Server)
调用这个普通类对象,通常会创建并初始化一个实例:
web = factory("web-a")
print(type(web) is Server)
print(web.name)
输出:
True
web-a
类对象和它创建的实例是两个不同对象。类对象可以被保存和传递,因此插件系统、对象工厂与依赖注入经常把“类”作为参数交给其他代码。
06 统一对象模型,能力却不相同
数字、字符串、函数和类都可以被名称绑定,也可以放进容器、作为参数传递,或从函数返回。
但“一切皆对象”不等于“一切都能调用”。只有支持调用协议的对象才能使用 ():
def hello():
return "hello"
class Server:
pass
print(callable(hello))
print(callable(Server))
print(callable(10))
print(callable("nginx"))
输出:
True
True
False
False
对象能做什么,最终取决于它的类型与实现的协议,而不是“对象”这个身份本身。
统一对象模型也为一些更高级的能力打下基础:
装饰器可以接收函数对象、包装它,再让原名称绑定返回的新对象;
闭包可以返回函数对象,并依靠词法作用域保留对外层绑定的访问;
元编程可以检查、创建或替换函数与类对象。
这些能力还依赖名称绑定、作用域、描述器、调用协议或元类等机制,“一切皆对象”是共同基础,不是唯一条件。后面的装饰器、迭代器和上下文管理器,也会继续沿用“类型决定协议能力”这条主线。
07 一张图总结
把前两篇连起来,Python 的变量与对象模型已经形成一条完整关系:
变量更准确地说是名称,名称负责绑定对象;
每个对象都有身份、类型和值;
类型决定对象支持哪些操作和协议;
数字、字符串、容器、函数、类等运行时值都遵守对象规则;
函数和类可调用,但可调用只是部分对象拥有的能力。
现在看下面的代码:
a = [1, 2]
b = a
b.append(3)
print(a)
print(a is b)
输出:
[1, 2, 3]
True
a 和 b 都是名称,并且绑定着同一个列表对象。append(3) 修改的是这个共享对象的内容,而不是名称 b 本身。
这又带来一组新问题:为什么整数不能原地修改,列表却可以?什么时候赋值只增加一个引用,什么时候需要浅拷贝或深拷贝?
以上就是“图解 Python:数字也能调用方法?一文搞懂“一切皆对象””的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14403/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料