流暢的python學習筆記-第2章

第2章 序列構成的數組

[toc]html

第二章開始介紹了列表這種數據結構,這個在python是常常用到的結構python

列表推導

列表的推導,將一個字符串編程一個列表,有下面的2種方法。
其中第二種方法更簡潔。可讀性也比第一種要好。編程

str = 'abc'
string = []

# 第一種方法
for s in str:
    print(string.append(s))

# 第二種方法
ret = [s for s in str]
print(ret)
用這種for…in的方法來推導列表,有個好處就是不會有變量泄露也就是越界的問題

列表的推導還有一種方式,稱爲生成器表達式。
表達式都差很少,不過是方括號編程了圓括號而已數組

生成器的好處是什麼呢?數據結構

列表推導是首先生成一個組合的列表,這會佔用到內存。
而生成式則是在每個for循環運行時才生成一個組合,這樣就不會預先佔用內存
生成器表達式不會一次將整個列表加載到內存之中,而是生成一個生成器對象(Generator objector),因此一次只加載一個列表元素

舉個例子來講明:app

有20000個數組的列表。分別用列表推導法和生成式表達法來進行遍歷。
並用memory_profiler 來監控代碼佔用的內存dom

列表推導法例子:性能

# 若是系統沒有這個模塊就執行pip install memory_profiler進行安裝
from memory_profiler import profile


@profile
def fun_try():
    test = []

    for i in range(20000):
        test.append(i)

    for num in [t for t in test]:
        print(num)


fun_try()

這個代碼運行結果是:網站

Line #    Mem usage    Increment   Line Contents
================================================
     5     39.9 MiB     39.9 MiB   @profile
     6                             def fun_try():
     7     39.9 MiB      0.0 MiB       test = []
     8
     9     40.8 MiB      0.0 MiB       for i in range(20000):
    10     40.8 MiB      0.1 MiB           test.append(i)
    11
    12     41.2 MiB      0.2 MiB       for num in [t for t in test]:
    13     41.2 MiB      0.0 MiB           print(num)



生成式例子:ui

# 若是系統沒有這個模塊就執行pip install memory_profiler進行安裝
from memory_profiler import profile


@profile
def fun_try():
    test = []

    for i in range(20000):
        test.append(i)

    for num in (t for t in test):
        print(num)


fun_try()

這個代碼運行結果是:

Line #    Mem usage    Increment   Line Contents
================================================
     5     40.1 MiB     40.1 MiB   @profile
     6                             def fun_try():
     7     40.1 MiB      0.0 MiB       test = []
     8
     9     41.1 MiB      0.0 MiB       for i in range(20000):
    10     41.1 MiB      0.1 MiB           test.append(i)
    11
    12     41.1 MiB      0.0 MiB       for num in (t for t in test):
    13     41.1 MiB      0.0 MiB           print(num)


結論:

`經過這兩個結果能夠看到列表推導法增長了0.2MB的內存
除非特殊的緣由,應該常常在代碼中使用生成器表達式。
但除非是面對很是大的列表,不然是不會看出明顯區別的。
`



下面介紹下元組。說到元組,第一個反應就應該是不可變列表。
但做者同時還介紹了元組的不少其餘特性。
首先來看下元組的拆包。

#元組拆包
t = (20, 8)
a, b = t
print(a, b)

若是在進行拆包的同時,並非對全部的元組數據都感興趣。
_佔位符就能幫助處理這種狀況

import os
 _, filename = os.path.split("/home/jian/prj/demo.txt")
print(_, filename)  #/home/jian/prj demo.txt

上面元組拆包的時候是對可迭代對象進行遍歷,而後一一賦值到變量。
可是若是想經過給每一個元組元素的命名來訪問,則需用到命名元組namdtuple

# collections.namedtuple構建一個帶有字段名的元組和一個有名字的類
from collections import namedtuple, OrderedDict

City = namedtuple('City', 'name country population')
tokyo = City('Tokyo', 'JP', '123')
tokyo_data = ('Tokyo', 'JP', '123')

print(tokyo)  # City(name='Tokyo', country='JP', population='123')
print(tokyo.name, tokyo.country, tokyo.population)  # Tokyo JP 123
print(City._fields)  # ('name', 'country', 'population')

tokyo = City._make(tokyo_data)
print(tokyo)  # City(name='Tokyo', country='JP', population='123')

OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', '123')])
print(tokyo._asdict())
# OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', '123')])



*符號用來處理剩下的元素

a, b, *rest = range(5)
print(a, b, rest)  # 0 1 [2, 3, 4]

a, *b, rest = range(5)
print(a, b, rest)  # 0 [1,2,3] 4
  • 序列的操做:

增量賦值:
增量運算符+,*等實際上是調用__iadd__/__add__/__mul__方法。
對於可變對象來講,+調用的是__iadd__,對於不可變對象來講對象調用的是__add__

二者有什麼區別呢。

先看下面的例子

str = [1, 2, 3]
str1 = (1, 2, 3)

print("str:%d" % id(str))
print("str1:%d" % id(str1))

str += str
str1 += str1

print("str:%d" % id(str))
print("str1:%d" % id(str1))

獲得的結果以下:

str:2256630301640
str1:2256630239736
str:2256630301640
str1:2256630606152

str是列表,str1是元組,列表是可變對象,元組是不可變對象。

在進行加法運算後,str的id沒有改變,所以仍是以前的對象,可是str1的id卻發生了改變,不是以前的對象了。

這種的差異在於__iadd__的方法相似調用a.extend(b)的方式,是在原有的對象上進行擴展操做。

可是__add__的方式相似於a=a+b。
首先a+b獲得一個新的的對象,而後複製給a,所以變量和以前的對象沒有任何聯繫。
而是被關聯到一個新的對象。一樣的乘法__imul__/__mul__也是相似的道理

列表組成的列表:

board = [['_'] * 3 for i in range(3)]
print(board)

board[1][2] = 'x'
print(board)

運行結果:

[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'x'], ['_', '_', '_']]

輸出三個列表的ID

board = [['_'] * 3 for i in range(3)]
print(board)

board[1][2] = 'x'
print(board)

# 輸出3個列表的ID
print(id(board[0]))
print(id(board[1]))
print(id(board[2]))

結果是分別屬於不一樣的ID:

3221578302664
3221578302536
3221578302600

咱們再來看下另一種用法。下面的代碼對一個包含3個列表的列表進行*3的操做。

board = [['_'] * 3] * 3
print(board)

board[1][2] = 'x'
print(board)
print(id(board[0]))
print(id(board[1]))
print(id(board[2]))

運行結果是:
發現id都同樣。說明了所有指向的是同一個對象。

[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'x'], ['_', '_', 'x'], ['_', '_', 'x']]
1195278432328
1195278432328
1195278432328

若是對不可變對象中的可變對象進行賦值會產生什麼後果,好比下面的這段代碼

t = (1, 2, [30, 40])
t[2] += [50, 60]

print(t)

運行結果直接報錯:

TypeError: 'tuple' object does not support item assignment

咱們能夠把代碼放在python在線調式網站進行調試

把代碼稍微修改下而後再看看狀況

t = (1, 2, [30, 40])
t[2].append([50, 60])

print(t)

爲何這兩種實現會帶來不一樣的結果呢?

緣由在於t是一個元組屬於不可變對象。但用t[2]+=[50,60]的時候是對一個元組進行賦值。
因此報錯誤。

可是同時t[2]又屬於一個列表可變對象。所以數據也更新成功了

可是若是用t[2].append([50,60])的操做則是對一個列表進行操做,而並無對一個元組進行賦值。所以可以更新成功且不會報錯誤。

這是一個頗有趣的例子。

對於理解可變對象和不可變對象的操做頗有幫助。

查看背後的字節碼狀況:

import dis
a = (1, 2, 3)

byte_info = dis.dis('a[2]+=1')
print(byte_info)

打印出反編譯的結果(反編譯後的代碼與彙編語言接近)是:

1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (2)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_CONST               1 (1)
             10 INPLACE_ADD
             12 ROT_THREE
             14 STORE_SUBSCR
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE
插播一個有趣的例子

python之交換變量值

import dis     

def swap1():
    x = 5
    y = 6
    x, y = y,x

def swap2():
    x = 5
    y = 6
    tmp = x
    x = y
    y = tmp

if __name__ == "__main__":
    print ("***SWAP1***")
    print (dis.dis(swap1))
    print ("***SWAP2***")
    print (dis.dis(swap2))

查看結果:

*** SWAP1***
  5           0 LOAD_CONST               1 (5)
              3 STORE_FAST               0 (x)
  6           6 LOAD_CONST               2 (6)
              9 STORE_FAST               1 (y)
  7          12 LOAD_FAST                1 (y)
             15 LOAD_FAST                0 (x)
             18 ROT_TWO             
             19 STORE_FAST               0 (x)
             22 STORE_FAST               1 (y)
             25 LOAD_CONST               0 (None)
             28 RETURN_VALUE      
             
***SWAP2***
10           0 LOAD_CONST               1 (5)
              3 STORE_FAST               0 (x)
11           6 LOAD_CONST               2 (6)
              9 STORE_FAST               1 (y)
12          12 LOAD_FAST                0 (x)
             15 STORE_FAST               2 (tmp)
13          18 LOAD_FAST                1 (y)
             21 STORE_FAST               0 (x)
14          24 LOAD_FAST                2 (tmp)
             27 STORE_FAST               1 (y)
             30 LOAD_CONST               0 (None)
             33 RETURN_VALUE

獲得結論

經過字節碼能夠看到,swap1和swap2最大的區別在於,swap1中經過ROT_TWO交換棧頂的兩個元素實現x和y值的互換,swap2中引入了tmp變量,多了一次LOAD_FAST, STORE_FAST的操做。

執行一個ROT_TWO指令比執行一個LOAD_FAST+STORE_FAST的指令快,

這也是爲何swap1比swap2性能更好的緣由


  • 數組:

在列表和元組中,存放的是具體的對象,如整數對象,字符對象。
以下面的整數b。佔據28個字節。由於存放的是整數對象,而非整數自己。

import sys
b = 1

print(sys.getsizeof(b)) # 28

對於存放大量數據來講。咱們選擇用數組的形式要好不少。
由於數組存儲的不是對象,而是數字的機器翻譯。也就是字節表述。

1183125-20170617154733759-2053769655.png

在array中須要規定各個字符的類型,如上表中的Type code。
定義好類型後則數組內的元素必須全是這個類型,不然會報錯。

就像下面的代碼。類型規定的是b也就是單字節的整數。
可是在插入的時候倒是c字符,則報錯:TypeError: an integer is required

import array

num = array.array('b')
num.append('c')

print(num)


在上表中,每一個類型都有字節大小的限制。若是超出了字節大小的限制也是會報錯的

仍是b的這個類型,是有符號的單字節整數,那麼範圍是-128到127.
若是咱們插入128.則報錯:signed char is greater than maximum 提示超過了最大

import array

num = array.array('b')
num.append(128)

print(num)

對於數組這種結構體來講,因爲佔用的內存小,所以在讀取和寫入文件的時候的速度更快,相比於列表來講的話。

數組與列表效率對比

下面來對比下:
首先是用列表生成並寫入txt文檔的用法

import time
import struct

# struct 例子
# a = 20
# 'i'表示一個int
# # struct.pack把python值轉化成字節流
# data = struct.pack('i', a)
# print(len(data))
# print(repr(data))
# print(data)

# 拆包
# a1 = struct.unpack('i', data)
# print("a1=", a1)


def arry_try_list():

    floats = [float for float in range(10**7)]

    fp = open('list.bin', 'wb')
    start = time.clock()

    for f in floats:

        strdata = struct.pack('i', f)
        fp.write(strdata)

    fp.close()

    end = time.clock()

    print(end - start)


arry_try_list()

執行結果:5.8789385

再看數組的形式:

from array import array
from random import random
import time


def array_try():

    floats = array('d', (random() for i in range(10**7)))
    start = time.clock()

    fp = open('floats.bin', 'wb')
    floats.tofile(fp)

    fp.close()

    end = time.clock()
    print(end - start)


array_try()

執行結果: 0.045192899999999994

能夠看到速度明顯提高了不少

再來對比讀文件的速度

from array import array
import time


def array_try():

    floats = array('d')
    start = time.clock()

    fp = open('floats.bin', 'rb')
    # 比直接從文本文件裏面讀快,後者使用內置的float方法把每一行文字變成浮點數
    floats.fromfile(fp, 10**7)
    fp.close()

    end = time.clock()
    print(end - start)


array_try()

執行結果是:0.1172238


使用memoryview模塊

精準地修改了一個數組的某個字節

#memoryview內存視圖
#在不復制內容的狀況下操做同一個數組的不一樣切片
from array import array

#5個短整型有符號整數的數組,類型碼是h
numbers = array('h', [-2, -1, 0, 1, 2])

memv = memoryview(numbers)
print(len(memv))

#轉成B類型,無符號字符
memv_oct = memv.cast('B')
tolist = memv_oct.tolist()
print(tolist)

memv_oct[5] = 4
print(numbers)

bisect模塊用法

import bisect

# 以空格做爲分隔打印S中全部元素再換行.
def print_all(S):
    for x in S:
        print(x, end = " ")
    print("")

# 有序向量SV.
SV = [1, 3, 6, 6, 8, 9]

#查找元素.
key = int(input())
print(bisect.bisect_left(SV, key))
print(bisect.bisect_right(SV, key))

# 插入新元素.
key = 0
bisect.insort_right(SV, key)

# 刪除重複元素的最後一個. 思考: 如何刪除第一個?
key = 6
i = bisect.bisect_right(SV, key)
i -= 1
# 注意此時i < len(SV)必然成立. 若是確實有key這個元素則刪除.
if (i > 0 and SV[i] == key): del(SV[i])
print_all(SV)

# 刪除重複key所在區間: [bisect.bisect_left(SV, key):bisect.bisect_right(SV, key)).
del SV[bisect.bisect_left(SV, key):bisect.bisect_right(SV, key)]
print_all(SV)

# 無序向量USV.
USV = [9, 6, 1, 3, 8, 6]

# 插入新元素
key = 0
USV.append(key)

# 刪除重複元素的最後一個.
key = 6
# 逆向遍歷, 若是存在則刪除.
i = len(USV) - 1
while i > 0:
    if USV[i] == key:
        USV[i] = USV[-1]
        USV.pop()
        break
    i -= 1
print_all(USV)

# 刪除重複元素的第一個.
try:
    i = USV.index(key)
except:
    pass
else:
    USV[i] = USV[-1]
    USV.pop()
print_all(USV)

pickle模塊用法

pickle模塊是將python值轉成成byte

try:
    import cPickle as pickle
except:
    import pickle

data = [{'a': 'A', 'b': 2, 'c': 3.0}]

# 編碼
data_string = pickle.dumps(data)

print("DATA:", data)
print("PICKLE:", data_string)
print(type(data_string))

# 解碼
data_from_string = pickle.loads(data_string)
print(data_from_string)

雙向隊列

雙向隊列deque

import timeit
from collections import deque


def way1():
    mylist = list(range(100000))
    for i in range(100000):
        mylist.pop()


def way2():
    mydeque = deque(range(100000))
    for i in range(100000):
        mydeque.pop()


if __name__ == "__main__":
    t1 = timeit.timeit("way1", setup="from __main__ import way1", number=10)
    print(t1)

    t2 = timeit.timeit("way2", setup="from __main__ import way2", number=10)
    print(t2)

結果:

6e-07
1.0000000000001327e-06
deque是雙向隊列,若是你的業務邏輯裏面須要大量的從隊列的頭或者尾部刪除,添加,用deque的性能會大幅提升!

若是隻是小隊列,而且對元素須要隨機訪問操做,那麼list會快一些。

# 雙向隊列用法

from collections import deque

# 建立一個隊列
q = deque([1])
print(q)

# 往隊列中添加一個元素
q.append(2)
print(q)

# 往隊列最左邊添加一個元素
q.appendleft(3)
print(q)

# 同時入隊多個元素
q.extend([4, 5, 6])
print(q)

# 在最左邊同時入隊多個元素
q.extendleft([7, 8, 9])
print(q)

# 剔除隊列中最後一個
q.pop()
print(q)

# 刪除隊列最左邊的一個元素
q.popleft()
print(q)

# 清空隊列
# q.clear()

# 獲取隊列長度
# length = q.maxlen
# print(length)
相關文章
相關標籤/搜索