好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 the_stars 于 2020-8-31 15:17 编辑
前几天重装系统, 之前的笔记不见了. 现在重新把类的创建过程记录一次.
很多原理是不是这样并不是特别的清楚, 但是表面运行的一些东西可以去探究一二. 本质还是需要去看Python的C源代码, 下面代码都是由我自己试出来的, 可能会有很多不合理的地方, 希望各位大神可以指出.
如果有pycharm, 以下所有print打印可以通过pycharm的debug调试来代替来查看各种参数或者过程
1. 关于type
每个类其实也是一个对象, 都是由type类创建出来的对象, 这个类(type的对象), 具有call行为, 因此这个类(type的对象)可以通过
cls() 可以加括号代表具有call行为, (对应方法__call__) 来创建实例. type是由自己创建而来的
>>> a = 1
>>> type(a)
<class `int`>
>>> type(int)
<class `type`>
>>> type(type)
<class `type`>
>>> class Test(object):
pass
>>> test_object = Test()
>>> type(test_object)
<class `Test`>
>>> type(Test)
<class `type`>
-
先扯一下关于type新手容易犯的错误. 在我刚学python那段时间, 看一个东西是什么类型, 就喜欢用print(type(xxx))来判断类型, 有一次当时判断一个参数是不是int类型, 因为第一次遇到, 立马想到的是 type(1) == "int", 结果条件一直是false, 当时就特别纳闷, 后面才知道, type(x)返回的是x的类, 因此type(1)返回的是int这个类, (关于为什么print(type(1))打印的是<class int >, 这个了解过__str__ 和__repr__ 方法的应该都很清楚了. 这里不涉及)
将一个class int 与 字符串"int"做比较自然是false, 既然type(1)是 class int 那么int("213")可不可以用type(1)("213")代替呢?
当然是可以的
>>> a = type(1)
>>> a("123")
123
因为type(1)返回的是int 所以type(1)("123") 等价 int("123"), 当然用type(2)什么的都可以 所以如果要判断x是不是int类型, 可以通过这样判断type(a) == int (注意就是int, 没有引号), 或者 type(a) is int, 不过更推荐isinstance(a, int)
(在最下方已更正), 这里isinstance(a, int)是不可以的, 推荐用type(a) is int
阅读过一些源码的同学应该见过不少这样的代码, 在一个实例方法里面, 姑且叫他test方法
class ...:
...
def test(self):
...
return type(self)(*args, **kwargs)
最后的type(self)其实就是当前的类, 后面加括号就是自己这个类加括号, 就是生成自己这个类的对象返回回去. 由于是Python是动态语言, 这里self可能不是自己这个类, 而是被传入其他类. 这里后面再说
-
既然所有的类都是由type创建的(这句话记住就好了), 那么哪些类的创建过程是怎样的? 现在先贴一下代码
class Meta(type):
pass
class Test(object, metaclass=Meta):
pass
这里metaclass=Meta是指定以Meta为元类(元类必须继承type或者其余type的子类)来创建Test类, 不指定默认为type, 为什么这么指定我就不清楚了. 记住就好了.
我猜测可能在type.__new__ 中会检测Test元类, 有的话type会让元类代{过}{滤}理它创建Test类忽略我的猜测, 免得干扰思路 .
现在我们已经指定了Test这个类由Meta创建, 也就是Test已经是Meta的对象了, 通过打印语句验证一下
>>> print(type(Test))
<class '__main__.Meta'>
打印结果已经不再是<class type >了, 可以通过将metaclass=Meta 删去再打印验证. 会发现打印的是<class type >
既然会看元类, 应该都了解过__new__, __call__, __init__ . 没有的话阅读起来可能会有点懵.
这里注意一下, type的三大方法, new, call, init与object等其余class的三个是完全不同的. 但是意义差不多.
既然Test是Meta的对象, 那么Test什么时候被创建出来? 定义的完后 , 一个class定义完后下面就可以拿来用了, 所以当Test定义完后, 既然它是Meta的对象, 自然会调用Meta.__new__ 来创建Test这个类(也是Meta的对象), 那么废话不多说, 重写Meta的new方法.
class Meta(type):
def __new__(mcs, *args, **kwargs):
print("Meta.__new__ start")
return type.__new__(mcs, *args, **kwargs)
class Test(object, metaclass=Meta):
pass
直接运行发现打印出了Meta.__new__ , 我们在这里只定义了类和方法, 没有执行任何语句, 因此打印信息出现就代表着Meta的new被调用了. 就是在Test定义的时候, 解释器会帮我们调用它的元类去创建它. 那么第二个问题来了, new中的参数分别代表着什么呢? 打印一下就知道了
class Meta(type):
def __new__(mcs, *args, **kwargs):
print(mcs)
print(args)
print(kwargs)
return type.__new__(mcs, *args, **kwargs)
class Test(object, metaclass=Meta):
pass
直接运行, 发现是
<class '__main__.Meta'>
('Test', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Test'})
{}
mcs表示这Meta这个元类,
args是一个元素有三个, 分别是'Test', object, 和一个字典
没有关键字参数.
这里就直接说args了, 第一个Test类的名字, 第二个是Test类的父类, 第三个是Test类的命名空间. 就是它的一些属性, 可以通过类名+. 调用, 而且这三个对于一个类来说是必须参数, 那么下面我们把Meta.__new__ 参数简化一点.
class Meta(type):
def __new__(mcs, name, bases, namespaces, **kwargs):
print(kwargs)
return type.__new__(mcs, name, bases, namespaces)
这里type.__new__ 不能传入**kwargs, 因为type.__new__ 只有4个参数, 关于这个kwargs可以暂时忽略干嘛用的, 只有自定义元类才可以加这个参数.
-
第二个问题, type.__new__ 返回的是什么东西? 很简单, 打印一下就可以了, 贴代码
class Meta(type):
def __new__(mcs, name, bases, namespaces, **kwargs):
cls = type.__new__(mcs, name, bases, namespaces)
print(cls)
return cls
class Test(object, metaclass=Meta):
pass
下面是运行结果
<class '__main__.Test'>
竟然是Test类, 那就代表着type.__new__ 通过传入的name, bases, namespaces来创建了一个类. 关于第参数一个mcs, 就是我猜测最终都是由type来为一个类指定元类去创建的原因(忽略).
那么为Test类定义一些类属性和方法, 看看能不能在Meta.__new__中去调用一下
class Meta(type):
def __new__(mcs, name, bases, namespaces, **kwargs):
cls = type.__new__(mcs, name, bases, namespaces)
print(cls.a)
cls.print()
return cls
class Test(object, metaclass=Meta):
a = 1
@classmethod
def print(cls):
print("我是<class `%s`>" % cls.__name__)
发现被正常调用
1
我是<class `Test`>
既然元类可以在类创建之前进行拦截, 如果我们Meta.__new__ 中把参数namespaces拦截并修改, 比如加一个叫做__init__ 的方法, 而Test中不定义__init__ , 那么当用Test创建实例后会自动调用__init__ 吗? 贴代码
def init(self):
print("我是<class `%s`>的实例%s" % (type(self), self))
class Meta(type):
def __new__(mcs, name, bases, namespaces, **kwargs):
namespaces['__init__'] = init
cls = type.__new__(mcs, name, bases, namespaces)
return cls
class Test(object, metaclass=Meta):
pass
Test()
发现打印如下
我是<class `<class '__main__.Test'>`>的实例<__main__.Test object at 0x000002CCED4E7640>
那就代表可以在创建之前拦截类并为他添加一些方法属性, 甚至嵌入你想要的任何操作.
__new__ 到这里也差不多了, 至于拦截后可以干什么就要看具体需求了,比如 通过__new__ 你可以简单实现如下功能, 定义某一各类, 如果这个类由一个方法是大写开头的, 直接抛出异常说不符合代码规范. 然后终止程序.
-
关于type.__init__ , 其实这个和正常的__init__ 做的事情差不多, 也是来初始化对象的(也是类), 不过要注意的事它只接受四个参数
class Meta(type):
def __new__(mcs, name, bases, namespaces, **kwargs):
namespaces['__init__'] = init
cls = type.__new__(mcs, name, bases, namespaces)
return cls
def __init__(cls, name, bases, namespaces, **kwargs):
type.__init__(cls, name, bases, namespaces)
class Test(object, metaclass=Meta):
pass
关于**kwargs一样, 只有自定义元类可以接受这个参数, __init__ 可以接受的参数和__new__几乎一样 , 唯一不同的是第一个参数, new中传入的是元类Meta, init中传入的事new中返回的创建好的cls(也就是Test类), 在init中进行初始化合情合理. __init__ 差不多就这里了, 可以通过哪些参数做想要为Test类进行初始化的操作
-
关于type.__call__ , 这个就负责在创建对象的时候进行一些调度了
如果Test定义了__init__方法和__new__ , 这些方法为什么会被调用, 是谁帮我们调用的, 这个其实是type.__call__ 帮我们调用了这两个方法,
了解过__call__ 方法应该可以理解下面的话, 假设一个类Test, 创建实例了, test = Test(), 这里问: 为什么Test可以加括号? 就是因为Test这个类(type的对象)的类(也就是type)定义__call__ 方法, 使得type的对象(也就是Test类)可以进行加括号调用, 说了这么多, 下面上代码
class Meta(type):
def __call__(cls, *args, **kwargs):
print("Meta.__call__")
return type.__call__(cls, *args, **kwargs) # 仅仅是加一个打印语句, 原来的type返回什么我们不去碰它
class Test(object, metaclass=Meta):
pass
t = Test()
这里有个 有意思的地方, 如果直接运行, 会打印出来Meta.__call__ , 我们似乎并没有执行什么函数或者方法, 仅仅是定义了一个类, 实例化了一个对象, 而这个对象也并没有__init__和__new__方法 . 我们只在Meta.__call__ 中有过打印语句而已, 尝试删掉
t = Test() , 会发现不会打印了, 那代表着在实例化Test()调用了Meta.__call__ 方法.
那么第二个问题, __call__ 传入的参数是什么, 打印一下就知道了.
class Meta(type):
def __call__(cls, *args, **kwargs):
print(cls)
print(args)
print(kwargs)
return type.__call__(cls, *args, **kwargs) # 仅仅是加一个打印语句, 原来的type返回什么我们不去碰它
class Test(object, metaclass=Meta):
pass
Test(1, 2, 3, 4, a=2, b=3)
打印结果
<class '__main__.Test'>
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Traceback (most recent call last):
File "F:/PycharmProjects/Test/meta_test/meta.py", line 16, in <module>
Test(1, 2, 3, 4, a=2, b=3)
File "F:/PycharmProjects/Test/meta_test/meta.py", line 9, in __call__
return type.__call__(cls, *args, **kwargs)
TypeError: Test() takes no arguments
其实args和kwargs就是在实例化的时候传入的位置参数和关键字参数, 第一个cls就是代表着Test类. 后面报错因为Test并没有定义
init方法去接受参数. 而在Meta.__call__ 中并没有显示的调用Test.__init__ 啊, 那么久只能在type.__call__ type帮我们干了这些事情, 帮我们自动调用__init__, 和__new__ 等, 那么我们拦截args, kwargs, 不让它传入到type.__call__ 中, 是不是就可以不会报错了呢? 代码
class Meta(type):
def __call__(cls, *args, **kwargs):
print(cls)
print(args)
print(kwargs)
return type.__call__(cls) # 移除了参数
class Test(object, metaclass=Meta):
pass
Test(1, 2, 3, 4, a=2, b=3)
输出
<class '__main__.Test'>
(1, 2, 3, 4)
{'a': 2, 'b': 3}
果然没有报错, 我们在call中拦截了传给init的参数, 就不会导致参数不符合情况了
下一步通过print确认一下是不是
class Meta(type):
def __call__(cls, *args, **kwargs):
print("Meta.__call__ start")
print(args)
print(kwargs)
obj = type.__call__(cls, *args, **kwargs) # 恢复之前的参数
print("Meta.__call__ end")
return obj
class Test(object, metaclass=Meta):
def __new__(cls, *args, **kwargs):
print("Test.__new__ start")
print(args)
print(kwargs)
self = object.__new__(cls)
print("Test.__new__ end")
return self
def __init__(self, *args, **kwargs):
print("Test.__init__ start")
print(args)
print(kwargs)
print("Test.__init__ end")
test_obj = Test(1, 2, 3, 4, a=2, b=3)
打印结果
Meta.__call__ start
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Test.__new__ start
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Test.__new__ end
Test.__init__ start
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Test.__init__ end
Meta.__call__ end
很清晰的可以看到, 在type.__call__中, 首先帮我们调用了Test__new__, 然后才帮我们调用的Test.__init__ , 注意最后是Test.__init__ end 后, 才到Meta.__call__ end , 所以test_obj 接收的是Meta.__call__中返回的obj, 并不是Test.__new__返回的self, 虽然在这里他们都一样, 并且都是同一个对象和地址. , 那么我在Meta.__call__ 返回一个随意的对象, test_object接收的可不再是Test的实例的, 甚至于Test毫无关系, 代码
class Meta(type):
def __call__(cls, *args, **kwargs):
print("Meta.__call__ start")
print(args)
print(kwargs)
obj = type.__call__(cls, *args, **kwargs) # 恢复之前的参数
print("Meta.__call__ end")
return Demo()
class Test(object, metaclass=Meta):
def __new__(cls, *args, **kwargs):
print("Test.__new__ start")
print(args)
print(kwargs)
self = object.__new__(cls)
print("Test.__new__ end")
return self
def __init__(self, *args, **kwargs):
print("Test.__init__ start")
print(args)
print(kwargs)
print("Test.__init__ end")
class Demo(object):
pass
test_object = Test(1, 2, 3, 4, a=2, b=3)
print("-" * 20)
print(test_object)
打印结果
Meta.__call__ start
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Test.__new__ start
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Test.__new__ end
Test.__init__ start
(1, 2, 3, 4)
{'a': 2, 'b': 3}
Test.__init__ end
Meta.__call__ end
--------------------
<__main__.Demo object at 0x0000024DFF1883A0>
返回的test_object是Demo对象.
到这来, 总共类的创建过程以及行为差不多就通了, 从类的创建拦截type.__new__ , 到给类初始化type.__init__ . 到控制类产生对象type.__call__
2. 关于**kwargs
-
上面一直说只有自定义元类可以接收**kwargs , 但是这东西是怎么来的, 看下面代码
class Meta(type):
def __new__(mcs, name, bases, namespaces, **kwargs):
print(kwargs)
return type.__new__(mcs, name, bases, namespaces)
kws = {"a": 1, "b": lambda: 1}
class Test(object, metaclass=Meta, _root=True, **kws):
pass
打印结果
{'_root': True, 'a': 1, 'b': <function <lambda> at 0x000002AFAE146280>}
其实就是接收在类定义后面的括号里面给的关键字参数而已. 和函数用法相同, 不过metaclass这些有特殊意义的关键字参数并没有被接收, 可能是被type拦截了.
3. 关于__init_subclass__ 和 bound method
-
__init_subclass__ 可以在执行一些简单的拦截类的行为使用, 可以无须自定义元类.
-
bound method 一个函数真正绑定在一个对象身上成为方法, 从而省略传入第一个参数, 是在type.__call__ 中完成的, 在这之前其实那些实例方法都只是函数, 比如
class Demo(object):
def f(self):
print(self)
class Test(object):
def f(self, x, y):
self.f()
print(x, y)
Test.f(Demo(), 1, 2)
输出
<__main__.Demo object at 0x0000026F0C827460>
1 2
在没有被绑定之前完全就是可以当一个函数使用.
-
具体的下次有机会在记一记吧.
更正 isinstance(a, int)
isinstance(obj, type_tuple) 是判断obj是type_tuple中的实例的, 父类也可以, 因此这里isinstance(int, int)是 False, 是我的失误.
那么应该推荐用type(a) is int, 至于为什么不推荐type(a) == int, 这里也拓展一下吧, 因为用is判断是判断内存地址, 而 == 是调用__eq__ , 我们这里的需求是a的类型就是int, 在这里无论==还是int都没问题, 毕竟我们不会去更改int这个内置的类, 如果是我们自己写的类或者一些三方库, 且__eq__ 被重写了, 可能会发生一些预期之外的结果. 比如.
class Meta(type):
def __eq__(cls, other):
return cls is not other
class Test(object, metaclass=Meta):
pass
print(Test == Test)
print(Test is Test)
输出
False
True
是不是很意外? 因为type(t) == Test会去调用Test类的类 (就是Meta)的__eq__ 方法, 返回的自然是False, 但是用is只比较内存地址, 而每个类只有唯一的一个地址, 那么就一定为True, 而且效率也高于==, 因==还回去调用函数, 如果函数里面还有复杂的操作, 那么效率更慢.
PS: __eq__ 如果定义在Test中, 那么这个eq只在Test生成的对象作==比较的时候才会被调用, 不要混淆eq定义在Meta里面和Test里面.
|
免费评分
-
查看全部评分
|