zoukankan      html  css  js  c++  java
  • Python虚拟机类机制之自定义class(四)

    用户自定义class

    在本章中,我们将研究对用户自定义class的剖析,在demo1.py中,我们将研究单个class的实现,所以在这里并没有关于继承及多态的讨论。然而在demo1.py中,我们看到了许多类的内容,其中包括类的定义、类的构造函数、对象的实例化、类成员函数的调用等

    demo1.py

    class A(object):
        name = "Python"
     
        def __init__(self):
            print("A::__init__")
     
        def f(self):
            print("A::f")
     
        def g(self, aValue):
            self.value = aValue
            print(self.value)
     
     
    a = A()
    a.f()
    a.g(10)
    

      

    我们都知道,对于一个包含函数定义的Python源文件,在Python源文件编译后,会得到一个与源文件对应的PyCodeObject对象A,而与函数对应的PyCodeObject对象B则存储在A的co_consts变量中。那么对于包含类的Python源文件,编译之后的结果又如何呢?

    >>> source = open("demo1.py").read()
    >>> co = compile(source, "demo1.py", "exec")
    >>> co.co_consts
    ('A', <code object A at 0x7f1048929dc8, file "demo1.py", line 1>, 10, None)
    >>> A_co = co.co_consts[1]
    >>> A_co.co_consts
    ('Python', <code object __init__ at 0x7f1048929648, file "demo1.py", line 4>, <code object f at 0x7f1048929918, file "demo1.py", line 7>, <code object g at 0x7f1048929af8, file "demo1.py", line 10>)
    >>> A_co.co_names
    ('__name__', '__module__', 'name', '__init__', 'f', 'g')
    

      

    可以看到,class A会编译成一个PyCodeObject,存放在源文件code的co_consts变量中,而class A的函数也会编译成PyCodeObject,存放在对A对应的PyCodeObject中

    class的动态元信息

    所谓的class的元信息就是指关于class的信息,比如说class的名称,它所拥有的属性、方法、该class实例化时要为实例对象申请的内存空间大小等。对于demo1.py中所定义的class A来说,我们必须要知道这样的信息:class A中,有一个符号f,这个f对应了一个函数,还有一个符号g,也对应一个函数。有了这些关于A的元信息,才能创建A的class对象。元信息在编程语言中是一个非常重要的概念,正是有了这个东西,Java、C#的一些初级的诸如反射(Reflection)等动态特性才有可能得到实现。在以后的剖析中可以看到,Python中的元信息概念被发挥到淋漓尽致,因而Python也提供了Java、C#等语言所没有的高度灵活的动态性

    现在,我们可以解释一下demo1.py所对应的字节码,看一下关于class A的字节码是长什么样的?

    >>> source = open("demo1.py").read()
    >>> co = compile(source, "demo1.py", "exec")
    >>> import dis
    >>> dis.dis(co)
      1           0 LOAD_CONST               0 ('A')
                  3 LOAD_NAME                0 (object)
                  6 BUILD_TUPLE              1
                  9 LOAD_CONST               1 (<code object A at 0x7f91e6ec7dc8, file "demo1.py", line 1>)
                 12 MAKE_FUNCTION            0
                 15 CALL_FUNCTION            0
                 18 BUILD_CLASS         
                 19 STORE_NAME               1 (A)
    
     15          22 LOAD_NAME                1 (A)
                 25 CALL_FUNCTION            0
                 28 STORE_NAME               2 (a)
    
     16          31 LOAD_NAME                2 (a)
                 34 LOAD_ATTR                3 (f)
                 37 CALL_FUNCTION            0
                 40 POP_TOP             
    
     17          41 LOAD_NAME                2 (a)
                 44 LOAD_ATTR                4 (g)
                 47 LOAD_CONST               2 (10)
                 50 CALL_FUNCTION            1
                 53 POP_TOP             
                 54 LOAD_CONST               3 (None)
                 57 RETURN_VALUE  
    

      

    我们单独把class A相关的字节码指令提取出来

    0   LOAD_CONST               0 ('A')
    3   LOAD_NAME                0 (object)
    6   BUILD_TUPLE              1
    9   LOAD_CONST               1 (<code object A at 0x7f1048929dc8, file "demo1.py", line 1>)
    12  MAKE_FUNCTION            0
    15  CALL_FUNCTION            0
    18  BUILD_CLASS        
    19  STORE_NAME               1 (A)
    

      

    现在,我们可以开始分析class A是如何执行的了:

    首先执行"0 LOAD_CONST   0"指令将类A的名称压入到运行时栈中,而接下来的LOAD_NAME指令和BUILD_TUPPLE指令是一个非常关键的点,这两条指令将基于类A的所有基类创建一个基类列表,当然这里只有一个名为object的基类。随后,Python虚拟机通过"9 LOAD_CONST   1"指令将与A对应的PyCodeObject压入到运行时栈中,并通过MAKE_FUNCTION指令创建一个PyFunctiobObject对象。在这些操作完成之后,我们来看一看这时的运行时栈

    图1-1   MAKE_FUNCTION指令完成后的运行时栈

    之后,Python虚拟机开始执行"15 CALL_FUNCTION   0"指令。根据函数机制那一章的分析,我们知道调用CALL_FUNCTION会创建一个新的PyFrameObject对象,并开始执行这个PyFrameObject对象中所包含的字节码序列,很显然,这些字节码序列来自运行时栈中那个PyFunctiobObject对象。参考上面的描述,我们可以发现,这段字节码序列实际就是来自与A对应的PyCodeObject对象。换句话说,现在Python虚拟机所面对的目标从与demo1.py对应的字节码序列转换到与class A对应的字节码序列

    >>> dis.dis(A_co)
      1           0 LOAD_NAME                0 (__name__)
                  3 STORE_NAME               1 (__module__)
     
      2           6 LOAD_CONST               0 ('Python')
                  9 STORE_NAME               2 (name)
     
      4          12 LOAD_CONST               1 (<code object __init__ at 0x7f1048929648, file "demo1.py", line 4>)
                 15 MAKE_FUNCTION            0
                 18 STORE_NAME               3 (__init__)
     
      7          21 LOAD_CONST               2 (<code object f at 0x7f1048929918, file "demo1.py", line 7>)
                 24 MAKE_FUNCTION            0
                 27 STORE_NAME               4 (f)
     
     10          30 LOAD_CONST               3 (<code object g at 0x7f1048929af8, file "demo1.py", line 10>)
                 33 MAKE_FUNCTION            0
                 36 STORE_NAME               5 (g)
                 39 LOAD_LOCALS        
                 40 RETURN_VALUE 
    

      

    Python在执行源文件的CALL_FUNCTION中,实际上只执行了一个赋值语句和3个def语句,创建了3个PyFunctionObject对象

    开始的LOAD_NAME和STORE_NAME将符号__module__和全局名字空间中符号__name__对应的值__main__关联起来,并放入到local名字空间(PyFrameObject对象的f_locals)中。需要说明的是,这里的函数机制与之前有所不同,在前面执行"15 CALL_FUNCTION   0"指令,创建新的PyFrameObject对象时,PyFrameObject中的f_locals被创建了,并指向一个PyDictObject对象,而在函数机制中f_locals是被设置为NULL,函数机制中局部变量是以一种位置参数的形式存放在了运行时栈前面的那段内存

    接着,Python虚拟机连续执行3个(LOAD_CONST、MAKE_FUNCTION、STORE_NAME)指令序列对,每个指令序列都会创建一个与类中成员函数对应的PyFunctiobObject对象,通过STORE_NAME存入到local名字空间中

    回头想想,这一路下来,我们好像创建不少东西,但目前为止,创建的有用的东西都被放到local名字空间中,这里面存的恰恰是最重要的东西——class A的元信息

    现在有动态元信息,那么必然会有静态元信息。关于这二者的区别,后面还会讨论

    既然class A的动态元信息创建完毕,那我们是不是应该要拿到A的class对象?于是,我们开始后退,退出当前的class A的PyFrameObject,即栈帧,回到原先源文件所对应的PyFrameObject中,但是在回退之前,我们必须要把当前栈帧class A的f_locals带走,不然class A的动态元信息创建了等于没有创建,我们依旧不知道A有几个变量?有几个函数?

    ceval.c

    case LOAD_LOCALS:
        if ((x = f->f_locals) != NULL) {
            Py_INCREF(x);
            PUSH(x);
            continue;
        }
        PyErr_SetString(PyExc_SystemError, "no locals");
        break;
    

      

    LOAD_LOCALS将f_locals压入运行时栈中,随后的RETURN_VALUE指令将运行时栈的f_locals返回给上一级的栈帧。这时候,我们又回到CALL_FUNCTION,CALL_FUNCTION获得class A的f_locals后,将其压入运行时栈,现在的运行时栈如图1-2

    图1-2   CALL_FUNCTION指令完成后的运行时栈 

    我们可以在call_function的实现代码中打印PyDictObject对象的代码,以观察返回的对象,我们针对类名为A的class对象打印其返回的f_locals,可能会有人觉得有点奇怪,这里的类名居然是从一个函数中获取的?我们都知道,func是一个PyFuncObject类型的对象,可以通过PyEval_GetFuncName获取其函数的名字,但是在调用call_function时类名也保存在PyFuncObject中的func_name吗?是的,没错。但别忘了,既然class A语句的执行都是以调用函数的形式来生成,那为什么这个函数的函数名不能作为类名呢?

    static PyObject *call_function(PyObject ***pp_stack, int oparg)
    {
    	……
    	PyObject *func = *pfunc;
    	PyObject *x, *w;
    	……
    	while ((*pp_stack) > pfunc) {
    		w = EXT_POP(*pp_stack);
    		Py_DECREF(w);
    		PCALL(PCALL_POP);
    	}
    	//func_name即为类名,x即为上个栈帧所返回的f_locals
    	char *func_name = PyEval_GetFuncName(func);
    	if (strcmp(func_name, "A") == 0) {
    		PyObject *std = PySys_GetObject("stdout");
    		PyFile_WriteObject(x, std, Py_PRINT_RAW);
    		printf("
    ");
    	}
    	return x;
    }
    

      

    重新编译并在Python命令行执行如下class A,会进入之前我们特定的if分支中并打印f_locals

    >>> class A(object):
    ...     a = 1
    ...     d = {1: "Robert", 2: "Python"}
    ...     def f(self):
    ...         pass
    ...     def g(self, value):
    ...         pass
    ...
    {'a': 1, '__module__': '__main__', 'd': {1: 'Robert', 2: 'Python'}, 'g': <function g at 0x7fe2482a1230>, 'f': <function f at 0x7fe248297668>}
    

      

    可以看到,其返回的确实是class A的动态元信息

    在前面,Python虚拟机已经获得了关于class的属性表(动态元信息),那么在build_class中,这个动态元信息将作为methods出现在build_class函数的参数列表中。有一点值的注意的是,methods中并没有包含所有关于class的元信息,在methods中,只包含了在class中包含的属性和方法。从广义上来讲,方法也是一种属性,所以我们可以说,class的动态元信息中包含了class的所有属性

    static PyObject *
    build_class(PyObject *methods, PyObject *bases, PyObject *name)
    {
        PyObject *metaclass = NULL, *result, *base;
        //[1]:检查属性表中是否有指定的__metaclass__
        if (PyDict_Check(methods))
            metaclass = PyDict_GetItemString(methods, "__metaclass__");
        if (metaclass != NULL)
            Py_INCREF(metaclass);
        else if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
            //[2]:获得A的第一基类,object
            base = PyTuple_GET_ITEM(bases, 0);
            //[3]:获得object.__class__
            metaclass = PyObject_GetAttrString(base, "__class__");
            if (metaclass == NULL) {
                PyErr_Clear();
                metaclass = (PyObject *)base->ob_type;
                Py_INCREF(metaclass);
            }
        }
        else {
            ……
        }
        result = PyObject_CallFunctionObjArgs(metaclass, name, bases, methods, NULL);
        ……
        return result;
    }
    

      

    虽然我们虽然知道class的属性,但对于这个class对象的类型是什么,应该如何创建,要分配多少内存,却没有任何信息。在build_class中,metaclass正是关于class对象的另一部分元信息,我们称为静态元信息。在静态元信息中,隐藏着所有的class对象应该如何创建的信息,注意,我们这里说的是所有的class对象

    在build_class中,包含了为classic class和new style class确定metaclass的过程,当然,这里我们只考虑new style class确定metaclass的过程

    如果用户没有指定,Python虚拟机会选择class的第一基类的type作为该class的metaclass。对于这里的A来说,其第一基类为object,而我们已经知道object.__class__为<type 'type'>。所以最终获得的metaclass为<type 'type'>这个class对象

    对于PyIntObject、PyDictObject这些对象,其所有的元信息都包含在其对应的类型对象中。而为什么关于一个class对象的所有元信息不能包含在其自身当中,却要分离为两部分呢?因为用户会在源文件中定义不同的class,其所包含的属性肯定是不同的,这就决定了只能使用动态机制来保存class的属性,这个元信息只能是动态的,所以我们称为动态元信息,即我们看到的参数methods,而对于所有的class都可能共用的元信息,比如class对象的type和class对象的创建策略,这些则存放在了class对象的metaclasss中

    PyIntObject、PyDictObject这些对象是Python静态提供的,它们都具有相同的接口集合,当然,有的对象可能是不支持某个接口,但不影响它的所有元信息可以完全存储在其类型对象中:而用户自定义的class对象,其接口集合是动态的,不可能在metaclass中静态指定,如图1-3展示了多个class对象和元信息的关系

    图1-4   class对象与元信息之间的关系

    如果对动态元信息和静态元信息还有不理解的同学可以这样想:现在我们有教师类Teacher和厨师类Chief这两个类,教师拥有教书育人的能力,厨师拥有烹饪佳肴的能力,这是两个类的动态元信息,两个类的能力不同,但相同的是教师和厨师都是人,都是两个眼睛一张嘴,那么这些公共的信息就是静态元信息,因为教师和厨师都是人,如果我们有创造人的能力,一定是基于两个眼睛一张嘴的限定来创造,不然就是别的物种了

    调用metaclass完成class对象的创建

    在了解class对象的创建之前,我们再回顾一下PyType_Type

    typeobject.c

    PyTypeObject PyType_Type = {
    	PyObject_HEAD_INIT(&PyType_Type)
    	……
    	(ternaryfunc)type_call,			/* tp_call */
    	……
    	type_new,				/* tp_new */
    	……
    };
    

      

    tp_call、tp_new在创建class对象中,起着至关重要的作用,同时PyObject_HEAD_INIT(&PyType_Type)这行代码代表,PyType_Type->ob_type指向的是其自身

    现在,我们开始解析如何创建class对象。在获得了metaclass之后,build_class通过PyObject_CallFunctionObjArgs函数完成“调用metaclass”的动作,从而完成class对象的创建。之前说过,Python中一个对象是否可调用,要看其是否定义了tp_call,当调用一个对象时,会将对象传入PyObject_Call函数,这个函数中会调用其对象的tp_call,从而完成对象的调用。很幸运,PyType_Type定义了tp_call,这说明PyType_Type是一个可调用的对象

    现在问题来了,一个Python程序中class对象可能成千上万,而PyType_Type却只有一个,这一个PyType_Type如何创建出不同的class对象呢?其中的奥妙则集中之前我们所看到的PyObject_CallFunctionObjArgs函数的几个参数中,这几个参数分别是class的类名、基类列表和属性表,在PyObject_CallFunctionObjArgs中,这几个参数会被打包到一个tupple对象中,最终进入PyObject_Call函数,现在,让我们进入到创建class对象处:

    //object.h
    typedef PyObject * (*ternaryfunc)(PyObject *, PyObject *, PyObject *);
     
    //abstract.c
    PyObject * PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw)
    {
        //arg即是PyObject_CallFunctionObjArgs中打包的tupple对象
        ternaryfunc call;
     
        if ((call = func->ob_type->tp_call) != NULL) {
            PyObject *result = (*call)(func, arg, kw);
            if (result == NULL && !PyErr_Occurred())
                PyErr_SetString(
                    PyExc_SystemError,
                    "NULL result without error in PyObject_Call");
            return result;
        }
        PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable",
                 func->ob_type->tp_name);
        return NULL;
    }
    

      

    最终,由于PyType_Type的ob_type还是指向PyType_Type,所以最终将调用到PyType_Type中定义的tp_call操作。下面,来看一下PyType_Type的tp_call操作:

    static PyObject *
    type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
    {
        PyObject *obj;
     
        if (type->tp_new == NULL) {
            PyErr_Format(PyExc_TypeError,
                     "cannot create '%.100s' instances",
                     type->tp_name);
            return NULL;
        }
     
        obj = type->tp_new(type, args, kwds);
        //如果创建的是实例对象,则调用__init__进行初始化
        if (obj != NULL) {
            if (type == &PyType_Type &&
                PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
                (kwds == NULL ||
                 (PyDict_Check(kwds) && PyDict_Size(kwds) == 0)))
                return obj;
            if (!PyType_IsSubtype(obj->ob_type, type))
                return obj;
            type = obj->ob_type;
            if (PyType_HasFeature(type, Py_TPFLAGS_HAVE_CLASS) &&
                type->tp_init != NULL &&
                type->tp_init(obj, args, kwds) < 0) {
                Py_DECREF(obj);
                obj = NULL;
            }
        }
        return obj;
    }
    

      

    PyType_Type中的tp_new指向tp_new,而这个tp_new才是class对象创建的地方

    typeobject.c

    static PyObject * type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
    {
        //metatype是PyType_Type(<type 'type'>),args中包含了(类名、基类列表、属性表)
        PyObject *name, *bases, *dict;
        static char *kwlist[] = {"name", "bases", "dict", 0};
        PyObject *slots, *tmp, *newslots;
        PyTypeObject *type, *base, *tmptype, *winner;
        PyHeapTypeObject *et;
        PyMemberDef *mp;
        Py_ssize_t i, nbases, nslots, slotoffset, add_dict, add_weak;
        int j, may_add_dict, may_add_weak;
     
        ……
        //将args中的(类名、基类列表、属性表)分别解析到name、bases、dict三个变量中
        if (!PyArg_ParseTupleAndKeywords(args, kwds, "SO!O!:type", kwlist,
                         &name,
                         &PyTuple_Type, &bases,
                         &PyDict_Type, &dict))
            return NULL;
    	……
        //确定最佳metaclass,存储在PyObject *metatype中
    	winner = metatype;
    	for (i = 0; i < nbases; i++) {
    		tmp = PyTuple_GET_ITEM(bases, i);
    		tmptype = tmp->ob_type;
    		if (tmptype == &PyClass_Type)
    			continue; /* Special case classic classes */
    		if (PyType_IsSubtype(winner, tmptype))
    			continue;
    		if (PyType_IsSubtype(tmptype, winner)) {
    			winner = tmptype;
    			continue;
    		}
    		PyErr_SetString(PyExc_TypeError,
    				"metaclass conflict: "
    				"the metaclass of a derived class "
    				"must be a (non-strict) subclass "
    				"of the metaclasses of all its bases");
    		return NULL;
    	}
    	if (winner != metatype) {
    		if (winner->tp_new != type_new) /* Pass it to the winner */
    			return winner->tp_new(winner, args, kwds);
    		metatype = winner;
    	}
    	……
        //确定最佳base,存储在PyObject *base中
    	base = best_base(bases);
    	……
        //为class对象申请内存,尽管PyType_Type的tp_alloc为0,但PyBaseObject_Type的tp_alloc为PyType_GenericAlloc,在PyType_Ready中被继承了,创建的内存大小为tp_basicsize + tp_itemsize
        type = (PyTypeObject *)metatype->tp_alloc(metatype, nslots);
        et = (PyHeapTypeObject *)type;
        et->ht_name = name;
        //设置PyTypeObject中的各个域
        type->tp_as_number = &et->as_number;
        type->tp_as_sequence = &et->as_sequence;
        type->tp_as_mapping = &et->as_mapping;
        type->tp_as_buffer = &et->as_buffer;
        type->tp_name = PyString_AS_STRING(name);
        //设置基类和基类列表
        type->tp_bases = bases;
        type->tp_base = base;
     
        //设置属性列表
        type->tp_dict = dict = PyDict_Copy(dict);
        //如果自定义
        tmp = PyDict_GetItemString(dict, "__new__");
        if (tmp != NULL && PyFunction_Check(tmp)) {
            tmp = PyStaticMethod_New(tmp);
            if (tmp == NULL) {
                Py_DECREF(type);
                return NULL;
            }
            PyDict_SetItemString(dict, "__new__", tmp);
            Py_DECREF(tmp);
        }
     
        //[1]:为class对象对应的instance对象设置内存大小信息
        slotoffset = base->tp_basicsize;
        if (add_dict) {
            if (base->tp_itemsize)
                type->tp_dictoffset = -(long)sizeof(PyObject *);
            else
                type->tp_dictoffset = slotoffset;
            slotoffset += sizeof(PyObject *);
        }
        if (add_weak) {
            assert(!base->tp_itemsize);
            type->tp_weaklistoffset = slotoffset;
            slotoffset += sizeof(PyObject *);
        }
        type->tp_basicsize = slotoffset;
        type->tp_itemsize = base->tp_itemsize;
        type->tp_members = PyHeapType_GET_MEMBERS(et);
        //调用PyType_Ready对class对象进行初始化
        if (PyType_Ready(type) < 0) {
            Py_DECREF(type);
            return NULL;
        }
     
        return (PyObject *)type;
    }
    

      

    Python虚拟机首先会将类名、基类列表和属性表从args这个tupple对象中解析出来,然后会基于基类列表及传入的metaclass(参数metatype)确定最佳的metaclass和base,对于我们的A来说,最佳的metaclass为<type 'type'>,最佳的base为<type 'object'>

    随后,Python虚拟机会调用metatype->tp_alloc尝试为所要创建的与A对应的class对象分配内存,这里需要注意的是,在PyType_Type中,我们会发现tp_alloc为NULL,那这样一调用Python虚拟机还不立即报错?别忘了,在Python进行初始化时,有一项动作就是从基类继承各种操作,由于type.__bases__中的第一基类是<type 'object'>,所以<type 'type'>会继承<type 'object'>的tp_alloc操作,即PyType_GenericAlloc。对于我们的A(或者说,对于任何继承自object的class对象来说)PyType_GenericAlloc最终将申请metatype->tp_basicsize+metatype->tp_itemsize大小的空间。从PyType_Type的定义中我们可以看到,这个大小实际上就是sizeof(PyHeapTypeObject)+sizeof(PyMemberDef)。到这里就能明白为什么会有PyHeapTyoeObject,原来是为了用户自定义class对象准备的

    此后,就是设置<class A>这个class对象的各个域,其中包括了在tp_dict上设置了属性表,在上述代码[1]处,这里计算了与<class A>对应的instance对象的内存大小信息,换句话说,以后通过a = A()这样的表达式创建一个instance对象时,需要为这个instance对象申请多大的内存呢?对于A(任何继承自object的class对象也成立)来说,这个大小为PyBaseObject_Type->tp_basicsize+8。其中的8为2*sizeof(PyObject *)。为什么后面要跟着两个PyObject *的空间,而且这些空间的地址被设置给tp_dictoffset和tp_weaklistoffset呢?这个以后还会解释

    最后,Python虚拟机还会调用PyType_Ready对<class A>进行和内置class对象一样的初始化动作。到此,A对应的class对象正式创建完毕。图1-5显示了用户自定义class对象和内置class对象最终在布局上的区别

    图1-5   用户自定义class对象和内置class对象的内存布局对比

    本质上,无论是用户自定义的class对象还是内置的class对象,在Python虚拟机内部,都可以用一个PyTypeObject来表示。但不同的是,内置class对象的PyTypeObject及其关联的PyNumberMethods等内存位置都是在编译时确定的,它们在内存中的位置是分离的,而用户自定义的class对象的PyTypeObject和PyNumberMethods等的内存位置是连续的,必须在运行时动态申请内存

    现在,我们对Python中“可调用”这个概念应该有了一定的认识,在Python中,不拘对象、不拘大小,只要对象定义了tp_call操作,就能进行调用操作。我们已经看到,Python中的class对象是调用metaclass对象创建的。如果按照这个逻辑往前推测,那么调用class对象,是不是就能得到instance对象的,在后面分析instance对象的创建,还会介绍

  • 相关阅读:
    Linux Kernel 2:用户空间的初始化
    Linux Kernel系列一:开篇和Kernel启动概要
    谢宝友:会说话的Linux内核
    如何给USB移动硬盘格式化分区
    AVR单片机最小系统 基本硬件线路与分析
    Altium Designer 基本封装
    AVR单片机命名规则
    LynxFly科研小四轴横空出世,开源,F4,WIFI --(转)
    四轴自适应控制算法的一些尝试开源我的山猫飞控和梯度在线辨识自适应等算法—(转)
    我的四轴专用PID参数整定方法及原理---超长文慎入(转)
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9629435.html
Copyright © 2011-2022 走看看