请选择 进入手机版 | 继续访问电脑版

湖南新梦想

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 69|回复: 0

Python 中对象是如何被调用的?(一)

[复制链接]

3099

主题

3499

帖子

1万

积分

论坛元老

Rank: 8Rank: 8

积分
12384
发表于 2022-6-28 15:37:20 | 显示全部楼层 |阅读模式
       楔子
  我们知道对象被创建,主要有两种方式,一种是通过Python/C API,另一种是通过调用类型对象。对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们即可以通过[]创建,也可以通过list(),前者是Python/C API,后者是调用类型对象。
  但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。而一个对象如果可以被调用,那么这个对象就是callable,否则就不是callable。
  而决定一个对象是不是callable,就取决于其对应的类型对象中是否定义了某个方法。如果从Python的角度看的话,这个方法就是 __call__,从解释器角度看的话,这个方法就是 tp_call。
  从 Python 的角度看对象的调用
  调用 int、str、tuple 可以创建一个整数、字符串、元组,调用自定义的类也可以创建出相应的实例对象,说明类型对象是可调用的,也就是callable。那么这些类型对象(int、str、tuple、class等等)的类型对象(type)内部一定有 __call__ 方法。
  1.   # int可以调用
  2.   # 那么它的类型对象、也就是元类(type), 内部一定有__call__方法
  3.   print(hasattr(type, "__call__"))  # True
  4.   # 而调用一个对象,等价于调用其类型对象的 __call__ 方法
  5.   # 所以 int(3.14)实际就等价于如下
  6.   print(type.__call__(int, 3.14))  # 3
复制代码

  注意:这里描述的可能有一些绕,我们说 int、str、float 这些都是类型对象(简单来说就是类),而 123、"你好"、3.14 是其对应的实例对象,这些都没问题。但type是不是类型对象,显然是的,虽然我们称呼它为元类,但它也是类型对象,如果 print(type) 显示的也是一个类。
  那么相对 type 而言,int、str、float 是不是又成了实例对象呢?因为它们的类型是 type。
  所以 class 具有二象性:
  ·如果站在实例对象(如:123、"satori"、[]、3.14)的角度上,它是类型对象
  · 如果站在 type 的角度上,它是实例对象
  同理 type 的类型是也是 type,那么 type 既是 type 的类型对象,type 也是 type 的实例对象。虽然这里描述的会有一些绕,但应该不难理解,并且为了避免后续的描述出现歧义,这里我们做一个申明:
  · 整数、浮点数、字符串等等,我们称之为实例对象
  · int、float、str、dict,以及我们自定义的类,我们称之为类型对象
  · type 虽然也是类型对象,但我们称它为元类
  所以 type 的内部有 __call__ 方法,那么说明类型对象都是可调用的,因为调用类型对象就是调用 type 的 __call__ 方法。而实例对象能否调用就不一定了,这取决于它的类型对象中是否定义了 __call__ 方法,因为调用一个对象,本质上是执行其类型对象内部的 __call__ 方法。
  1.   class A:
  2.      pass
  3.   a = A()
  4.   # 因为我们自定义的类 A 里面没有 __call__
  5.   # 所以 a 是不可以被调用的
  6.   try:
  7.      a()
  8.   except Exception as e:
  9.      # 告诉我们 A 的实例对象不可以被调用
  10.      print(e)  # 'A' object is not callable   
  11.   # 如果我们给 A 设置了一个 __call__
  12.   type.__setattr__(A, "__call__", lambda self: "这是__call__")
  13.   # 发现可以调用了
  14.   print(a())  # 这是__call__
复制代码
  我们看到这就是动态语言的特性,即便在类创建完毕之后,依旧可以通过type进行动态设置,而这在静态语言中是不支持的。所以type是所有类的元类,它控制了我们自定义类的生成过程,type这个古老而又强大的类可以让我们玩出很多新花样。
  但是对于内置的类,type是不可以对其动态增加、删除或者修改属性的,因为内置的类在底层是静态定义好的。因为从源码中我们看到,这些内置的类、包括元类,它们都是PyTypeObject对象,在底层已经被声明为全局变量了,或者说它们已经作为静态类存在了。所以type虽然是所有类型对象的元类,但是只有在面对我们自定义的类,type才具有增删改的能力。
  而且我们也解释过,Python 的动态性是解释器将字节码翻译成 C 代码的时候动态赋予的,因此给类动态设置属性或方法只适用于动态类,也就是在 py 文件中使用 class 关键字定义的类。
  而对于静态类、或者编写扩展模块时定义的扩展类(两者是等价的),它们在编译之后已经是指向 C 一级的数据结构了,不需要再被解释器解释了,因此解释器自然也就无法在它们身上动手脚,毕竟彪悍的人生不需要解释。
  1.   try:
  2.      type.__setattr__(dict, "__call__", lambda self: "这是__call__")
  3.   except Exception as e:
  4.      print(e)  # can't set attributes of built-in/extension type 'dict'
复制代码
  我们看到抛异常了,提示我们不可以给内置/扩展类型dict设置属性,因为它们绕过了解释器解释执行这一步,所以其属性不能被动态设置。
  同理其实例对象亦是如此,静态类的实例对象也不可以动态设置属性:
  1.   class Girl:  
  2.      pass
  3.   g = Girl()
  4.   g.name = "古明地觉"
  5.   # 实例对象我们也可以手动设置属性
  6.   print(g.name)  # 古明地觉
  7.   lst = list()
  8.   try:
  9.      lst.name = "古明地觉"
  10.   except Exception as e:
  11.      # 但是内置类型的实例对象是不可以的
  12.      print(e)  # 'list' object has no attribute 'name'
复制代码
  可能有人奇怪了,为什么列表不行呢?答案是内置类型的实例对象没有__dict__属性字典,因为相关属性或方法底层已经定义好了,不可以动态添加。如果我们自定义类的时候,设置了__slots__,那么效果和内置的类是相同的。
  当然了,我们后面会介绍如何通过动态修改解释器来改变这一点,举个栗子,不是说静态类无法动态设置属性吗?下面我就来打自己脸:
  1.   import gc
  2.   try:
  3.      type.__setattr__(list, "ping", "pong")
  4.   except TypeError as e:
  5.      print(e)  # can't set attributes of built-in/extension type 'list'
  6.   # 我们看到无法设置,那么我们就来改变这一点
  7.   attrs = gc.get_referents(tuple.__dict__)[0]
  8.   attrs["ping"] = "pong"
  9.   print(().ping)  # pong
  10.   attrs["append"] = lambda self, item: self + (item,)
  11.   print(
  12.      ().append(1).append(2).append(3)
  13.   )  # (1, 2, 3)
复制代码
  我脸肿了。好吧,其实这只是我们玩的一个小把戏,当我们介绍完整个 CPython 的时候,会来专门聊一聊如何动态修改解释器。比如:让元组变得可修改,让 Python 真正利用多核等等。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|湖南新梦想 ( 湘ICP备18019834号-2 )

GMT+8, 2022-8-16 02:57 , Processed in 0.040227 second(s), 19 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表