1.【Java原理系列】 Java可序列化接口Serializable原理全面用法示例源码分析
2.pyc文件是怎么创建的?
【Java原理系列】 Java可序列化接口Serializable原理全面用法示例源码分析
实现Serializable接口的类表示该类可以进行序列化。未实现此接口的类将不会被序列化或反序列化。所有实现Serializable接口的子类也是可序列化的。这个序列化接口没有方法或字段,仅用于标识可序列化的语义。
为了使非可序列化的1990封神榜修复版源码类的子类能够进行序列化,子类需要承担保存和恢复父类的公共、受保护以及(如果可访问)包级字段状态的责任。只有当扩展的类具有可访问的无参构造函数来初始化类的状态时,子类才能承担这种责任。如果不满足这个条件,则声明类为可序列化是错误的,错误会在运行时被检测到。
在反序列化过程中,非可序列化类的字段将使用类的公共或受保护的无参构造函数进行初始化。无参构造函数必须对可序列化的子类可访问。可序列化子类的字段将从流中恢复。
在遍历图形结构时,可能会遇到不支持Serializable接口的对象。在这种情况下,将抛出NotSerializableException异常,并标识非可序列化对象的类。
实现Serializable接口的类需要显式指定自己的serialVersionUID,以确保在不同的java编译器实现中获得一致的值。如果未显式声明serialVersionUID,则序列化运行时会根据类的各个方面计算出一个默认的serialVersionUID值。
在使用Serializable接口时,有一些注意事项需要注意。xdqd分时指标源码例如,writeObject方法适用于以下场景:在覆写writeObject方法时,必须调用out.defaultWriteObject()来使用默认的序列化机制将对象的非瞬态字段写入输出流。只有在确实需要自定义序列化行为或保存额外的字段时,才需要覆写writeObject方法。
可以使用Externalizable接口替代Serializable接口,以实现更细粒度的控制,但需要更多的开发工作。Externalizable接口允许在序列化时指定额外的字段,但需要在类中实现writeExternal和readExternal方法。
序列化和反序列化的过程是通过ObjectOutputStream和ObjectInputStream来完成的。可以使用这两个类的writeObject和readObject方法来手动控制序列化和反序列化的过程。
序列化示例:定义了一个Person类,并实现了Serializable接口。Person类有两个字段:name和age。age字段使用了transient关键字修饰,表示该字段不会被序列化。在main方法中,创建了一个Person对象并将其序列化到文件中。从文件中读取序列化的数据,并使用强制类型转换将其转换为Person对象。输出原始的person对象和恢复后的对象,验证序列化和反序列化的结果。
序列化兼容性示例:在类进行了修改后,可以通过显式声明serialVersionUID来解决之前序列化的对象无法被正确反序列化的问题。
加密和验证示例:在进行网络传输或持久化存储时,可以使用加密算法对序列化的撑压线源码数据进行加密,或使用数字签名来验证数据的完整性。
自定义序列化行为示例:如果需要对对象的状态进行特殊处理,或以不同于默认机制的方式序列化对象的字段,可以通过覆写writeObject方法来控制序列化过程。
使用Externalizable接口的示例:定义一个类,实现Externalizable接口,并在类中实现writeExternal和readExternal方法,用于保存和恢复额外的字段。
序列化和反序列化的源码分析:序列化示例中的writeObject方法用于将指定的对象写入ObjectOutputStream中进行序列化。而readObject方法用于从ObjectInputStream中读取一个对象进行反序列化。
序列化和反序列化的核心代码段展示了如何在序列化和反序列化过程中处理对象的类、类的签名以及类和其所有超类的非瞬态和非静态字段的值。确保了对象的完整恢复和验证过程的执行。
pyc文件是怎么创建的?
pyc文件的触发
前面我们提到,每一个代码块(code block)都会对应一个PyCodeObject对象,Python会将该对象存储在pyc文件中。但不幸的是,事实并不总是这样。有时,当我们运行一个简单的程序时并没有产生pyc文件,因此我们猜测:有些Python程序只是临时完成一些琐碎的工作,这样的程序仅仅只会运行一次,然后就不会再使用了,因此也就没有保存至pyc文件的必要。
如果我们在代码中加上了一个import abc这样的语句,再执行你就会发现Python为其生成了pyc文件,这就说明import会触发pyc的宁波狙击点源码生成。
实际上,在运行过程中,如果碰到import abc这样的语句,那么Python会在设定好的path中寻找abc.pyc或者abc.pyd文件。如果没有这些文件,而是只发现了abc.py,那么Python会先将abc.py编译成PyCodeObject,然后创建pyc文件,并将PyCodeObject写到pyc文件里面去。
接下来,再对abc.pyc进行import动作,对,并不是编译成PyCodeObject对象之后就直接使用。而是先写到pyc文件里面去,然后再将pyc文件里面的PyCodeObject对象重新在内存中复制出来。
关于Python的import机制,我们后面会剖析,这里只是用来完成pyc文件的触发。当然得到pyc文件还有其它方法,比如使用py_compile模块。
#a.pyclassA:a=1#b.pyimporta执行b.py的时候,会发现创建了a.cpython-.pyc。另外关于pyc文件的创建位置,会在当前文件的同级目录下的__pycache__目录中创建,名字就叫做:py文件名.cpython-版本号.pyc。
pyc文件里面包含哪些内容上面我们提到,Python通过import module进行加载时,生产条码系统源码如果没有找到相应的pyc或者pyd文件,就会在py文件的基础上自动创建pyc文件。而创建之后,会往里面写入三个内容:
1. magic number
这是Python定义的一个整数值,不同版本的Python会定义不同的magic number,这个值是为了保证Python能够加载正确的pyc。
比如Python3.7不会加载3.6版本的pyc,因为Python在加载pyc文件的时候会首先检测该pyc的magic number,如果和自身的magic number不一致,则拒绝加载。
2. pyc的创建时间
这个很好理解,判断源代码的最后修改时间和pyc文件的创建时间。如果pyc文件的创建时间比源代码的修改时间要早,说明在生成pyc之后,源代码被修改了,那么会重新编译并生成新的pyc,而反之则会直接加载已存在的pyc。
3. PyCodeObject对象
这个不用说了,肯定是要存储的。
pyc文件的写入下面就来看看pyc文件是如何写入上面三个内容的。
既然要写入,那么肯定要有文件句柄,我们来看看:
//位置:Python/marshal.c//FILE是C自带的文件句柄//可以把WFILE看成是FILE的包装typedefstruct{ FILE*fp;//文件句柄//下面的字段在写入信息的时候会看到interror;intdepth;PyObject*str;char*ptr;char*end;char*buf;_Py_hashtable_t*hashtable;intversion;}WFILE;首先是写入magic number和创建时间,它们会调用PyMarshal_WriteLongToFile函数进行写入:
voidPyMarshal_WriteLongToFile(longx,FILE*fp,intversion){ //magicnumber和创建时间,只是一个整数//在写入的时候,使用char[4]来保存charbuf[4];//声明一个WFILE类型变量wfWFILEwf;//内存初始化memset(&wf,0,sizeof(wf));//初始化内部成员wf.fp=fp;wf.ptr=wf.buf=buf;wf.end=wf.ptr+sizeof(buf);wf.error=WFERR_OK;wf.version=version;//调用w_long将x、也就是版本信息或者时间写到wf里面去w_long(x,&wf);//刷到磁盘上w_flush(&wf);}所以该函数只是初始化了一个WFILE对象,真正写入则是调用的w_long。
staticvoidw_long(longx,WFILE*p){ w_byte((char)(x&0xff),p);w_byte((char)((x>>8)&0xff),p);w_byte((char)((x>>)&0xff),p);w_byte((char)((x>>)&0xff),p);}w_long则是调用 w_byte 将 x 逐个字节地写到文件里面去。
而写入PyCodeObject对象则是调用了PyMarshal_WriteObjectToFile,我们也来看看长什么样子。
voidPyMarshal_WriteObjectToFile(PyObject*x,FILE*fp,intversion){ charbuf[BUFSIZ];WFILEwf;memset(&wf,0,sizeof(wf));wf.fp=fp;wf.ptr=wf.buf=buf;wf.end=wf.ptr+sizeof(buf);wf.error=WFERR_OK;wf.version=version;if(w_init_refs(&wf,version))return;/*callermushcheckPyErr_Occurred()*/w_object(x,&wf);w_clear_refs(&wf);w_flush(&wf);}可以看到和PyMarshal_WriteLongToFile基本是类似的,只不过在实际写入的时候,PyMarshal_WriteLongToFile调用的是w_long,而PyMarshal_WriteObjectToFile调用的是w_object。
staticvoidw_object(PyObject*v,WFILE*p){ charflag='\0';p->depth++;if(p->depth>MAX_MARSHAL_STACK_DEPTH){ p->error=WFERR_NESTEDTOODEEP;}elseif(v==NULL){ w_byte(TYPE_NULL,p);}elseif(v==Py_None){ w_byte(TYPE_NONE,p);}elseif(v==PyExc_StopIteration){ w_byte(TYPE_STOPITER,p);}elseif(v==Py_Ellipsis){ w_byte(TYPE_ELLIPSIS,p);}elseif(v==Py_False){ w_byte(TYPE_FALSE,p);}elseif(v==Py_True){ w_byte(TYPE_TRUE,p);}elseif(!w_ref(v,&flag,p))w_complex_object(v,flag,p);p->depth--;}可以看到本质上还是调用了w_byte,但这仅仅是一些特殊的对象。如果是列表、字典之类的数据,那么会调用w_complex_object,也就是代码中的最后一个else if分支。
w_complex_object这个函数的源代码很长,我们看一下整体结构,具体逻辑就不贴了,我们后面会单独截取一部分进行分析。
staticvoidw_complex_object(PyObject*v,charflag,WFILE*p){ Py_ssize_ti,n;//如果是整数的话,执行整数的写入逻辑if(PyLong_CheckExact(v)){ //......}//如果是浮点数的话,执行浮点数的写入逻辑elseif(PyFloat_CheckExact(v)){ if(p->version>1){ //......}else{ //......}}//如果是复数的话,执行复数的写入逻辑elseif(PyComplex_CheckExact(v)){ if(p->version>1){ //......}else{ //......}}//如果是字节序列的话,执行字节序列的写入逻辑elseif(PyBytes_CheckExact(v)){ //......}//如果是字符串的话,执行字符串的写入逻辑elseif(PyUnicode_CheckExact(v)){ if(p->version>=4&&PyUnicode_IS_ASCII(v)){ //......}else{ //......}}else{ //......}}//如果是元组的话,执行元组的写入逻辑elseif(PyTuple_CheckExact(v)){ //......}//如果是列表的话,执行列表的写入逻辑elseif(PyList_CheckExact(v)){ //......}//如果是字典的话,执行字典的写入逻辑elseif(PyDict_CheckExact(v)){ //......}//如果是集合的话,执行集合的写入逻辑elseif(PyAnySet_CheckExact(v)){ //......}//如果是PyCodeObject对象的话//执行PyCodeObject对象的写入逻辑elseif(PyCode_Check(v)){ //......}//如果是Buffer的话,执行Buffer的写入逻辑elseif(PyObject_CheckBuffer(v)){ //......}else{ W_TYPE(TYPE_UNKNOWN,p);p->error=WFERR_UNMARSHALLABLE;}}源代码虽然长,但是逻辑非常单纯,就是对不同的对象、执行不同的写动作,然而其最终目的都是通过w_byte写到pyc文件中。了解完函数的整体结构之后,我们再看一下具体细节,看看它在写入对象的时候到底写入了哪些内容?
staticvoidw_complex_object(PyObject*v,charflag,WFILE*p){ //......elseif(PyList_CheckExact(v)){ W_TYPE(TYPE_LIST,p);n=PyList_GET_SIZE(v);W_SIZE(n,p);for(i=0;i<n;i++){ w_object(PyList_GET_ITEM(v,i),p);}}elseif(PyDict_CheckExact(v)){ Py_ssize_tpos;PyObject*key,*value;W_TYPE(TYPE_DICT,p);/*ThisoneisNULLobjectterminated!*/pos=0;while(PyDict_Next(v,&pos,&key,&value)){ w_object(key,p);w_object(value,p);}w_object((PyObject*)NULL,p);}//......}以列表和字典为例,它们在写入的时候实际上写的是内部的元素,其它对象也是类似的。
deffoo():lst=[1,2,3]#把列表内的元素写进去了print(foo.__code__.co_consts)#(None,1,2,3)但问题来了,如果只是写入元素的话,那么Python在加载的时候怎么知道它是一个列表呢?所以在写入的时候不能光写数据,类型信息也要写进去。我们再看一下上面列表和字典的写入逻辑,里面都调用了W_TYPE,它负责将类型信息写进去。
因此无论对于哪种对象,在写入具体数据之前,都会先调用W_TYPE将类型信息写进去。如果没有类型信息,那么当Python加载pyc文件的时候,只会得到一坨字节流,而无法解析字节流中隐藏的结构和蕴含的信息。
所以在往pyc文件里写入数据之前,必须先写入一个标识,诸如TYPE_LIST、TYPE_TUPLE、TYPE_DICT等等,这些标识正是对应的类型信息。
如果解释器在pyc文件中发现了这样的标识,则预示着上一个对象结束,新的对象开始,并且也知道新对象是什么样的对象,从而也知道该执行什么样的构建动作。当然,这些标识也是可以看到的,在底层已经定义好了。
//marshal.c#defineTYPE_NULL'0'#defineTYPE_NONE'N'#defineTYPE_FALSE'F'#defineTYPE_TRUE'T'#defineTYPE_STOPITER'S'#defineTYPE_ELLIPSIS'.'#defineTYPE_INT'i'/*TYPE_INTisnotgeneratedanymore.Supportedforbackwardcompatibilityonly.*/#defineTYPE_INT'I'#defineTYPE_FLOAT'f'#defineTYPE_BINARY_FLOAT'g'#defineTYPE_COMPLEX'x'#defineTYPE_BINARY_COMPLEX'y'#defineTYPE_LONG'l'#defineTYPE_STRING's'#defineTYPE_INTERNED't'#defineTYPE_REF'r'#defineTYPE_TUPLE'('#defineTYPE_LIST'['#defineTYPE_DICT'{ '#defineTYPE_CODE'c'#defineTYPE_UNICODE'u'#defineTYPE_UNKNOWN'?'#defineTYPE_SET'<'#defineTYPE_FROZENSET'>'到了这里可以看到,其实Python对PyCodeObject对象的导出实际上是不复杂的。因为不管什么对象,最后都为归结为两种简单的形式,一种是数值写入,一种是字符串写入。
上面都是对数值的写入,比较简单,仅仅需要按照字节依次写入pyc即可。然而在写入字符串的时候,Python设计了一种比较复杂的机制,有兴趣可以自己阅读源码,这里不再介绍。
PyCodeObject的包含关系有下面一个文件:
//位置:Python/marshal.c//FILE是C自带的文件句柄//可以把WFILE看成是FILE的包装typedefstruct{ FILE*fp;//文件句柄//下面的字段在写入信息的时候会看到interror;intdepth;PyObject*str;char*ptr;char*end;char*buf;_Py_hashtable_t*hashtable;intversion;}WFILE;0显然编译之后会创建三个PyCodeObject对象,但是有两个PyCodeObject对象是位于另一个PyCodeObject对象当中的。
也就是foo和A对应的PyCodeObject对象,位于模块对应的PyCodeObject对象当中,准确的说是位于co_consts指向的常量池当中。举个栗子:
//位置:Python/marshal.c//FILE是C自带的文件句柄//可以把WFILE看成是FILE的包装typedefstruct{ FILE*fp;//文件句柄//下面的字段在写入信息的时候会看到interror;intdepth;PyObject*str;char*ptr;char*end;char*buf;_Py_hashtable_t*hashtable;intversion;}WFILE;1我们看到f2对应的PyCodeObject确实位于f1的常量池当中,准确的说是f1的常量池中有一个指针指向f2对应的PyCodeObject。
不过这都不是重点,重点是PyCodeObject对象是可以嵌套的。当在一个作用域内部发现了一个新的作用域,那么新的作用域对应的PyCodeObject对象会位于外层作用域的PyCodeObject对象的常量池中,或者说被常量池中的一个指针指向。
而在写入pyc的时候会从最外层、也就是模块的PyCodeObject对象开始写入。如果碰到了包含的另一个PyCodeObject对象,那么就会递归地执行写入新的PyCodeObject对象。
如此下去,最终所有的PyCodeObject对象都会写入到pyc文件当中。因此pyc文件里的PyCodeObject对象也是以一种嵌套的关系联系在一起的,和代码块之间的关系是保持一致的。
//位置:Python/marshal.c//FILE是C自带的文件句柄//可以把WFILE看成是FILE的包装typedefstruct{ FILE*fp;//文件句柄//下面的字段在写入信息的时候会看到interror;intdepth;PyObject*str;char*ptr;char*end;char*buf;_Py_hashtable_t*hashtable;intversion;}WFILE;2这里问一下,上面那段代码中创建了几个PyCodeObject对象呢?
答案是6个,首先模块是一个,foo函数一个,bar函数一个,类A一个,类A里面的foo函数一个,类A里面的bar函数一个,所以一共是6个。
而且这里的PyCodeObject对象是层层嵌套的,一开始是对整个全局模块创建PyCodeObject对象,然后遇到了函数foo,那么再为函数foo创建PyCodeObject对象,依次往下。
所以,如果是常量值,则相当于是静态信息,直接存储起来便可。可如果是函数、类,那么会为其创建新的PyCodeObject对象,然后再收集起来。
小结以上就是pyc文件相关的内容,源文件在编译之后会得到pyc文件。因此我们不光可以手动导入 pyc,用Python直接执行pyc文件也是可以的。
以上就是本次分享的所有内容,想要了解更多欢迎前往公众号:Python编程学习圈,每日干货分享