python中对象的引用,python类的调用

  python中对象的引用,python类的调用

  本文主要介绍如何在Python中调用对象。通过示例代码进行了非常详细的介绍,对于大家的学习或者工作都有一定的参考价值。有需要的朋友就跟着下面的边肖学习吧。

  

目录
从Python的角度查看对象的调用,从解释器的角度总结对象的调用

  

楔子

  我们了解对象是如何创建的,主要有两种方式,一种是通过Python/C API,另一种是通过调用类型对象。对于内置类型的实例对象,两种方法都支持,比如list,可以用[]或者list()创建,前者是Python/C API,后者是调用类型对象。

  但是对于自定义类的实例对象,我们只能通过调用类型对象来创建它们。如果一个对象可以被调用,那么它就是可调用的,否则它就是不可调用的。

  对象是否可调用取决于方法是否在其对应的类型对象中定义。从Python的角度来看,这个方法是__call__,从解释器的角度来看,这个方法是tp_call。

  

从 Python 的角度看对象的调用

  调用int、str、tuple可以创建整数、字符串、元组,调用自定义类也可以创建相应的实例对象,也就是说类型对象是可调用的。那么这些类型对象(int,str,tuple,class等)的类型对象内部一定有一个call方法。).

  # int可以被调用

  #那么它的类型对象,也就是元类(type),内部必须有__call__方法。

  print(hasattr(type, _ _ call _ _ )# True

  #而调用一个对象相当于调用其类型对象的__call__方法。

  #所以int(3.14)实际上等价于如下

  打印(类型。__call__(int,3.14)) # 3

  注意:这里的描述可能会有些绕路。我们说int,str,float都是类型对象(简单来说就是类),而123, Hello ,3.14是它们对应的实例对象。这些都可以。但是类型是类型对象吗?显然,它是。虽然我们称它为元类,但它也是一个类型对象,如果print(type)也显示了一个类的话。

  那么,相对于type,int,str,float又是实例对象吗?因为他们的类型就是类型。

  所以阶级具有双重性:

  从实例对象来看(如:123,禅悟,[],3.14),是类型对象;从类型上看,是实例对象;同样,type的类型也是类型,所以type既是type的类型对象,也是type的实例对象。虽然这里的描述会有一些弯路,但应该不难理解,而且为了避免后续描述的歧义,这里我们做一个声明:

  整数、浮点数、字符串等。我们称之为实例对象int,float,str,dict,我们的自定义类,称为类型对象。虽然type也是一个type对象,但我们称之为元类,所以type内部有一个call方法,也就是说type对象都是可调用的,因为调用type对象就是调用type的call方法。实例对象能否被调用取决于call方法是否在其类型对象中定义,因为调用一个对象实质上是执行其类型对象内部的call方法。

  A:级

  及格

  a=A()

  #因为我们的自定义类a中没有__call__。

  #所以A不能被调用

  尝试:

  答()

  e:除外

  #告诉我们A的实例对象不能被调用

  print(e) # A 对象不可调用

  #如果我们设置了__call__

  类型。_ _ setattr _ _ (a, __call__ ,lambda self 3360 这是_ _ call _ _ )

  #发现可以调用。

  Print(a()) #这是__call__

  我们可以看到,这就是动态语言的特点,即使在类创建之后,仍然可以使用。

  通过type进行动态设置,而这在静态语言中是不支持的。所以type是所有类的元类,它控制了我们自定义类的生成过程,type这个古老而又强大的类可以让我们玩出很多新花样。

  但是对于内置的类,type是不可以对其动态增加、删除或者修改属性的,因为内置的类在底层是静态定义好的。因为从源码中我们看到,这些内置的类、包括元类,它们都是PyTypeObject对象,在底层已经被声明为全局变量了,或者说它们已经作为静态类存在了。所以type虽然是所有类型对象的元类,但是只有在面对我们自定义的类,type才具有增删改的能力。

  Python 的动态性是解释器将字节码翻译成 C 代码的时候动态赋予的,因此给类动态设置属性或方法只适用于动态类,也就是在 py 文件中使用 class 关键字定义的类。

  而对于静态类、或者编写扩展模块时定义的扩展类(两者是等价的),它们在编译之后已经是指向 C 一级的数据结构了,不需要再被解释器解释了,因此解释器自然也就无法在它们身上动手脚,毕竟彪悍的人生不需要解释。

  

try:

   type.__setattr__(dict, "__call__", lambda self: "这是__call__")

  except Exception as e:

   print(e) # cant set attributes of built-in/extension type dict

  我们看到抛异常了,提示我们不可以给内置/扩展类型dict设置属性,因为它们绕过了解释器解释执行这一步,所以其属性不能被动态设置。

  同理其实例对象亦是如此,静态类的实例对象也不可以动态设置属性:

  

class Girl: 

   pass

  g = Girl()

  g.name = "古明地觉"

  # 实例对象我们也可以手动设置属性

  print(g.name) # 古明地觉

  lst = list()

  try:

   lst.name = "古明地觉"

  except Exception as e:

   # 但是内置类型的实例对象是不可以的

   print(e) # list object has no attribute name

  可能有人奇怪了,为什么列表不行呢?答案是内置类型的实例对象没有__dict__属性字典,因为相关属性或方法底层已经定义好了,不可以动态添加。如果我们自定义类的时候,设置了__slots__,那么效果和内置的类是相同的。

  当然了,我们后面会介绍如何通过动态修改解释器来改变这一点,举个栗子,不是说静态类无法动态设置属性吗?下面我就来打自己脸:

  

import gc

  try:

   type.__setattr__(list, "ping", "pong")

  except TypeError as e:

   print(e) # cant set attributes of built-in/extension type list

  # 我们看到无法设置,那么我们就来改变这一点

  attrs = gc.get_referents(tuple.__dict__)[0]

  attrs["ping"] = "pong"

  print(().ping) # pong

  attrs["append"] = lambda self, item: self + (item,)

  print(

   ().append(1).append(2).append(3)

  ) # (1, 2, 3)

  我脸肿了。好吧,其实这只是我们玩的一个小把戏,当我们介绍完整个 CPython 的时候,会来专门聊一聊如何动态修改解释器。比如:让元组变得可修改,让 Python 真正利用多核等等。

  

  

从解释器的角度看对象的调用

  我们以内置类型 float 为例,我们说创建一个 PyFloatObject,可以通过3.14或者float(3.14)的方式。前者使用Python/C API创建,3.14直接被解析为 C 一级数据结构,也就是PyFloatObject实例;后者使用类型对象创建,通过对float进行一个调用、将3.14作为参数,最终也得到指向C一级数据结构PyFloatObject实例。

  Python/C API的创建方式我们已经很清晰了,就是根据值来推断在底层应该对应哪一种数据结构,然后直接创建即可。我们重点看一下通过类型调用来创建实例对象的方式。

  如果一个对象可以被调用,它的类型对象中一定要有tp_call(更准确的说成员tp_call的值是一个函数指针,不可以是0),而PyFloat_Type是可以调用的,这就说明PyType_Type内部的tp_call是一个函数指针,这在Python的层面上我们已经验证过了,下面我们再来通过源码看一下。

  

//typeobject.c

  PyTypeObject PyType_Type = {

   PyVarObject_HEAD_INIT(&PyType_Type, 0)

   "type", /* tp_name */

   sizeof(PyHeapTypeObject), /* tp_basicsize */

   sizeof(PyMemberDef), /* tp_itemsize */

   (destructor)type_dealloc, /* tp_dealloc */

   //... /* tp_hash */

   (ternaryfunc)type_call, /* tp_call */

   //...

  }

  我们看到在实例化PyType_Type的时候PyTypeObject内部的成员tp_call被设置成了type_call。这是一个函数指针,当我们调用PyFloat_Type的时候,会触发这个type_call指向的函数。

  因此 float(3.14) 在C的层面上等价于:

  

(&PyFloat_Type) -> ob_type -> tp_call(&PyFloat_Type, args, kwargs);

  // 即:

  (&PyType_Type) -> tp_call(&PyFloat_Type, args, kwargs);

  // 而在创建 PyType_Type 的时候,给 tp_call 成员传递的是 type_call

  // 因此最终相当于

  type_call(&PyFloat_Type, args, kwargs)

  如果用 Python 来演示这一过程的话:

  

# float(3.14),等价于

  f1 = float.__class__.__call__(float, 3.14)

  # 等价于

  f2 = type.__call__(float, 3.14)

  print(f1, f2) # 3.14 3.14

  这就是 float(3.14) 的秘密,相信list、dict在实例化的时候是怎么做的,你已经猜到了,做法是相同的。

  

# lst = list("abcd")

  lst = list.__class__.__call__(list, "abcd")

  print(lst) # [a, b, c, d]

  # dct = dict([("name", "古明地觉"), ("age", 17)])

  dct = dict.__class__.__call__(dict, [("name", "古明地觉"), ("age", 17)])

  print(dct) # {name: 古明地觉, age: 17}

  最后我们来围观一下 type_call 函数,我们说 type 的 call 方法,在底层对应的是 type_call 函数,它位于Object/typeobject.c中。

  

static PyObject *

  type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)

  {

   // 如果我们调用的是 float

   // 那么显然这里的 type 就是 &PyFloat_Type

   // 这里是声明一个PyObject *

   // 显然它是要返回的实例对象的指针

   PyObject *obj;

   // 这里会检测 tp_new是否为空,tp_new是什么估计有人已经猜到了

   // 我们说__call__对应底层的tp_call

   // 显然__new__对应底层的tp_new,这里是为实例对象分配空间

   if (type->tp_new == NULL) {

   // tp_new 是一个函数指针,指向具体的构造函数

   // 如果 tp_new 为空,说明它没有构造函数

   // 因此会报错,表示无法创建其实例

   PyErr_Format(PyExc_TypeError,

   "cannot create %.100s instances",

   type->tp_name);

   return NULL;

   }

   //通过tp_new分配空间

   //此时实例对象就已经创建完毕了,这里会返回其指针

   obj = type->tp_new(type, args, kwds);

   //类型检测,暂时不用管

   obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);

   if (obj == NULL)

   return NULL;

   //我们说这里的参数type是类型对象,但也可以是元类

   //元类也是由PyTypeObject结构体实例化得到的

   //元类在调用的时候执行的依旧是type_call

   //所以这里是检测type指向的是不是PyType_Type

   //如果是的话,那么实例化得到的obj就不是实例对象了,而是类型对象

   //要单独检测一下

   if (type == &PyType_Type &&

   PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&

   (kwds == NULL

   (PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))

   return obj;

   //tp_new应该返回相应类型对象的实例对象(的指针)

   //但如果不是,就直接将这里的obj返回

   //此处这么做可能有点难理解,我们一会细说

   if (!PyType_IsSubtype(Py_TYPE(obj), type))

   return obj;

   //拿到obj的类型

   type = Py_TYPE(obj);

   //执行 tp_init

   //显然这个tp_init就是__init__函数

   //这与Python中类的实例化过程是一致的。

   if (type->tp_init != NULL) {

   //将tp_new返回的对象作为self,执行 tp_init

   int res = type->tp_init(obj, args, kwds);

   if (res < 0) {

   //执行失败,将引入计数减1,然后将obj设置为NULL

   assert(PyErr_Occurred());

   Py_DECREF(obj);

   obj = NULL;

   }

   else {

   assert(!PyErr_Occurred());

   }

   }

   //返回obj

   return obj;

  }

  因此从上面我们可以看到关键的部分有两个:

  

  • 调用类型对象的 tp_new 指向的函数为实例对象申请内存
  • 调用 tp_init 指向的函数为实例对象进行初始化,也就是设置属性

  所以这对应Python中的__new__和__init__,我们说__new__是为实例对象开辟一份内存,然后返回指向这片内存(对象)的指针,并且该指针会自动传递给__init__中的self。

  

class Girl:

   def __new__(cls, name, age):

   print("__new__方法执行啦")

   # 写法非常固定

   # 调用object.__new__(cls)就会创建Girl的实例对象

   # 因此这里的cls指的就是这里的Girl,注意:一定要返回

   # 因为__new__会将自己的返回值交给__init__中的self

   return object.__new__(cls)

   def __init__(self, name, age):

   print("__init__方法执行啦")

   self.name = name

   self.age = age

  g = Girl("古明地觉", 16)

  print(g.name, g.age)

  """

  __new__方法执行啦

  __init__方法执行啦

  """

  __new__里面的参数要和__init__里面的参数保持一致,因为我们会先执行__new__,然后解释器会将__new__的返回值和我们传递的参数组合起来一起传递给__init__。因此__new__里面的参数除了cls之外,一般都会写

  args和

  *kwargs。

  然后再回过头来看一下type_call中的这几行代码:

  

static PyObject *

  type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)

  {

   //......

   //......

   if (!PyType_IsSubtype(Py_TYPE(obj), type))

   return obj;

   //......

   //......

  }

  我们说tp_new应该返回该类型对象的实例对象,而且一般情况下我们是不写__new__的,会默认执行。但是我们一旦重写了,那么必须要手动返回object.__new__(cls)。可如果我们不返回,或者返回其它的话,会怎么样呢?

  

class Girl:

   def __new__(cls, *args, **kwargs):

   print("__new__方法执行啦")

   instance = object.__new__(cls)

   # 打印看看instance到底是个什么东东

   print("instance:", instance)

   print("type(instance):", type(instance))

   # 正确做法是将instance返回

   # 但是我们不返回, 而是返回个 123

   return 123

   def __init__(self, name, age):

   print("__init__方法执行啦")

  g = Girl()

  """

  __new__方法执行啦

  instance: <__main__.Girl object at 0x000002C0F16FA1F0>

  type(instance): <class __main__.Girl>

  """

  这里面有很多可以说的点,首先就是 init 里面需要两个参数,但是我们没有传,却还不报错。原因就在于这个 init 压根就没有执行,因为 new 返回的不是 Girl 的实例对象。

  通过打印 instance,我们知道了object.__new__(cls) 返回的就是 cls 的实例对象,而这里的cls就是Girl这个类本身。我们必须要返回instance,才会执行对应的__init__,否则__new__直接就返回了。我们在外部来打印一下创建的实例对象吧,看看结果:

  

class Girl:

   def __new__(cls, *args, **kwargs):

   return 123

   def __init__(self, name, age):

   print("__init__方法执行啦")

  g = Girl()

  print(g, type(g)) # 123 <class int>

  我们看到打印的是123,所以再次总结一些tp_new和tp_init之间的区别,当然也对应__new__和__init__的区别:

  

  • tp_new:为该类型对象的实例对象申请内存,在Python的__new__方法中通过object.__new__(cls)的方式申请,然后将其返回
  • tp_init:tp_new的返回值会自动传递给self,然后为self绑定相应的属性,也就是进行实例对象的初始化

  但如果tp_new返回的不是对应类型的实例对象的指针,比如type_call中第一个参数接收的&PyFloat_Type,但是tp_new中返回的却是PyLongObject *,所以此时就不会执行tp_init。

  以上面的代码为例,我们Girl中的__new__应该返回Girl的实例对象才对,但实际上返回了整型,因此类型不一致,所以不会执行__init__。

  下面我们可以做总结了,通过类型对象去创建实例对象的整体流程如下:

  

  • 第一步:获取类型对象的类型对象,说白了就是元类,执行元类的 tp_call 指向的函数,即 type_call
  • 第二步:type_call 会调用该类型对象的 tp_new 指向的函数,如果 tp_new 为 NULL,那么会到 tp_base 指定的父类里面去寻找 tp_new。在新式类当中,所有的类都继承自 object,因此最终会执行 object 的 __new__。然后通过访问对应类型对象中的 tp_basicsize 信息,这个信息记录着该对象的实例对象需要占用多大的内存,继而完成申请内存的操作

  调用type_new 创建完对象之后,就会进行实例对象的初始化,会将指向这片空间的指针交给 tp_init,但前提是 tp_new 返回的实例对象的类型要一致。

  所以都说 Python 在实例化的时候会先调用 new 方法,再调用 init 方法,相信你应该知道原因了,因为在源码中先调用 tp_new、再调用的 tp_init。

  

static PyObject *

  type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)

  {

   //调用__new__方法, 拿到其返回值

   obj = type->tp_new(type, args, kwds);

   if (type->tp_init != NULL) {

   //将__new__返回的实例obj,和args、kwds组合起来

   //一起传给 __init__

   //其中 obj 会传给 self,

   int res = type->tp_init(obj, args, kwds);

   //......

   return obj;

  }

  所以源码层面表现出来的,和我们在 Python 层面看到的是一样的。

  

  

小结

  到此,我们就从 Python 和解释器两个层面了解了对象是如何调用的,更准确的说我们是从解释器的角度对 Python 层面的知识进行了验证,通过 tp_new 和 tp_init 的关系,来了解 newinit 的关系。

  另外,对象调用远不止我们目前说的这么简单,更多的细节隐藏在了幕后,只不过现在没办法将其一次性全部挖掘出来。后续我们会循序渐进,一点点揭开它什么面纱,并且在这个过程中还会不断地学习到新的东西。比如说,实例对象在调用方法的时候会自动将实例本身作为参数传递给 self,那么它为什么传递呢?解释器在背后又做了什么工作呢?这些我们就以后慢慢说吧。

  到此这篇关于浅谈Python中对象是如何被调用的的文章就介绍到这了,更多相关Python对象调用内容请搜索盛行IT软件开发工作室以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT软件开发工作室!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: