知其然也要知其因此然,python中的容器對象真的很少,日常咱們會很問心無愧的根據需求來使用對應的容器,不定長數據用list
,想去重用set
,想快速進行匹配用dict
,字符處理用str
,可爲什麼能實現這個效果呢?好比咱們用list
的時候,知道這玩意能夠隨意存儲各類格式,存整型、浮點、字符串、甚至還能夠嵌套list
等其餘容器,這底層的原理究竟是用數組實現的,仍是用鏈表?好比咱們的字典,底層是用數組仍是其餘?若是是其餘如哈希表,那又怎麼實現輸入數據的順序排列?此次不妨一層層剖析,推演一番。貪多嚼不爛,本次就先對list
進行分析html
這個名字很容易和其它語言(C++、Java等)標準庫中的鏈表混淆,不過事實上在CPython
的列表根本不是列表(這話有點繞,可能換成英文理解起來容易些:python中的list不是咱們所學習的list),在CPython中,列表被實現爲長度可變的數組。python
從細節上看,Python中的列表是由對其它對象的引用組成的連續數組,指向這個數組的指針及其長度被保存在一個列表頭結構中。這意味着,每次添加或刪除一個元素時,由引用組成的數組須要該標大小(從新分配)。在實現過程當中,Python在建立這些數組時採用了指數分配的方式,其結果致使每次操做不都須要改變數組的大小,可是也由於這個緣由添加或取出元素的平均複雜度較低。編程
這個方式帶來的後果是在普通鏈表上「代價很小」的其它一些操做在Python中計算複雜度相對太高。數組
list.insert(i,item)
方法在任意位置插入一個元素——複雜度O(N)list.pop(i)
或list.remove(value)
刪除一個元素——複雜度O(N)讓咱們先看下list實現的源碼,源汁源味,細細品評。咱們先發現list多重繼承自MutableSequence
和Generic
。以後咱們能夠讀到,list的相關內嵌函數的實現,如append、pop、extend、insert等其實都是經過繼承來實現的,那麼咱們就不得不去找一下MutableSequence
和Generic
這兩個類的實現底層,也只有解答了這兩個類以後,咱們才能回答爲什麼list能夠實現動態添加數據,並且刪除和插入的複雜度還不是那麼優秀。緩存
class list(MutableSequence[_T], Generic[_T]): @overload def __init__(self) -> None: ... @overload def __init__(self, iterable: Iterable[_T]) -> None: ... if sys.version_info >= (3,): def clear(self) -> None: ... def copy(self) -> List[_T]: ... def append(self, object: _T) -> None: ... def extend(self, iterable: Iterable[_T]) -> None: ... def pop(self, index: int = ...) -> _T: ... def index(self, object: _T, start: int = ..., stop: int = ...) -> int: ... def count(self, object: _T) -> int: ... def insert(self, index: int, object: _T) -> None: ... def remove(self, object: _T) -> None: ... def reverse(self) -> None: ... if sys.version_info >= (3,): def sort(self, *, key: Optional[Callable[[_T], Any]] = ..., reverse: bool = ...) -> None: ... else: def sort(self, cmp: Callable[[_T, _T], Any] = ..., key: Callable[[_T], Any] = ..., reverse: bool = ...) -> None: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T]: ... def __str__(self) -> str: ... __hash__: None # type: ignore @overload def __getitem__(self, i: int) -> _T: ... @overload def __getitem__(self, s: slice) -> List[_T]: ... @overload def __setitem__(self, i: int, o: _T) -> None: ... @overload def __setitem__(self, s: slice, o: Iterable[_T]) -> None: ... def __delitem__(self, i: Union[int, slice]) -> None: ... if sys.version_info < (3,): def __getslice__(self, start: int, stop: int) -> List[_T]: ... def __setslice__(self, start: int, stop: int, o: Sequence[_T]) -> None: ... def __delslice__(self, start: int, stop: int) -> None: ... def __add__(self, x: List[_T]) -> List[_T]: ... def __iadd__(self: _S, x: Iterable[_T]) -> _S: ... def __mul__(self, n: int) -> List[_T]: ... def __rmul__(self, n: int) -> List[_T]: ... if sys.version_info >= (3,): def __imul__(self: _S, n: int) -> _S: ... def __contains__(self, o: object) -> bool: ... def __reversed__(self) -> Iterator[_T]: ... def __gt__(self, x: List[_T]) -> bool: ... def __ge__(self, x: List[_T]) -> bool: ... def __lt__(self, x: List[_T]) -> bool: ... def __le__(self, x: List[_T]) -> bool: ...
這個類實際上是來自於collections.abc.MutableSequence
,其實也就是所謂的抽象基礎類裏面的可變序列的方法。數據結構
Python的序列有兩種,可變序列和不可變序列併爲其提供了兩個基類Sequence
和MutableSequence
,這兩個基類存在於內置模塊collections.abc
中,與其餘常見的類如int
、list
等不一樣,這兩個基類都是抽象基類。這裏涉及到一個新的概念抽象基類,什麼是抽象基類呢?app
對於抽象基類,目前能夠不用關注太多,只需知道抽象基類是指不能實例化產生實例對象的類,後面有機會咱們再專門來討論抽象基類。less
Sequence
和MutableSequence
是兩個抽象基類,所以這兩個類都是不能實例化產生實例對象,那要Sequence
和MutableSequence
兩個抽象基類還有什麼做用呢?ide
其實抽象基類的做用並非實例化產生實例對象的,它的做用更多的像是定義一種規則,或者官方的說法叫作協議,這樣之後咱們但願建立這種類型的對象時,要求遵循這種規則或者協議。如今咱們須要瞭解序列類型都有哪些協議,這須要學習abc模塊中的Sequence
和MutableSequence
兩個類。函數
Sequence和MutableSequence兩個類的繼承關係以下:
圖中粗體表示抽象基類,斜體表示抽象方法,不妨理解爲並未作具體實現的方法,剩下的爲抽象基類中已經實現的方法。
能夠看到,這裏面的繼承關係並不複雜,可是信息量很大,應該牢記這個圖,由於這對理解序列類型很是重要。咱們看到,可變序列MutableSequence
類繼承自不可變序列Sequence
類,Sequence
類又繼承了兩個類Reversible
和Collection
,Collection
又繼承自Container
、 Iterable
、Sized
三個抽象基類。經過這個繼承圖,咱們至少應該可以知道,對於標準不可變序列類型Sequence
,應該至少實現如下幾種方法(遵循這些協議):
__contains__,__iter__,__len__,__reversed__,__getitem__,index,count
這幾個方法到底意味着什麼呢?在前面的list
的實現源碼裏面咱們能夠窺探一二:
__contains__
方法,就意味着list能夠進行成員運算,即便用in
和not in
的效果__iter__
方法,意味着list是一個可迭代對象,能夠進行for
循環、拆包、生成器表達式等多種運算__len__
方法,意味着可使用內置函數len()
。同時,當判斷一個list的布爾值時,若是list沒有實現__bool__
方法,也會嘗試調用__len__
方法__reversed__
方法,意味着能夠實現反轉操做__getitem__
方法,意味着能夠進行索引和切片操做index
和count
方法,則表示能夠按條件取索引和統計頻數。標準的Sequence
類型聲明瞭上述方法,這意味着繼承自Sequence
的子類,其實例化產生的對象將是一個可迭代對象、可使用for循環、拆包、生成器表達式、in、not in、索引、切片、翻轉等等不少操做。這同時也代表,若是咱們說一個對象是不可變序列時,暗示這個對象是一個可迭代對象、可使用for循環、......。
而對於標準可變序列MutableSequence
,咱們發現,除了要實現不可變序列中幾種方法以外,至少還須要實現以下幾個方法(遵循這些協議):
__setitem__,__delitem__,insert,append,extend,pop,remove,__iadd__
這幾個方法又意味着什麼呢?一樣以Python的內置類型list爲例進行說明:
__setitem__
方法,就能夠對列表中的元素進行修改,如a = [1,2]
,代碼a[0]=2
就是在調用這個方法__delitem__
,pop
,remove
方法,就能夠對列表中的元素進行刪除,如a = [1,2]
,代碼del a[0]
就是在調用__delitem__
方法insert
,append
,extend
方法,就能夠在序列中插入元素__iadd__
方法,列表就能夠進行增量賦值這就是說,對於標準可變序列類型,除了執行不可變類型的查詢操做以外,其子類的實例對象均可以執行增刪改的操做。
抽象基類Sequence
和MutableSequence
聲明瞭對於一個序列類型應該實現那些方法,很顯然,若是一個類直接繼承自Sequence
類,內部也重載了Sequence
中的七個方法,那麼顯然這個類必定是序列類型了,MutableSequence
的子類也是同樣。確實如此,可是當咱們查看列表list、字符序列str、元組tuple的繼承鏈時,發如今其mro列表中並無Sequence和MutableSequence類,也就是說,這些內置類型並無直接繼承自這兩個抽象基類,那麼爲何咱們在文章的開頭還要說他們都是序列類型呢?
>>> list.__mro__ (<class 'list'>, <class 'object'>) >>> tuple.__mro__ (<class 'tuple'>, <class 'object'>) >>> str.__mro__ (<class 'str'>, <class 'object'>) >>> dict.__mro__ (<class 'dict'>, <class 'object'>)
其實,Python中有一種被稱爲鴨子類型的編程風格。在這種風格下,咱們並不太關注一個對象的類型是什麼,它繼承自那個類型,而是關注他能實現那些功能,定義了那些方法。正所謂若是一個東西看起來像鴨子,走起來像鴨子,叫起來像鴨子,那他就是鴨子。
在這種思想之下,若是一個類並非直接繼承自Sequence
,可是內部卻實現了__contains__
、__iter__
、__len__
、__reversed__
、__getitem__
、index
,count
幾個方法,咱們就能夠稱之爲不可變序列。甚至都沒必要這麼嚴格,可能只須要實現__len__
,__getitem__
兩個方法就能夠稱做是不可變序列類型。對於可變序列也一樣如此。
鴨子類型的思想貫穿了Python面向對象編程的始終。
這個類其實就是泛型的實現,從註釋中能夠發現,這個其實也是抽象基類,本質上用來實現多類型參數輸入。好比在list中咱們能夠既存入int,又能夠是str,還能夠是list,也能夠是dict等等多個不一樣類型的元素,這個本質上就是依賴於這個類的繼承。
class Generic: """Abstract base class for generic types. A generic type is typically declared by inheriting from this class parameterized with one or more type variables. For example, a generic mapping type might be defined as:: class Mapping(Generic[KT, VT]): def __getitem__(self, key: KT) -> VT: ... # Etc. This class can then be used as follows:: def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT: try: return mapping[key] except KeyError: return default """ __slots__ = () _is_protocol = False @_tp_cache def __class_getitem__(cls, params): if not isinstance(params, tuple): params = (params,) if not params and cls is not Tuple: raise TypeError( f"Parameter list to {cls.__qualname__}[...] cannot be empty") msg = "Parameters to generic types must be types." params = tuple(_type_check(p, msg) for p in params) if cls in (Generic, Protocol): # Generic and Protocol can only be subscripted with unique type variables. if not all(isinstance(p, TypeVar) for p in params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be type variables") if len(set(params)) != len(params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be unique") else: # Subscripting a regular Generic subclass. _check_generic(cls, params, len(cls.__parameters__)) return _GenericAlias(cls, params) def __init_subclass__(cls, *args, **kwargs): super().__init_subclass__(*args, **kwargs) tvars = [] if '__orig_bases__' in cls.__dict__: error = Generic in cls.__orig_bases__ else: error = Generic in cls.__bases__ and cls.__name__ != 'Protocol' if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: tvars = _collect_type_vars(cls.__orig_bases__) # Look for Generic[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. # Also check for and reject plain Generic, # and reject multiple Generic[...]. gvars = None for base in cls.__orig_bases__: if (isinstance(base, _GenericAlias) and base.__origin__ is Generic): if gvars is not None: raise TypeError( "Cannot inherit from Generic[...] multiple types.") gvars = base.__parameters__ if gvars is not None: tvarset = set(tvars) gvarset = set(gvars) if not tvarset <= gvarset: s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) s_args = ', '.join(str(g) for g in gvars) raise TypeError(f"Some type variables ({s_vars}) are" f" not listed in Generic[{s_args}]") tvars = gvars cls.__parameters__ = tuple(tvars)
做爲一個經常使用數據結構,在不少場景中被用來當作數組使用,可能不少時候都以爲list無非就是一個動態數組,就像C++中的vector或者Go中的slice同樣。從源碼的實現中,咱們也能夠發現list繼承MutableSequence
而且擁有泛型的效果,但這樣就能夠斷言說list就是一個動態數組嗎?
咱們來思考一個簡單的問題,Python中的list容許咱們存儲不一樣類型的數據,既然類型不一樣,那內存佔用空間就就不一樣,不一樣大小的數據對象又是如何"存入"數組中呢?
咱們能夠分別在數組中存儲了一個字符串,一個整形,以及一個字典對象,假如是數組實現,則須要將數據存儲在相鄰的內存空間中,而索引訪問就變成一個至關困難的事情了,畢竟咱們沒法猜想每一個元素的大小,從而沒法定位想要的元素位置。
是不是經過鏈表結構實現的呢? 畢竟鏈表支持動態的調整,藉助於指針能夠引用不一樣類型的數據,好比下面的圖示中的鏈表結構。可是這樣的話使用下標索引數據的時候,須要依賴於遍歷的方式查找,O(n)的時間複雜度訪問效率實在是過低。
不過對於鏈表的使用,系統開銷也較大,畢竟每一個數據項除了維護本地數據指針外,還要維護一個next指針
,所以還要額外分配8字節數據,同時鏈表分散性使其沒法像數組同樣利用CPU的緩存來高效的執行數據讀寫。
咱們這個時候再來推敲一下list
這個結構的內部實現,筆者接下來的推演都是基於CPython
來的,不一樣語言的實現語法應該是不一樣的,不過思路大同小異。
Python中的list數據結構實現要更比想象的更簡單且純粹一些,保留了數組內存連續性訪問的方式,只是每一個節點存儲的不是實際數據,而是對應數據的指針,以一個指針數組的形式來進行存儲和訪問數據項,對應的結構以下面圖示:
實現的細節能夠從其Python的源碼中找到, 定義以下:
typedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject;
內部list的實現的是一個C結構體,該結構體中的ob_item
是一個指針數組,存儲了全部對象的指針數據,allocated
是已分配內存的數量, PyObject_VAR_HEAD
是一個宏擴展包含了更多擴展屬性用於管理數組,好比引用計數以及數組大小等內容。
既然是一個動態數組,則必然會面臨一個問題,即如何進行容量的管理,大部分的程序語言對於此類結構使用動態調整策略,也就是當存儲容量達到必定閾值的時候,擴展容量,當存儲容量低於必定的閾值的時候,縮減容量。道理很簡單,不過實施起來可沒那麼容易,何時擴容,擴多少,何時執行回收,每次又要回收多少空閒容量,這些都是在實現過程當中須要明確的問題。
對於Python中list的動態調整規則程序中定義以下:當追加數據容量已滿的時候,經過下面的方式計算再次分配的空間大小,建立新的數組,並將全部數據複製到新的數組中。這是一種相對數據增速較慢的策略,回收的時候則當容量空閒一半的時候執行策略,獲取新的縮減後容量大小。其實這個方式就很像TCP的滑動窗口的機制
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6); new_allocated += newsize // 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …
假如咱們使用一種最簡單的策略:超出容量加倍,低於一半容量減倍。這種策略會有什麼問題呢?設想一下當咱們在容量已滿的時候進行一次插入,隨即刪除該元素,交替執行屢次,那數組數據豈不是會不斷的被總體複製和回收,已經無性能可言了。
接下來,咱們來看下list數據結構的幾個常見操做。首先是在list上執行append的操做, 該函數將元素添加到list的尾部。注意這裏是指針數據被追加到尾部,而不是實際元素。
test = list() test.append("hello yerik")
向列表添加字符串:test.append("hello yerik") 時發生了什麼?其實是調用了底層的 C 函數 app1()。
arguments: list object, new element returns: 0 if OK, -1 if not app1: n = size of list call list_resize() to resize the list to size n+1 = 0 + 1 = 1 list[n] = list[0] = new element return 0
對於一個空的list,此時數組的大小爲0,爲了可以插入元素,咱們須要對數組進行擴容,按照上面的計算公式進行調整大小。好比這時候只有一個元素,那麼newsize = 1, 計算的new_allocated = 3 + 1 = 4 , 成功插入元素後,直到插入第五元素以前咱們都不須要從新分配新的空間,從而避免頻繁調用 list_resize() 函數,提高程序性能。
咱們嘗試繼續添加更多的元素到列表中,當咱們插入元素"abc"的時候,其內部數組大小不足以容納該元素,執行新一輪動態擴容,此時newsize = 5 , new_allocated = 3 + 5 = 8
>>> test.append(520) >>> test.append(dict()) >>> test.append(list()) >>> test.append("abc") >>> test ['hello yerik', 520, {}, [], 'abc']
執行插入後的數據存儲空間分佈以下圖所示:
在列表偏移量 2 的位置插入新元素,整數 5:test.insert(1,2.33333333),內部調用ins1() 函數。
arguments: list object, where, new element returns: 0 if OK, -1 if not ins1: resize list to size n+1 = 5 -> 4 more slots will be allocated starting at the last element up to the offset where, right shift each element set new element at offset where return 0
python實現的insert函數接收兩個參數,第一個是指定插入的位置,第二個爲元素對象。中間插入會致使該位置後面的元素進行移位操做,因爲是存儲的指針所以實際的元素不須要進行位移,只須要位移其指針便可。
>>> test.insert(2,2.33333333) >>> test ['hello yerik', 520, 2.33333333, {}, [], 'abc']
插入元素爲一個字符串對象,建立該字符串並得到其指針(ptr5), 將其存入索引爲2的數組位置中,並將其他後續元素分別移動一個位置便可,insert函數調用完成。正是因爲須要進行「檢查擴容」的緣由,從而致使了該操做的複雜度達到了O(n),而不是鏈表所存在的O(1)
取出列表最後一個元素 即l.pop(),調用了 listpop() 函數。在 listpop() 函數中會調用 list_resize 函數,若是此時元素的使用率低於一半,則進行空閒容量的回收。
arguments: list object returns: element popped listpop: if list empty: return null resize list with size 5 - 1 = 4. 4 is not less than 8/2 so no shrinkage set list object size to 4 return last element
在鏈表中pop 操做的平均複雜度爲 O(1)。不過因爲可能須要進行存儲空間大小的修改,所以致使複雜度上升
>>> test.pop() 'abc' >>> test.pop() [] >>> test.pop() {} >>> test ['hello yerik', 520, 2.33333333]
末尾位置的元素被回收,指針清空,這時候長度爲5,容量爲8,所以不須要執行任何的回收策略。當咱們繼續執行三次pop使其長度變爲3後,此時使用量低於了一半的容量,須要執行回收策略。回收的方式一樣是利用上面的公式進行處理,好比這裏新的大小爲3,則返回容量大小爲3+3 = 6 ,並不是回收所有的空閒空間。
pop的操做也是須要進行檢查縮小,所以也是致使複雜度爲O(n)
remove函數會指定刪除的元素,而該元素能夠在列表中的任意位置。所以每次執行remove都必須先依次遍歷數據項,進行匹配,直到找到對應的元素位置。執行刪除可能會致使部分元素的遷移。Remove操做的總體時間複雜度爲O(n)。
>>> test ['hello yerik', 520, 2.33333333] >>> test.remove(520) >>> test ['hello yerik', 2.33333333]
其實對於Python列表這種數據結構的動態調整,在其餘語言中也都存在,只是你們可能在平常使用中並無意識到,瞭解了動態調整規則,咱們能夠經過好比手動分配足夠的空間,來減小其動態分配帶來的遷移成本,使得程序運行的更高效。
另外若是事先知道存儲在列表中的數據類型都相同,好比都是整形或者字符等類型,能夠考慮使用arrays庫,或者numpy庫,二者都提供更直接的數組內存存儲模型,而不是上面的指針引用模型,所以在訪問和存儲效率上面會更高效一些。