深入理解python虛擬機(jī)之多繼承與?mro
python 繼承的問題
繼承是一種面向?qū)ο缶幊痰母拍睿梢宰屢粋€(gè)類(子類)繼承另一個(gè)類(父類)的屬性和方法。子類可以重寫父類的方法,或者添加自己的方法和屬性。這種機(jī)制使得代碼可以更加模塊化和易于維護(hù)。在 Python 中,繼承是通過在子類的定義中指定父類來實(shí)現(xiàn)的。例如:
class Animal: def __init__(self, name): self.name = name def speak(self): raise NotImplementedError("Subclass must implement abstract method") class Dog(Animal): def speak(self): return "woof"
在這個(gè)例子中,我們定義了一個(gè) Animal
類和一個(gè) Dog
類。Dog
類繼承了 Animal
類,并且重寫了 speak
方法。此時(shí),如果我們創(chuàng)建一個(gè) Dog
實(shí)例并調(diào)用 speak
方法,它將返回 "woof"
。
父類的修改會影響子類
當(dāng)你修改父類的代碼時(shí),可能會影響到繼承自它的子類。這是因?yàn)樽宇惱^承了父類的所有屬性和方法,包括它們的實(shí)現(xiàn)。如果你修改了父類的實(shí)現(xiàn),可能會導(dǎo)致子類的行為發(fā)生變化。因此,在修改父類代碼時(shí),你需要仔細(xì)考慮這個(gè)問題,并盡量避免對子類的影響。
多層繼承的復(fù)雜性
在面向?qū)ο缶幊讨?,有時(shí)需要多層繼承,即一個(gè)類繼承自另一個(gè)繼承自另一個(gè)類。這會導(dǎo)致代碼的復(fù)雜性增加,因?yàn)槟阈枰紤]每個(gè)類之間的關(guān)系和可能出現(xiàn)的命名沖突。另外,多層繼承也會增加代碼的耦合性,使得代碼難以重構(gòu)和維護(hù)。
多繼承當(dāng)中一個(gè)非常經(jīng)典的問題就是棱形繼承,菱形繼承是指一個(gè)子類繼承了兩個(gè)父類,而這兩個(gè)父類又繼承自同一個(gè)基類的情況,如下圖所示:
A
/ \
B C
\ /
D
在這種情況下,子類 D
會繼承兩份來自基類 A
的屬性和方法,這可能會導(dǎo)致一些不必要的問題。例如,如果基類 A
中有一個(gè)名為 foo()
的方法,而基類 B
和 C
都分別重寫了這個(gè)方法,并在子類 D
中調(diào)用了這個(gè)方法,那么子類 D
就無法確定應(yīng)該調(diào)用哪個(gè)版本的 foo()
方法。
另外一種情況就是在多繼承的時(shí)候不同的基類定義了同樣的方法,那么子類就無法確定應(yīng)該使用哪個(gè)父類的實(shí)現(xiàn)。例如,考慮下面這個(gè)示例:
class A: def method(self): print('A') class B: def method(self): print('B') class C(A, B): pass c = C() c.method() # 輸出什么?
在這個(gè)示例中,類 C
繼承了類 A
和類 B
的 method
方法,但是這兩個(gè)方法具有相同的方法名和參數(shù)列表。因此,當(dāng)我們調(diào)用 c.method()
方法時(shí),Python 將無法確定應(yīng)該使用哪個(gè)父類的實(shí)現(xiàn)。
為了解決上面所提到的問題,Python 提供了方法解析順序(Method Resolution Order,MRO)算法,這個(gè)算法可以幫助 Python 確定方法的調(diào)用順序,也就是在調(diào)用的時(shí)候確定調(diào)用哪個(gè)基類的方法。
MRO
在 Python 中,多重繼承會引起 MRO(Method Resolution Order,方法解析順序)問題。當(dāng)一個(gè)類繼承自多個(gè)父類時(shí),Python 需要確定方法調(diào)用的順序,即優(yōu)先調(diào)用哪個(gè)父類的方法。為了解決這個(gè)問題,Python 實(shí)現(xiàn)了一種稱為 C3算法的 MRO 算法,它是一種確定方法解析順序的算法。
我們先來體驗(yàn)使用一下 python 的 mro 的結(jié)果是什么樣的,下面是一個(gè)使用多重繼承的示例:
class A: pass class B(A): pass class C(A): pass class D(B, C): pass
在這個(gè)示例中,D
類繼承自 B
類和 C
類,而 B
和 C
又都繼承自 A
類。因此,D
類的 MRO 列表如下所示:
[D, B, C, A]
這個(gè)列表的解析順序?yàn)?D -> B -> C -> A
,也就是說,當(dāng)我們調(diào)用 D
類的方法時(shí),Python 會首先查找 D
類的方法,然后查找 B
類的方法,再查找 C
類的方法,最后查找 A
類的方法。
你可能會有疑問,為什么 cpython 需要這樣去進(jìn)行解析,為什么不在解析完類 B 之后直接解析 A,還要解析完類 C 之后再去解析 A,我們來看下面的代碼:
class A: def method_a(self): print("In class A") class B(A): pass class C(A): def method_a(self): print("In class C") class D(B, C): pass if __name__ == '__main__': obj = D() print(D.mro()) obj.method_a()
上面的代碼輸出結(jié)果如下所示:
[<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>] In class C
在上面的代碼當(dāng)中繼承體系也是棱形繼承,在類 A 當(dāng)中有一個(gè)方法 method_a
其中類 B 繼承了 A 但是沒有重寫這個(gè)方法,類 B 繼承自 A 但是重寫了這個(gè)方法,類 D 同時(shí)繼承了類 B 和類 C,當(dāng)對象 obj 調(diào)用方法 method_a
的時(shí)候發(fā)現(xiàn)在類 B 當(dāng)中沒有這個(gè)方法,這個(gè)時(shí)候如果直接在類 A 查找這個(gè)方法在很多情況下并不是我們想要的,因?yàn)?D 繼承了 C 如果 C 重寫了這個(gè)方法的話我們應(yīng)該需要調(diào)用類 C 的 method_a
方法,而不是調(diào)用類 A 的 method_a
。
從上面的案例我們可以知道,一個(gè)子類不能夠跨過他的直接父類(D 的直接父類就是 B 和 C)調(diào)用更上層的方法,而是先需要在所有的直接父類查看是否有這個(gè)方法,在 cpython 的 mro 實(shí)現(xiàn)當(dāng)中是能夠保證這一點(diǎn)的,這種性質(zhì)叫做“單調(diào)性”(monotonicity)。
C3 算法
C3 算法是 Python 中使用的 MRO 算法,它可以用來確定一個(gè)類的方法解析順序。首先我們需要知道的就是,當(dāng)一個(gè)類所繼承的多個(gè)類當(dāng)中有相同的基類或者定義了名字相同的方法,會是一個(gè)問題。mro 就是我給他一個(gè)對象,他會給我們返回一個(gè)類的序列,當(dāng)我們打算從對象當(dāng)中獲取一個(gè)屬性或者方法的時(shí)候就會順著這個(gè)序列從左往右進(jìn)行查找,若查找成功則返回,否則繼續(xù)查找后續(xù)的類。
現(xiàn)在我們來詳細(xì)介紹一下 C3 算法的實(shí)現(xiàn)細(xì)節(jié),這個(gè)算法的主要流程是一個(gè)遞歸求解 mro 的過程,假設(shè) A 繼承自 [B, C, D, E, F],那么 C3 算法求 mro 的實(shí)現(xiàn)流程如下所示:
- mro(A) = [A] + merge(mro(B), mro(C), mro(D), mro(E), mro(F), [B, C, D, E, F] )
- merge 函數(shù)的原理是遍歷傳入的序列,找到一個(gè)這樣的序列,序列的第一個(gè)類型只能在其他序列的頭部,或者沒有在其他序列出現(xiàn),并且將這個(gè)序列加入到 merge 函數(shù)的返回序列當(dāng)中,并且將這個(gè)類從所有序列當(dāng)中刪除,重復(fù)這個(gè)步驟直到所有的序列都為空。
class O class A extends O class B extends O class C extends O class D extends O class E extends O class K1 extends A, B, C class K2 extends D, B, E
對 K2 求 mro 序列的結(jié)果如下所示:
L(K2) := [K2] + merge(L(D), L(B), L(E), [D, B, E]) = [K2] + merge([D, O], [B, O], [E, O], [D, B, E]) // 選擇D = [K2, D] + merge([O], [B, O], [E, O], [B, E]) // 不選O,選擇B 因?yàn)?O 在 [B, O] 和 [E, O] 當(dāng)中出現(xiàn)了而且不是第一個(gè) = [K2, D, B] + merge([O], [O], [E, O], [E]) // 不選O,選擇E 因?yàn)?O 在 [E, O] 當(dāng)中出現(xiàn)了而且不是第一個(gè) = [K2, D, B, E] + merge([O], [O], [O]) // 選擇O = [K2, D, B, E, O]
我們自己實(shí)現(xiàn)的 mro 算法如下所示:
from typing import Iterable class A: pass class B(A): pass class C(A): pass class D(B, C): pass def mro(_type: type): bases = _type.__bases__ lin_bases = [] for base in bases: lin_bases.append(mro(base)) lin_bases.append(list(bases)) return [_type] + merge(lin_bases) def merge(types: Iterable[Iterable[type]]): res = [] seqs = types while True: seqs = [s for s in seqs if s] if not seqs: # if seqs is empty return res for seq in seqs: head = seq[0] if not [s for s in seqs if head in s[1:]]: break else: # 如果遍歷完所有的類還是找不到一個(gè)合法的類 則說明 mro 算法失敗 這個(gè)繼承關(guān)系不滿足 C3 算法的要求 raise Exception('can not find mro sequence') res.append(head) for s in seqs: if s[0] == head: del s[0] if __name__ == '__main__': print(D.mro()) print(mro(D)) assert D.mro() == mro(D)
上面的程序的輸出結(jié)果如下所示:
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>] [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
只需要理解求解 mro 序列的過程,上面的代碼比較容易理解,首先就是遞歸求解各個(gè)父類的 mro 序列,然后將他們按照從左到右的順序放入到一個(gè)列表當(dāng)中,最終將父類的 mro 序列進(jìn)行 merge 操作,返回結(jié)果即可。
merge 函數(shù)的主要操作為,按照從左到右的順序遍歷各個(gè)父類的 mro 序列,如果第一個(gè)類沒有在其他父類的 mro 序列當(dāng)中出現(xiàn),或者是其他父類 mro 序列當(dāng)中的第一個(gè)類的話就可以將這個(gè)類加入到返回的 mro 列表當(dāng)中,否則選擇下一個(gè)類的 mro 序列進(jìn)行相同的操作,直到找到一個(gè)符合上面條件的類,如果遍歷完所有的父類還是沒有找到的話那么就報(bào)錯(cuò)。
Mypy 針對 mro 實(shí)現(xiàn)
mypy 是一個(gè) python 類型的靜態(tài)分析工具,它也實(shí)現(xiàn)了 C3 算法用于計(jì)算 mro ,下面是它的代碼實(shí)現(xiàn)。
class MroError(Exception): """Raised if a consistent mro cannot be determined for a class.""" def linearize_hierarchy( info: TypeInfo, obj_type: Callable[[], Instance] | None = None ) -> list[TypeInfo]: # TODO describe if info.mro: return info.mro bases = info.direct_base_classes() if not bases and info.fullname != "builtins.object" and obj_type is not None: # Probably an error, add a dummy `object` base class, # otherwise MRO calculation may spuriously fail. bases = [obj_type().type] lin_bases = [] for base in bases: assert base is not None, f"Cannot linearize bases for {info.fullname} {bases}" lin_bases.append(linearize_hierarchy(base, obj_type)) lin_bases.append(bases) return [info] + merge(lin_bases) def merge(seqs: list[list[TypeInfo]]) -> list[TypeInfo]: seqs = [s.copy() for s in seqs] result: list[TypeInfo] = [] while True: seqs = [s for s in seqs if s] if not seqs: return result for seq in seqs: head = seq[0] if not [s for s in seqs if head in s[1:]]: break else: raise MroError() result.append(head) for s in seqs: if s[0] is head: del s[0]
上面的函數(shù) linearize_hierarchy
就是用于求解 mro 的函數(shù),上面的實(shí)現(xiàn)整體過程和我們自己實(shí)現(xiàn)的 C3 算法是一樣的,首先遞歸調(diào)用 linearize_hierarchy
計(jì)算得到父類的 mro 序列,最后將得到的 mro 進(jìn)行 merge 操作。
cpython 虛擬機(jī) mro 實(shí)現(xiàn)
在本小節(jié)當(dāng)中主要給大家介紹一下在 cpython 當(dāng)中 C 語言層面是如何實(shí)現(xiàn) mro 的。需要知道的 cpython 對于 mro 的實(shí)現(xiàn)也是使用我們在上面提到的算法,算法原理也是一樣的。
static PyObject * mro_implementation(PyTypeObject *type) { PyObject *result; PyObject *bases; PyObject **to_merge; Py_ssize_t i, n; if (type->tp_dict == NULL) { if (PyType_Ready(type) < 0) return NULL; } // 獲取類型 type 的所有父類 bases = type->tp_bases; // bases 的數(shù)據(jù)類型為 tuple assert(PyTuple_Check(bases)); n = PyTuple_GET_SIZE(bases); // 檢查基類的 mro 序列是否計(jì)算出來了 for (i = 0; i < n; i++) { PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(bases, i); if (base->tp_mro == NULL) { PyErr_Format(PyExc_TypeError, "Cannot extend an incomplete type '%.100s'", base->tp_name); return NULL; } assert(PyTuple_Check(base->tp_mro)); } // 如果是單繼承 也就是只繼承了一個(gè)類 那么就可以走 fast path if (n == 1) { /* Fast path: if there is a single base, constructing the MRO * is trivial. */ PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(bases, 0); Py_ssize_t k = PyTuple_GET_SIZE(base->tp_mro); result = PyTuple_New(k + 1); if (result == NULL) { return NULL; } // 直接將父類的 mro 序列加在 當(dāng)前類的后面 即 mro = [當(dāng)前類, 父類的 mro 序列] Py_INCREF(type); PyTuple_SET_ITEM(result, 0, (PyObject *) type); for (i = 0; i < k; i++) { PyObject *cls = PyTuple_GET_ITEM(base->tp_mro, i); Py_INCREF(cls); PyTuple_SET_ITEM(result, i + 1, cls); } return result; } /* This is just a basic sanity check. */ if (check_duplicates(bases) < 0) { return NULL; } /* Find a superclass linearization that honors the constraints of the explicit tuples of bases and the constraints implied by each base class. to_merge is an array of tuples, where each tuple is a superclass linearization implied by a base class. The last element of to_merge is the declared tuple of bases. */ // 如果是多繼承就要按照 C3 算法進(jìn)行實(shí)現(xiàn)了 to_merge = PyMem_New(PyObject *, n + 1); if (to_merge == NULL) { PyErr_NoMemory(); return NULL; } // 得到所有父類的 mro 序列,并將其保存到 to_merge 數(shù)組當(dāng)中 for (i = 0; i < n; i++) { PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(bases, i); to_merge[i] = base->tp_mro; } // 和前面我們自己實(shí)現(xiàn)的算法一樣 也要將所有基類放在數(shù)組的最后 to_merge[n] = bases; result = PyList_New(1); if (result == NULL) { PyMem_Del(to_merge); return NULL; } Py_INCREF(type); PyList_SET_ITEM(result, 0, (PyObject *)type); // 合并數(shù)組 if (pmerge(result, to_merge, n + 1) < 0) { Py_CLEAR(result); } PyMem_Del(to_merge); // 將得到的結(jié)果返回 return result; }
在上面函數(shù)當(dāng)中我們可以分析出來整個(gè)代碼的流程和我們在前面提到的 C3 算法一樣,
static int pmerge(PyObject *acc, PyObject **to_merge, Py_ssize_t to_merge_size) { int res = 0; Py_ssize_t i, j, empty_cnt; int *remain; // remain[i] 表示 to_merge[i] 當(dāng)中下一個(gè)可用的類 也就是說 0 - i-1 的類已經(jīng)被處理合并了 /* remain stores an index into each sublist of to_merge. remain[i] is the index of the next base in to_merge[i] that is not included in acc. */ remain = PyMem_New(int, to_merge_size); if (remain == NULL) { PyErr_NoMemory(); return -1; } // 初始化的時(shí)候都是從第一個(gè)類開始的 所以下標(biāo)初始化成 0 for (i = 0; i < to_merge_size; i++) remain[i] = 0; again: empty_cnt = 0; for (i = 0; i < to_merge_size; i++) { PyObject *candidate; PyObject *cur_tuple = to_merge[i]; if (remain[i] >= PyTuple_GET_SIZE(cur_tuple)) { empty_cnt++; continue; } /* Choose next candidate for MRO. The input sequences alone can determine the choice. If not, choose the class which appears in the MRO of the earliest direct superclass of the new class. */ // 得到候選的類 candidate = PyTuple_GET_ITEM(cur_tuple, remain[i]); // 查看各個(gè)基類的 mro 序列的尾部當(dāng)中是否包含 condidate 尾部就是除去剩下的 mro 序列當(dāng)中的第一個(gè) 剩下的類就是尾部當(dāng)中含有的類 tail_contains 就是檢查尾部當(dāng)中是否包含 condidate for (j = 0; j < to_merge_size; j++) { PyObject *j_lst = to_merge[j]; if (tail_contains(j_lst, remain[j], candidate)) // 如果尾部當(dāng)中包含 condidate 則說明當(dāng)前的 candidate 不符合要求需要查看下一個(gè) mro 序列的第一個(gè)類 看看是否符合要求 如果還不符合就需要找下一個(gè) 再進(jìn)行重復(fù)操作 goto skip; /* continue outer loop */ } // 找到了則將 condidate 加入的返回的結(jié)果當(dāng)中 res = PyList_Append(acc, candidate); if (res < 0) goto out; // 更新 remain 數(shù)組,在前面我們提到了當(dāng)加入一個(gè) candidate 到返回值當(dāng)中的時(shí)候需要將這個(gè)類從所有的基類的 mro 序列當(dāng)中刪除 (事實(shí)上只可能刪除各個(gè) mro 序列當(dāng)中的第一個(gè)類)因此需要更新 remain 數(shù)組 for (j = 0; j < to_merge_size; j++) { PyObject *j_lst = to_merge[j]; if (remain[j] < PyTuple_GET_SIZE(j_lst) && PyTuple_GET_ITEM(j_lst, remain[j]) == candidate) { remain[j]++; } } goto again; skip: ; } if (empty_cnt != to_merge_size) { set_mro_error(to_merge, to_merge_size, remain); res = -1; } out: PyMem_Del(remain); return res; } static int tail_contains(PyObject *tuple, int whence, PyObject *o) { Py_ssize_t j, size; size = PyTuple_GET_SIZE(tuple); for (j = whence+1; j < size; j++) { if (PyTuple_GET_ITEM(tuple, j) == o) return 1; } return 0; }
再談 MRO
在本篇文章當(dāng)中主要給大家介紹了多繼承存在的問題,以及介紹了在 python 當(dāng)中的解決方案 C3 算法。之所以被稱作 C3 算法,主要是因?yàn)檫@個(gè)算法有以下三點(diǎn)特性:
- a consistent extended precedence graph
- preservation of local precedence order
- fitting a monotonicity criterion
我們使用上面的圖來分析一下上面的三個(gè)特性是在說明什么,在上面的繼承關(guān)系當(dāng)中類 A 當(dāng)中有一個(gè)方法 method,類 B 和類 C 繼承自類 A 并且類 C 重寫了 method 方法,類 D 繼承了 B 和 C 。
- monotonicity 單調(diào)性,這個(gè)特性主要是說明子類不能夠跨過父類直接調(diào)用父類的父類的方法,比如在上面的類當(dāng)中,當(dāng)類 D 調(diào)用 method 方法的時(shí)候,調(diào)用的是類 C 的 method 方法而不是類 A 的 method 方法,雖然類 B 沒有 method 而且類 A 有 method 方法,但是子類 D 不能夠跨過父類 B 直接調(diào)用 類 A 的方法,必須檢查類 C 是否有這個(gè)方法,如果有就調(diào)用 C 的,如果 B C 都沒有才調(diào)用 A 的。
- preservation of local precedence order(保留局部優(yōu)先順序),這一點(diǎn)表示 mro 要保證按照繼承先后的順序去查找,也就是說先繼承的先查找,比如 D(B, C) 那么如果同一個(gè)方法類 B 和類 C 都有,那么就會優(yōu)先使用 B 當(dāng)中的方法。
- a consistent extended precedence graph,這一點(diǎn)是相對來說比較復(fù)雜的,這個(gè)特性也是一個(gè)關(guān)于優(yōu)先級的特性,是之前局部優(yōu)先的擴(kuò)展,他的意思是如果兩個(gè)類 A B 有相同的方法,如果 A 或者 A 的子類出現(xiàn)在 B 或者 B 的子類之前,那么 A 的優(yōu)先級比 B 高。比如說對于下圖當(dāng)中的繼承關(guān)系 editable-scrollable-pane 繼承自 scrollable-pane 和 editable-pane,editable-pane繼承自 editing-mixin 和 pane,scrollable-pane 繼承自 scrolling-mixin 和 pane?,F(xiàn)在有一個(gè) editable-scrollable-pane 對象調(diào)用一個(gè)方法,如果這個(gè)方法只在 scrolling-mixin 和 editing-mixin 當(dāng)中出現(xiàn),那么會調(diào)用 scrolling-mixin 當(dāng)中的方法,不會調(diào)用 editing-mixin 當(dāng)中的方法。這是因?yàn)閷τ?editable-scrollable-pane 對象來說 scrollable-pane 在 editable-pane 前面,而前者是 scrolling-mixin 的子類,后者是 editing-mixin 的子類,這是符合前面我們所談到的規(guī)則。
上圖來自論文 A Monotonic Superclass Linearization for Dylan ,這篇論文便是 C3 算法的出處。如果你對這篇論文感興趣的話,論文下載地址為 c3-linearization.pdf (opendylan.org)。
總結(jié)
在本篇文章當(dāng)中主要給大家詳細(xì)分析了 python 當(dāng)中是如何解決多繼承存在的問題的,并且詳細(xì)分析了 C3 算法以及他在 python 和虛擬機(jī)層面的實(shí)現(xiàn),最后簡要介紹了 C3 算法的三個(gè)特性,通過仔細(xì)分析這三個(gè)特性可以幫助我們深入理解整個(gè)繼承樹的調(diào)用鏈,當(dāng)然在實(shí)際編程當(dāng)中最好使用更簡潔的繼承方式,這也可以避免很多問題。
以上就是深入理解python虛擬機(jī)之多繼承與 mro的詳細(xì)內(nèi)容,更多關(guān)于python虛擬機(jī) 多繼承與 mro的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python多進(jìn)程(加入進(jìn)程池)操作常見案例
這篇文章主要介紹了python多進(jìn)程(加入進(jìn)程池)操作,結(jié)合常見案例形式分析了Python多進(jìn)程復(fù)制文件、加入進(jìn)程池及多進(jìn)程聊天等相關(guān)操作技巧,需要的朋友可以參考下2019-10-10python 定時(shí)器,輪詢定時(shí)器的實(shí)例
今天小編就為大家分享一篇python 定時(shí)器,輪詢定時(shí)器的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-02-02pygame編寫音樂播放器的實(shí)現(xiàn)代碼示例
這篇文章主要介紹了pygame編寫音樂播放器的實(shí)現(xiàn)代碼示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11python實(shí)現(xiàn)數(shù)據(jù)分析與建模
這篇文章主要介紹了python實(shí)現(xiàn)數(shù)據(jù)分析與建模功能,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-07-07python socket網(wǎng)絡(luò)編程之粘包問題詳解
這篇文章主要介紹了python socket網(wǎng)絡(luò)編程之粘包問題詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04基于python實(shí)現(xiàn)ROC曲線繪制廣場解析
這篇文章主要介紹了基于python實(shí)現(xiàn)ROC曲線繪制廣場解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06