亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

在Python中存儲字符串

 更新時間:2022年05月11日 17:26:55   作者:??編程學習網(wǎng)????  
這篇文章主要介紹了在Python中存儲字符串,文章通過unicode展開主題相關(guān)內(nèi)容,具有一定的參考價值,需要的小伙伴可以參考一下

前言:

在這篇Python字符集和字符編碼中我們提到了unicode,該字符集對世界上的文字進行了系統(tǒng)的整理,讓計算機可以用統(tǒng)一的方式處理文本,而且目前已經(jīng)支持超過13萬個字符,天然地支持多國語言。

所以不管什么文字,都可以用一個unicode來表示。

但是問題來了,unicode能表示這么多的字符,那么占用的內(nèi)存一定不低吧。的確,根據(jù)當時的編碼,一個unicode字符最高會占用到4字節(jié)。但是對于西方人來說,明明一個字符就夠用了,為啥需要那么多。

于是又出現(xiàn)了utf-8,它是為unicode提供的一個新的編碼規(guī)則,具有可變長的功能。不同種類的字符占用的大小不同,比如英文字符使用一個字節(jié)存儲,漢字使用3個字節(jié)存儲,Emoji 使用4個字節(jié)存儲。

但Python在表示unicode字符串時,使用的卻不是utf-8編碼,至于原因我們下面來分析一下。

unicode 的三種編碼

從Python3開始,字符串使用的是Unicode。而根據(jù)編碼的不同,Unicode的每個字符最大可以占到4字節(jié),從內(nèi)存的角度來說, 這種編碼有時會比較昂貴。

為了減少內(nèi)存消耗并且提高性能,Python的內(nèi)部使用了三種編碼方式來表示Unicode:

  • Latin-1 編碼:每個字符一字節(jié);
  • UCS2 編碼:每個字符兩字節(jié);
  • UCS4 編碼:每個字符四字節(jié);

在Python編程中,所有字符串的行為都是一致的,而且大多數(shù)時間我們都沒有注意到差異。然而在處理大文本的時候,這種差異就會變得異常顯著、甚至有些讓人出乎意料。

為了看到內(nèi)部表示的差異,我們使用sys.getsizeof函數(shù),查看一個對象所占的字節(jié)數(shù)。

import sys
print(sys.getsizeof("a"))  # 50
print(sys.getsizeof("憨"))  # 76
print(sys.getsizeof("??"))  # 80

我們看到都是一個字符,但是它們占用的內(nèi)存卻是不一樣的。因為Python面對不同的字符會采用不同的編碼,進而導(dǎo)致大小不同。

但需要注意的是:Python的每一個字符串都需要額外占用49-80字節(jié),因為要存儲一些額外的信息,比如:公共的頭部、哈希、長度、字節(jié)長度、編碼類型等等。

import sys
# 對于ASCII字符,一個占1字節(jié),顯然此時編碼是Latin-1編碼
print(sys.getsizeof("ab") - sys.getsizeof("a"))  # 1
# 對于漢字,日文等等,一個占用2字節(jié),此時是UCS2編碼
print(sys.getsizeof("憨憨") - sys.getsizeof("憨"))  # 2
print(sys.getsizeof("です") - sys.getsizeof("で"))  # 2
# 像emoji,則是一個占4字節(jié) ,此時是UCS4編碼
print(sys.getsizeof("????") - sys.getsizeof("??"))  # 4

而采用不同的編碼,那么底層結(jié)構(gòu)體實例的額外部分也會占用不同大小的內(nèi)存。
如果編碼是Latin-1,那么這個結(jié)構(gòu)體實例額外的部分會占49個字節(jié);編碼是UCS2,占74個字節(jié);編碼是UCS4,占76個字節(jié)。然后字符串所占的字節(jié)數(shù)就等于:額外的部分 + 字符個數(shù) * 單個字符所占的字節(jié)。

import sys
# 所以一個空字符串占用49個字節(jié)
# 此時會采用占用內(nèi)存最小的Latin-1編碼
print(sys.getsizeof(""))  # 49
# 此時使用UCS2
print(sys.getsizeof("憨") - 2)  # 74
# UCS4
print(sys.getsizeof("??") - 4)  # 76

為什么不使用utf-8編碼

上面提到的三種編碼,是Python在底層所使用的,但我們知道unicode還有一個utf-8編碼,那Python為啥不用呢?

先來拋出一個問題:首先我們知道Python支持通過索引查找一個字符串指定位置的字符,而且Python默認是以字符為單位的,不是字節(jié)(我們后面還會提),比如s[2]搜索的就是字符串s中的第3個字符。

s = "古明地覺"
print(s[2]) # 地

那么問題來了,我們知道通過索引查找字符串的某個字符,時間復(fù)雜度為O(1),那么Python是怎么通過索引瞬間定位到指定字符的呢?

顯然是通過指針的偏移,用索引乘上每個字符占的字節(jié)數(shù),得到偏移量,然后從頭部向后偏移指定數(shù)量的字節(jié)即可,這樣就能在定位到指定字符的同時還保證時間復(fù)雜度為O(1)。

但是這需要一個前提:字符串中每個字符所占的大小必須是相同的,如果字符占的大小不同,比如有的占1字節(jié)、有的占3字節(jié),顯然就無法通過指針偏移的方式了。這個時候若還想準確定位的話,只能按順序?qū)λ凶址贾饌€掃描,但這樣的話時間復(fù)雜度肯定不是O(1),而是O(n)

我們以Go為例,Go的字符串默認就是使用的utf-8編碼:

package main
import (
    "fmt"
)
func main() {
    s := "古明地覺"
    fmt.Println(s[2])  // 164
    fmt.Println(string(s[2]))  // ¤
}

驚了,我們看到打印的并不是我們希望的結(jié)果。因為Go底層使用的是utf-8編碼,不同的字符可能會占用不同的字節(jié)。但是Go通過索引定位的時候,時間復(fù)雜度也是O(1),所以定位的時候是以字節(jié)為單位、而不是字符。在獲取的時候也只會獲取一個字節(jié),而不是一個字符。

所以s[2]在Go里面指的是第3個字節(jié),而不是第3個字符,而漢字在utf-8編碼下占3個字節(jié),所以s[2]指的就是漢字古的第三個字節(jié)。我們看到打印的時候,該字節(jié)存的值為164。

s = "古明地覺"
print(s.encode("utf-8")[2])  # 164

這就是采用utf-8編碼帶來的弊端,它無法讓我們以O(shè)(1)的時間復(fù)雜度去準確地定位字符,盡管它在存儲的時候更加的省內(nèi)存。

Latin-1、UCS2、UCS4該使用哪一種?

我們說Python會使用3種編碼來表示unicode,所占字節(jié)大小分別是1、2、4字節(jié)。

因此Python在創(chuàng)建字符串的時候,會先掃描,嘗試使用占字節(jié)數(shù)最少的Latin-1編碼存儲,但是范圍肯定有限。如果發(fā)現(xiàn)了存儲不下的字符,只能改變編碼,使用UCS2,繼續(xù)掃描。但是又發(fā)現(xiàn)了新的字符,這個字符UCS2也無法存儲,因為兩個字節(jié)最多存儲65535個不同的字符,所以會再次改變編碼,使用UCS4。UCS4占四個字節(jié),肯定能存下了。

一旦改變編碼,字符串中的所有字符都會使用同樣的編碼,因為它們不具備可變長功能。比如這個字符串:"hello古明地覺",肯定都會使用UCS2,不存在說hello使用Latin1,古明地覺使用UCS2,因為一個字符串只能有一個編碼。

當通過索引獲取的時候,會將索引乘上每個字符占的字節(jié)數(shù),這樣就能跳到準確位置上,因為字符串里面的所有字符占用的字節(jié)都是一樣的,然后獲取的時候也會獲取指定的字節(jié)數(shù)。比如:使用UCS2編碼,那么定位到某個字符的時候,會取兩個字節(jié),這樣才能表示一個完整的字符。

import sys 
# 此時全部是ascii字符,那么Latin-1編碼可以存儲
# 所以結(jié)構(gòu)體實例額外的部分占49個字節(jié)
s1 = "hello"
# 有5個字符,一個字符一個字節(jié),所以加一起是54個字節(jié)
print(sys.getsizeof(s1))  # 54
# 出現(xiàn)了漢字,那么Latin-1肯定存不下,于是使用UCS2
# 所以此時結(jié)構(gòu)體實例額外的部分占74個字節(jié)
# 但是別忘了此時的英文字符也是ucs2,所以也是一個字符兩字節(jié)
s2 = "hello憨"
# 6個字符,74 + 6 * 2 = 86
print(sys.getsizeof(s2))  # 86
# 這個牛逼了,ucs2也存不下,只能ucs4存儲了
# 所以結(jié)構(gòu)體實例額外的部分占76個字節(jié)
s3 = "hello憨??"
# 此時所有字符一個占4字節(jié),7個字符
# 76 + 7 * 4 = 104
print(sys.getsizeof(s3))  # 104

除此之外,我們再舉一個例子更形象地證明這個現(xiàn)象。

import sys
s1 = "a" * 1000
s2 = "a" * 1000 + "??"
# 我們看到s2只比s1多了一個字符
# 但是兩者占的內(nèi)存,s2卻將近是s1的四倍。
print(sys.getsizeof(s1), sys.getsizeof(s2))  # 1049 4080

我們知道s2和s1的差別只是s2比s1多了一個字符,但就是這么一個字符導(dǎo)致s2比s1多占了3031個字節(jié)。然而這3031個字節(jié)不可能是多出來的字符所占的大小,什么字符一個會占到三千多個字節(jié),這是不可能的。

盡管如此,但它也是罪魁禍首,不過前面的1000個字符也是共犯。我們說Python會根據(jù)字符串選擇不同的編碼,s1全部是ascii字符,所以Latin1能存下,因此一個字符只占一個字節(jié)。所以大小就是49 + 1000 = 1049。

但是對于s2,Python發(fā)現(xiàn)前1000個字符Latin1能存下,不幸的是最后一個字符存不下,于是只能使用UCS4。而字符串的所有字符只能有一個編碼,為了保證索引查找的時間復(fù)雜度為O(1),前面一個字節(jié)就能存下的字符,也需要用4字節(jié)來存儲。這是Python的設(shè)計策略。

而我們說使用UCS4,結(jié)構(gòu)體額外的部分會占76個字節(jié),因此s2的大小就是:76 + 1001 * 4 = 4080

print(sys.getsizeof("爺?shù)那啻夯貋砹?))  # 88
print(sys.getsizeof("??的青春回來了"))  # 104

字符數(shù)量相同但是占用內(nèi)存大小不同,相信原因你肯定能分析出來。

所以如果字符串中的所有字符都是ASCII字符,則使用1字節(jié)Latin1對其進行編碼。基本上,Latin1能表示前256個Unicode字符,它支持多種拉丁語,如英語、瑞典語、意大利語、挪威語。但是它們不能存儲非拉丁語言,比如漢語、日語、希伯來語、西里爾語。這是因為它們的代碼點(數(shù)字索引)定義在1字節(jié)(0-255)范圍之外。

大多數(shù)流行的自然語言都可以采用2字節(jié)(UCS2)編碼,但當字符串包含特殊符號、emoji或稀有語言時,則使用4字節(jié)(UCS4)編碼。Unicode標準有將近300個塊(范圍),你可以在0XFFFF塊之后找到4字節(jié)塊。

假設(shè)我們有一個10G的ASCII文本,我們想把它加載到內(nèi)存中,但如果我們在文本中插入一個表情符號,那么字符串的大小將增加4倍。這是一個巨大的差異,你可能會在實踐當中遇到,比如處理NLP問題。

print(ord("a"))  # 97
print(ord("憨"))  # 25000
print(ord("??"))  # 128187

所以最著名和最流行的Unicode編碼都是utf-8,但是Python不在內(nèi)部使用它,而是使用Latin1、UCS2、UCS4。至于原因我們上面已經(jīng)解釋的很清楚了,主要是Python的索引是基于字符,而不是字節(jié)。

當一個字符串使用utf-8編碼存儲時,每個字符會根據(jù)自身選擇一個合適的大小。這是一種存儲效率很高的編碼,但是它有一個明顯的缺點。由于每個字符的字節(jié)長度可能不同,就導(dǎo)致無法按照索引瞬間定位到單個字符,即便能定位,也無法定位準確。如果想準,那么只能逐個掃描所有字符。

假設(shè)要對使用utf-8編碼的字符串執(zhí)行一個簡單的操作,比如s[5],就意味著Python需要掃描每一個字符,直到找到需要的字符,這樣效率是很低的。

但如果是固定長度的編碼就沒有這樣的問題,所以當Latin 1存儲的hello,在和UCS2存儲的古明地覺組合之后,整體每一個字符都會向大的方向擴展、變成了2字節(jié)。

這樣定位字符的時候,只需要將索引 * 2便可計算出偏移的字節(jié)數(shù)、然后跳轉(zhuǎn)該字節(jié)數(shù)即可。但如果原來的hello還是一個字節(jié)、而漢字是2字節(jié),那么只通過索引是不可能定位到準確字符的,因為不同類型字符的大小不同,必須要掃描整個字符串才可以。但是掃描字符串,效率又比較低,所以Python內(nèi)部才會使用這個方法,而不是使用utf-8。

所以對于Go來講,如果想像Python一樣,那么需要這么做:

package main
import (
    "fmt"
)

func main() {
    s := "hello古明地覺"
    //我們看到長度為17, 因為它使用utf-8編碼
    fmt.Println(s, len(s)) // hello古明地覺 17

    //如果想像Python一樣
    //那么Go提供了一個rune,相當于int32
    //此時每個字符均使用4個字節(jié),所以長度變成了9
    r := []rune(s)
    fmt.Println(string(r), len(r)) // hello古明地覺 9
    //雖然打印的內(nèi)容是一樣的,但是此時每個字符都使用4字節(jié)存儲

    //此時跳轉(zhuǎn)會和Python一樣偏移 5 * 4 個字節(jié)
    //然后獲取也會獲取4個字節(jié),因為一個字符占4個字節(jié)
    fmt.Println(string(r[5])) //古
}

所以utf-8編碼的unicode字符串里面的字符可能占用不同的字節(jié),顯然沒辦法實現(xiàn)當前Python字符串的索引查找效果,因此Python沒有使用utf-8編碼。

Python的做法是讓字符串的所有字符都占用相同的字節(jié),先使用占用內(nèi)存最小的Latin1,不行的話再使用UCS2、UCS4,總之會確保每個字符占用的字節(jié)是一樣的。至于原因的話我們上面分析的很透徹了,因為無論是索引還是切片、還是計算長度等等,都是基于字符來的,顯然這也符合人類的思維習慣。

小結(jié)

Python字符串的存儲策略,它并沒有使用最為流行的utf-8,歸根結(jié)底就在于這種編碼不適合Python的字符串。當然,我們在將字符串轉(zhuǎn)成字節(jié)序列的時候,一般使用的都是utf-8編碼。

到此這篇關(guān)于在Python中存儲字符串的文章就介紹到這了,更多相關(guān)Python存儲字符串內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • python數(shù)學模塊(math/decimal模塊)

    python數(shù)學模塊(math/decimal模塊)

    這篇文章主要介紹了python數(shù)學模塊(math/decimal模塊),文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-09-09
  • pandas按若干個列的組合條件篩選數(shù)據(jù)的方法

    pandas按若干個列的組合條件篩選數(shù)據(jù)的方法

    下面小編就為大家分享一篇pandas按若干個列的組合條件篩選數(shù)據(jù)的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-04-04
  • python指定寫入文件時的編碼格式方法

    python指定寫入文件時的編碼格式方法

    今天小編就為大家分享一篇python指定寫入文件時的編碼格式方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-06-06
  • 使用Python自定義創(chuàng)建的Log日志模塊

    使用Python自定義創(chuàng)建的Log日志模塊

    這篇文章主要介紹了使用Python自定義創(chuàng)建的Log日志模塊,日志文件是用于記錄系統(tǒng)操作事件的文件集合,可分為事件日志和消息日志。具有處理歷史數(shù)據(jù)、診斷問題的追蹤以及理解系統(tǒng)的活動等重要作用,需要的朋友可以參考下
    2023-07-07
  • python進階之自定義可迭代的類

    python進階之自定義可迭代的類

    這篇文章主要為大家詳細介紹了python進階之自定義可迭代的類,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-08-08
  • Python之Trimesh庫的使用方式

    Python之Trimesh庫的使用方式

    這篇文章主要介紹了Python之Trimesh庫的使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-06-06
  • Python爬蟲之PhantomJS和handless的使用詳解

    Python爬蟲之PhantomJS和handless的使用詳解

    這篇文章主要介紹了Python爬蟲之PhantomJS和handless的使用詳解,PhantomJS是一個基于Webkit的headless瀏覽器,它會把網(wǎng)站加載到內(nèi)存并使用webkit來編譯解釋執(zhí)行頁面上的JavaScript代碼,由于不進行css和gui渲染、不展示圖形界面,需要的朋友可以參考下
    2023-09-09
  • python中GIL的原理及用法總結(jié)

    python中GIL的原理及用法總結(jié)

    在本篇文章里小編給大家整理的是一篇關(guān)于python中GIL的原理及用法總結(jié)內(nèi)容,有需要的朋友們可以學習參考下。
    2021-03-03
  • python super()函數(shù)的基本使用

    python super()函數(shù)的基本使用

    這篇文章主要介紹了python super()函數(shù)的基本使用,幫助大家更好的理解和使用python,感興趣的朋友可以了解下
    2020-09-09
  • 使用python檢測網(wǎng)頁文本內(nèi)容屏幕上的坐標

    使用python檢測網(wǎng)頁文本內(nèi)容屏幕上的坐標

    在 Web 開發(fā)中,經(jīng)常需要對網(wǎng)頁上的文本內(nèi)容進行處理和操作,有時候,我們可能需要知道某個特定文本在屏幕上的位置,以便進行后續(xù)的操作,所以本文將介紹如何使用 Python 中的 Selenium 和 BeautifulSoup 庫來檢測網(wǎng)頁文本內(nèi)容在屏幕上的坐標,需要的朋友可以參考下
    2024-04-04

最新評論