1. 程式人生 > >python進階強化學習

python進階強化學習

執行效率 速度 程序 地方 union 一段 隨機 函數 dead

最近學習了慕課的python進階強化訓練,將學習的內容記錄到這裏,同時也增加了很多相關知識。
主要分為以下九個模塊:

  1. 基本使用
  2. 叠代器和生成器
  3. 字符串
  4. 文件IO操作
  5. 自定義類和類的繼承
  6. 函數裝飾器和類的裝飾器
  7. 進程和線程
  8. 內存管理和垃圾回收機制

基本使用

基本的數據包括:list,tuple(元組),set(集合)和dict(字典)、heapq、queue

  • 處理的實際問題是:過濾列表中的負數
    解決方案:
    1. 列表解析,最好的方式
    2. 字典,使用字典的方式和使用列表的方式差不多,都是對value做判斷,但缺點是使用額外空間
    3. flter函數,缺點是稍慢一點
    4. for 循環叠代,最慢的方式
      同理,對set和dict是一樣的
    from random import randint
    data = [randint(-1,10) for _ in range(10)]
    # 1. 列表解析 
    list_data = [x for x in data if x >= 0]
    # 2. 字典方式
    dict_ = dict(zip(data,[i for i in range(10)]))
    print(dict_)
    dict_data = {k:v for k,v in dict_.items() if v >= 0}
    # 3. filter 方式
    filter_data = filter(lambda x: x>=0, data)
    # 4. for循環方式
    res = []
    for x in data:
      if x>0:
          res.append(x)
  • 實際問題:使用元組中的單個元素只能用下標索引的方式來使用,但是導致代碼的可讀性不高,所以要元組中每個元素命名,提高程序的可讀性。
    解決方案:
    1. 實現枚舉,使被枚舉的元素等於下標
    2. 使用collection.namedtuple 結構來實現帶有簡單屬性的類結構,可以直接通過屬性來訪問.namedtuple 是 tuple的子類
# 出現的問題是:
student1 = ('Jim',16,'male','[email protected]')
student2 = ('Jone',16,'male','[email protected]')
student3 = ('Siliy',16,'female','[email protected]')
# name
print(student1[0])
# age
print(student1[1])
# sex
print(student1[2])

# 程序中出現這些數字導致程序的可讀性很差
# 1. 枚舉元素
NAME,AGE,SEX,MAIL = range(4)
# 2. 使用collection.namedtuple 
from collections import namedtuple
Student = namedtuple('Student',['name','age','sex','email'])
s = Student('jim',15,'male','[email protected]') # 可以直接使用元組放入元素
  • 實際問題:統計詞頻
    解決方案:collection.Counter獲取詞的出現次數,most_common獲取出現次數最多的幾個元素

  • 實際問題:按照值對字典排序。
    解決方案:
    1. 用zip重新組合key和value,再用sorted排序
    2. 直接實現sorted的key函數,用lambda函數
  • 實際問題:找到字典的公共鍵
    解決方案:獲取的字典的key是set類型,直接對set進行交集操作即可

  • 實際問題:讓字典保持元素輸入的順序
    解決方案:使用collection.OrderedDict結構代替原始的dcit結構

  • 實際問題:保證一個容量為n的隊列存儲的歷史記錄
    解決方案:使用collection.deque,它是一個雙端隊列,一旦元素個數超過限制就刪除頭元素

叠代器和可叠代對象

叠代器是訪問集合內元素的一種方式,叠代器對象從集合的第一個元素開始訪問,直到所有對象都被訪問才結束。叠代器只能向前不能後退。
叠代器的基本方法是__next__(self),字符串、列表和元組對象都可以創建叠代器。

生成器是一種簡易實現叠代器的方式,同時提供延遲操作,在需要的時候才產生結果,而不是立即產生結果。
創建生成器的有兩種方式:

  1. 生成器函數,使用yield,每次返回中間的一個結果,在每個結果的中間,掛起函數的狀態,以便下次重離開的地方重新開始
  2. 生成器表達式,類似列表,但是返回的是生成器對象
    生成器需要註意的是只能遍歷一遍。

可叠代對象是可以使用for循環遍歷的對象。

list = [1,2,3,4]
it = iter(list) # 創建叠代器對象
print(next(it)) # 輸出叠代器的下一個元素
# == > 1
print(it.__next__())
# == > 2
print(it.__next__())
# == > 3
print(it.__next__())
# == > 4
# 使用生成器函數
def gensquares(N):
    for i in range(N):
        yield i ** 2
for item in gensquares(5):
    print(item)
    
# 使用生成器表達式
squares = (x**2 for x in range(5)) #<generator object at 0x00B2EC88>

可以直接使用生成器來簡化計算
res = sum(x ** 2 for x in xrange(4)) # 省略直接構造list
#生成器只能遍歷一遍
def get_province_population(filename):
    with open(filename) as f:
        for lien in f:
            yield int(line)
gen = get_province_population('data.txt')
all_polulation = sum(gen)
for population in gen:
    print(population/all_population)
# 這段代碼不會有任何輸出,因為sum已經遍歷過生成器了,所以再次遍歷不會輸出任何結果

QA:

  • 為什麽要有叠代器對象?
    可以任意訪問實現遵守叠代器協議的對象,這樣訪問的時候可以不管訪問對象的性質,只要實現了iter和next函數就可以被訪問。這種方式充分將內容和方式解耦合。

  • 為什麽要有生成器?
    使用叠代器和生成器的方式可以省時間和空間,他們是每次返回下一個數據是在被調用到的時候才返回。
    使用生成器可以代碼量更少,代碼更清晰

  • 生成器、叠代器和可叠代對象的區別?
    生成器使用yield來創建可叠代對象的一種方式
    叠代器是使用Iterator來創建可叠代對象的一種方式
    生成器和叠代器的區別是:產生可叠代對象的方式不同,叠代器的方式較為復雜
    可叠代對象是可以被for循環遍歷的對象,是叠代器和生成器的產物。

A[可叠代對象] -->B[叠代器訪問]
A[可叠代對象]-->C[其他訪問方式]
B[可叠代對象]-->D[生成器]
D-->B
  • 實際問題:如何一個for循環中遍歷多個可叠代對象(一般是list、tuple和map對象)?
    解決方案:
    1. 使用zip函數將多個可叠代對象橫向拼接起來(並行)
    2. 使用itertools的chain對象,將多個叠代對象縱向拼接起來(串行)
  • 實際問題:實現正向叠代和方向叠代(按照step來叠代)
    解決方案:重寫函數的__iter__(self)和__reversed__(self)函數
# 該函數實現一個產生從start開始,每隔step產生一個數字,一直到end結束的數組
class FloatRange:
    def __init__(self,start,end,step):
        self.start = start
        self.end   = end
        self.step  = step
        
    def __iter__(self):
        t = self.start
        while t <= self.end:
            yield t
            t += self.step
    
    def __reversed__(self):
        t = self.end
        while t >= self.start:
            yield t
            t -= self.step

float_range = FloatRange(0,10,0.5)
# == > <__main__.FloatRange object at 0x7fbe70796470>
print(float_range) 

for x in float_range:
    print(x)
  • 實際問題:某軟件需求,從網絡抓取各個城市的氣溫信息,並一次顯示,如果一次抓取所有城市的信息,那麽顯示第一個城市的信息時會存在長期的時延問題。我們希望能使用“用時訪問”的策略,並且把所有城市的氣溫封裝到一個對象中,可以用for循環來叠代,如何解決問題?
    解決方案:實習一個叠代器和一個可叠代對象

import requests
from collections import Iterable, Iterator

# 叠代器是針對城市的,所有要有一個城市列表
class WeatherIterator(Iterator):
    def __init__(self, cities):
        self.cities = cities  # 城市列表
        self.index = 0  # 訪問的位置記錄

    def get_wather(self, city):
        r = requests.get(u'http://wthrcdn.etouch.cn/weather_mini?city=' + city)
        data = r.json()['data']['forecast'][0]
        return '%s: %s, %s' % (city, data['low'], data['high'])

    def __next__(self):
        print('叠代器 next')
        if self.index == len(self.cities):
            raise StopIteration

        city = self.cities[self.index]
        self.index += 1
        return self.get_wather(city)
        
class WeatherIterable(Iterable):

    def __init__(self, cities):
        self.cities = cities

    def __iter__(self):
        print('可叠代對象 iter')
        # 可叠代對象遍歷的時候調用的叠代器的next函數。
        # 返回的是一個叠代器對象
        return WeatherIterator(self.cities)

for x in WeatherIterable(['北京', '天津', '上海']):
    print(x)
# ==>
# 可叠代對象 iter
# 叠代器 next
# 北京: 低溫 -10℃, 高溫 -1℃
# 叠代器 next
# 天津: 低溫 -5℃, 高溫 1℃
# 叠代器 next
# 上海: 低溫 3℃, 高溫 9℃
# 叠代器 next


x = iter(WeatherIterable(['北京', '天津', '上海']))
print(list(x))
# ==>
# 可叠代對象 iter
# 叠代器 next
# 叠代器 next
# 叠代器 next
# 叠代器 next
# ['北京: 低溫 -10℃, 高溫 -1℃', '天津: 低溫 -5℃, 高溫 1℃', '上海: 低溫 3℃, 高溫 9℃']
  • 實際問題:生成器產生可叠代對象?
class WeatherGenerator():
    def __init__(self, cities):
        self.cities = cities

    def get_wather(self, city):
        r = requests.get(u'http://wthrcdn.etouch.cn/weather_mini?city=' + city)
        data = r.json()['data']['forecast'][0]
        return '%s: %s, %s' % (city, data['low'], data['high'])

    def __iter__(self):
        for x in self.cities:
            yield self.get_wather(x)

字符串

重點掌握幾個字符串的函數:
split(分割)
startwith(以某個字母開始)
endwith(以某個字符結束)
+連接操作
‘‘.join連接操作
str.ljust(左對齊)
str.rjust(右對齊)
str.center(居中)
str.format(<長度 >長度 =長度 實現zyou)
str.strip(刪除兩端的空白字符)
str.lstrip(刪除左邊的空白字符)
str.rstrip(刪除右邊的空白字符)
切片+拼接(刪除單個字符)
str.translate(刪除多種不同的字符-使用dict來定義要刪除的字符)
重點掌握re的幾個函數
re.replace
re.sub

文件的IO操作

設置文件的緩沖
open(buffering=XXX),XXX>1-- 全緩沖,XXX=1--行緩沖,XXX=0---無緩沖,XXX是緩沖區的大小
文件的路徑函數
os.path
文件的狀態函數
os.stat,os.fstat
使用臨時文件

from timefile import TemporaryFile, NamedTemporaryFile
f = TemporaryFile()
t = NamedTemporaryFile #創建臨時文件

文件的編碼和解碼問題
python2 和 python3 編碼問題
ASCII碼:一個字節(8位),包括拉丁文和數字
GB2112:兩個字節,表示漢字
Unicode:國際統一標準
utf-8:針對Unicode的可變長的字符編碼,使用1-4個字節表示符號

python2的字符串類型有兩種:str(字節數據)和unicode(unicode數據),這種命名方式更直白,也和C語言是一樣的
python3的字符串類型有兩種:str(unicode數據)和bytes(字節數據),就是將原來的str數據改成統一標準,容納更多數據,這種方式更符合使用習慣。

自定義類和類的繼承

創建類的方式有三種:

- class定義(最經常使用的一種)
- 使用type函數,因為class定義實在運行時動態創建的,而創建class的方法就是使用type函數,type函數可以查看一個對象的類型也可以創建一個新的class類型
- 使用metaclass元類,可以把元類看做的類的模板,類的就是要生成的實例。

元類是類的模板,類是實例的模板

def fn(self,name='world'): #先定義函數
    print('hello')
# 第一個參數是class的名字,第二個參數是父類的集合,第三個參數是方法名和函數綁定
Hello = type('hello',(object,),dcit(hello=fn))

創建抽象類的方式有兩種:

  • 使用raise NotImplementedError的方式來
  • 使用abc.abstractmethod的函數裝飾器
  • 使用meatclass設置類為abc.ABCMeta類

QA: 這三種方式有什麽區別?
使用NotImplementedError的方式時,如果類沒有實現抽象方法的話並且調用子類的抽象方法的話,會出錯。但是如果沒有調用的話,不會出錯。
使用abc.abstractmethod方式時,如果沒有實現子類方法,並且調用的話不會出現錯誤。
使用meta元類的方式的話,如果子類沒有實現抽象方法的話,實例化的過程就會出現錯誤。

實際問題:想要自定義新類型的元組,對於傳入的可叠代對象過濾小於等於0的元素。
解決方案:
1. 使用metaclass方式實際定義一種新的類的類型
2. 繼承tuple類,更改new實例化類的過程

class Person(object):
    """Silly Person"""
 
    def __new__(cls, name, age):
        print '__new__ called.'
        return super(Person, cls).__new__(cls, name, age)
 
    def __init__(self, name, age):
        print '__init__ called.'
        self.name = name
        self.age = age
 
    def __str__(self):
        return '<Person: %s(%s)>' % (self.name, self.age)
 
if __name__ == '__main__':
    piglei = Person('piglei', 24)
    print piglei

# == >
# piglei@macbook-pro:blog$ python new_and_init.py
# __new__ called.
# __init__ called.
# <Person: piglei(24)>

整個代碼的執行邏輯是:

  1. 首先執行new方法,該方法返回一個Person類的實例
  2. 調用這個實例的init方法
class IntTuple(tuple):
    def __new__(cls, iterable):
        g = (x for x in iterable if isinstance(x, int) and x > 0)
        return super(IntTuple, cls).__new__(cls, g)

    def __init__(self, iterable):
        # print self
        tuple.__init__(iterable)
        # super(IntTuple, self).__init__(iterable) # 不知道這種方式為什麽不可以調用父類的init函數

t = IntTuple([1, -1, 'abc', 6, ['x', 'y'], 3])
  • 實際問題:某個網絡遊戲中定義玩家類(id,name,status....)每有一個在線玩家,在服務器程序中則有一個player的實例,當在線人數很多的時候會存在大量的實例,如何降低這些實例的開銷。
    解決方案:關閉實例的dict屬性,dict屬性是保存實例動態創建的屬性的數據結構,這些結構占據大量存儲空間。
    使用sys.getsizeof()得到實例對象的占用空間
import sys

class Player:
    def __init__(self,id,name,age,status=0,level=1):
        self.id = id
        self.name = name
        self.age = age
        self.status =status
        self.level = level

class Player2():
    __slots__ = ('id','name','age','status')

    def __init__(self,id, name, age, status=0, level=1):
        self.id = id
        self.name = name
        self.age = age
        self.status = status
        # self.level = level

p1 = Player(1,'jim',15) # ==> 56
p2 = Player2(2,'john',11) # ==> 80

print(p2.__slots__)
# print(p2.__dict__) # 沒有dict屬性


print('p1占用空間的字節大小為:',sys.getsizeof(p1)) 
# p1占用空間的字節大小為: 56
print('p2占用空間的字節大小為:',sys.getsizeof(p2))
# p2占用空間的字節大小為: 72

print('p1和p2的差集:',set(dir(p1))-set(dir(p2)))
p1和p2的差集: {'__dict__', '__weakref__', 'level'}

這段代碼需要註意一個事情是:如果只是顯示定義了__slots__,沒有給實例動態增加屬性的話,p2的占用空間是較大的,盡管p2仍然沒有dict屬性。但是動態增加屬性後,__slots__方法就有優勢了。

  • 實際問題:創建可管理的對象屬性。在面向對象編程的過程中,直接訪問對象的屬性是不安全的,或設計上不夠靈活,但是使用get函數調用的方式在形式山不如訪問屬性簡潔。
    解決方案: 使用property描述符為類創建可管理屬性,fget/fset/fdel對應相應的屬性訪問.

描述符就是setter和getter方法

class Student(object):

    @property
    def score(self): #相當於是getter方法
        print('getter')
        return self._score

    @score.setter
    def score(self, value):
        print('setter')
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

s = Student()
s.score = 100
print(s.score)
# ==>
# setter
# getter
# 100
  • 實際問題:實現類的比較操作
    解決方案:重載類的__lt__,__le__函數即可

  • 實際問題:使用描述符對實例屬性做類型檢查
    解決方案:實現__set__,__get__方法,在set方法中做類型檢查。實現方式可以property方式

函數裝飾器和類的裝飾器

函數裝飾器

基礎知識
閉包是打破函數變量定義空間的一種方式,閉包裏面包裹自有變量,自由變量的可見範圍和閉包返回函數的範圍是一樣的。每個對象都包含一個__closure__屬性,當函數是閉包的時候,它返回的是一個由cell對象組成的元組對象。cell 對象的cell_contents 屬性就是閉包中的自由變量。
為什麽要用閉包:避免使用全局變量,可以保存自有變量的值

# 閉包的實例
def adder(x):
    def wrapper(y):
        return x + y
    return wrapper

adder5 = adder(5)

adder6 = adder5(10)
print(adder6)
# ==> 15,因為函數最開始封裝了x=5

# 輸出 11
adder7 = adder5(6)
print(adder7)
# ==> 11,因為函數最開始封裝了x=5

裝飾器本質是使用閉包函數,封裝了原先的函數,使其原函數在不改變代碼的前提下增加額外的功能,裝飾器函數返回的是原函數,只不過在原函數之前增加了一些功能。經過裝飾器後的函數和沒有經過裝飾器的函數的用法是一樣的。這種編程方式被稱為面向切面編程。主要使用場景是:向不同的函數添加大量和函數邏輯沒有關聯的代碼。比如:插入日誌、性能檢測、事務處理、權限校驗。

  • 實際問題:向兩個函數添加打印日誌的功能。
    解決方案:
  1. 直接修改原先的代碼。缺點是:向代碼中添加了和邏輯無關的代碼,改動了原有的代碼結構;而且需要添加大量的代碼。
  2. 直接定義新的函數,將原先的函數封裝到新的函數中。缺點是:需要修改原先函數的調用方式
  3. 用函數閉包的方式,在原函數之前完成日誌的工作,在返回原函數。優點是:不改變函數的調用方式。缺點是:需要將原函數替換成被包裹的函數。需要多寫一行代碼
  4. 用函數裝飾器的方式,和3的效果一樣的,但是用函數符號代替了多寫的一行代碼。

附一個講解裝飾器很好的網址:https://zhuanlan.zhihu.com/p/27449649

import logging

# =======       改進方式1.直接在原函數上添加          =================
def foo():
    print('foo function')
    logging.info('foo is running') # 增加打印log功能
    
def bar():
    print('bar function')
    logging.info('bar is running')  # 增加打印log功能

# =======    改進方式2. 重新定義新的函數,包裹要添加的項   =================
def use_logging(func):
    print(type(func))
    logging.info('%s is running' %func.__name__)
    func()
use_logging(foo)
# 這種方式破壞了原有的代碼的邏輯結構,使得原先的調用是foo()變成use_logging(foo)

# ========   改進方式3. 裝飾器返回包裹的函數       ==============
def use_logging(func):
    # 使用函數閉包,返回包裝後的原函數,這樣在調用原函數的時候會先調用閉包內的內容
    def wrapper(*args,**kwargs):
        logging.info('%s is running' %func.__name__)
        return func(*args,**kwargs)
    return wrapper

foo = use_logging(foo)
foo()
# 這種方式使得函數在進入和退出的時候像是一個橫切面,也被稱為面向切面編程

# ========   改進方式4. 裝飾器符號方式    ==============
def use_logging(func):
    # 使用函數閉包,返回包裝後的原函數,這樣在調用原函數的時候會先調用閉包內的內容
    def wrapper(*args,**kwargs):
        logging.info('%s is running' %func.__name__)
        return func(*args,**kwargs)
    return wrapper

@use_logging
def foo():
    print('foo function')
    logging.info('foo is running') # 增加打印log功能
# 這種方式使用裝飾器符號,省去代碼foo=use_logging(foo)
foo()
  • 定義一個帶參數的函數裝飾器
    解決方案:在原來的函數裝飾器上增加一層函數,用以接受參數

QA:為什麽一定要定義的一層的函數來接受參數?為什麽不是直接在原有的函數函數結構上多增加參數?用可變參數的方式?
查看多個文檔發現,函數裝飾器的寫法都是統一的,每個函數裝飾器的參數都是唯一的函數,可能是為了維護統一,所以選擇多增加一層函數封裝。

# ========   改進方式4. 裝飾器符號方式    ==============
# 多增加一層函數接收參數
def use_logging(level):
    
    def decorator(func):
    
        # 使用函數閉包,返回包裝後的原函數,這樣在調用原函數的時候會先調用閉包內的內容
        def wrapper(*args,**kwargs):
            logging.info('%s is running' %func.__name__)
            return func(*args,**kwargs)
        return wrapper
        
    return decorator

@use_logging
def foo():
    print('foo function')
    logging.info('foo is running') # 增加打印log功能
# 這種方式使用裝飾器符號,省去代碼foo=use_logging(foo)
foo()

QA:裝飾器中使用到了args和kwargs參數,為什麽要使用這兩個參數
args可以接收元組的參數,kwargs可以接收字典參數,這兩種組合在一起可以接受任意的參數,這樣就可以不破壞原先函數的參數。

  • 實際問題:如何為被裝飾的函數保存元數據
    -解決方案:
  1. 手動將原函數的所有屬性都直接付給新建立的包裹函數,缺點是需要實現大量的復制操作
  2. 使用標準庫functools中裝飾器wraps裝飾內部函數
# =========== 方式1. 將原函數的所有需要的屬性都付給被包裹的函數   ============================
def log(level="low"):
    def deco(func):
        def wrapper(*args,**kwargs):
            print("log was in...")
            if level == "low":
                print("detailes was needed")
            return func(*args,**kwargs)
        wrapper.__name__ = func.__name__
        wrapper.__dict__ = func.__dict__
        return wrapper
    return deco

@log()
def myFunc():
    '''I am myFunc...'''
    print("myFunc was called")
    
print(myFunc.__name__)
myFunc()
print(myFunc.__name__)

# 缺點是:需要大量復制的操作

# =========== 方式2. 使用functools的wrapper,update_wrapper  ============================
from functools import wraps,update_wrapper
def log(level="low"):
    def deco(func):
        @wraps(func)
        def wrapper(*args,**kwargs):
            print("log was in...")
            if level == "low":
                print("detailes was needed")
            return func(*args,**kwargs)
        update_wrapper(wrapper, func, ('__name__','__doc__'), ('__dict__',))
        return wrapper
    return deco
@log()
def myFunc():
    print("myFunc was called")

print(myFunc.__name__)
myFunc()
print(myFunc.__name__)
  • 實際問題:如何對某個函數使用多個裝飾器?
    解決方案:直接對函數添加多個裝飾器。多個裝飾器的使用過程是按照裝飾器的順序。
    如下代碼所示:順序是deco2->deco1->deco2
from time import ctime


def deco1(func):
    def decorator1(*args, **kwargs):
        print('decorator1 print')
        print('[%s]  %s() is called' % (ctime(), func.__name__))
        return func(*args, **kwargs)
    return decorator1

def deco2(func):
    def decorator2(*args, **kwargs):
        print('decorator2 print')
        print('[%s]  %s() is called' % (ctime(), func.__name__))
        return func(*args, **kwargs)
    return decorator2

@deco2
@deco1
def foo():
    print('Hello, Python')
foo()

# == >
# decorator2 print
# [Wed Dec 12 15:32:19 2018]  decorator1() is called
# decorator1 print
# [Wed Dec 12 15:32:19 2018]  foo() is called
# Hello, Python

類裝飾器

裝飾器不僅可以是函數,還可以是類,相比函數裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使用類裝飾器主要依靠類的__call__方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。

class Foo(object):
    def __init__(self, func):
        self._func = func
    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')
@Foo
def bar():
    print ('bar')
bar()

進程和線程

多任務是由多進程完成的,也可以由一個進程的多線程完成。線程是操作系統的最基本的執行單元。pyton標準庫提供了兩個模塊,tread和threading,thred是低級模塊,threding是高級模塊,對thred進行了封裝,絕大數情況下使用的是threding模塊。
以下開始從單線程--多線程 代碼改造


from time import ctime, sleep
# =============     方式1. 基本的單線程執行任務            ==================

def music(names):
    for i in range(len(names)):
        print('at time: %s, i am listening music %s' % (ctime(), names[i]))
        sleep(1)
    return

def movie(names):
    for i in range(len(names)):
        print('at time: %s, i am watching movie %s' % (ctime(), names[i]))
        sleep(5)

if __name__ == '__main__':
    music(('光年之外', '青花瓷'))
    movie(('暗戰', '熔爐'))
    print('at time %s, all is over' % ctime())
    
# =============     方式2. 多線程執行任務            ==================
import threading
from itertools import chain

def music(name):
    print('at time: %s, music %s start' % (ctime(), name))
    sleep(1)
    print('at time: %s, music %s end' % (ctime(), name))
    return
    
def movie(name):
    print('at time: %s, movie %s start' % (ctime(), name))
    sleep(5)
    print('at time: %s, movie %s end' % (ctime(), name))

if __name__ == '__main__':
    threads = []
    for inx, x in enumerate(chain(('光年之外', '青花瓷') + ('暗戰', '熔爐'))):
        if inx < 2:
            threads.append(threading.Thread(target=music, args=(x,))) #傳遞參數需要傳遞元組的形式,否則會把一個元素拆成多個元素
        else:
            threads.append(threading.Thread(target=movie, args=(x,)))
    for t in threads:
        t.setDaemon(True)  # 設置為守護線程,如果不設置為守護線程會被無限掛起
        t.start()
    # 這個程序會在主線程執行完結束後直接結束子線程
    print('at time %s, all is over' % ctime())
# ==== 方式3. 多線程改進,使主線程等待子線程結束之後再結束  =========
import threading
from itertools import chain

def music(name):
    print('at time: %s, music %s start' % (ctime(), name))
    sleep(1)
    print('at time: %s, music %s end' % (ctime(), name))
    return

def movie(name):
    print('at time: %s, movie %s start' % (ctime(), name))
    sleep(5)
    print('at time: %s, movie %s end' % (ctime(), name))

if __name__ == '__main__':
    threads = []
    for inx, x in enumerate(chain(('光年之外', '青花瓷') + ('暗戰', '熔爐'))):
        if inx < 2:
            threads.append(threading.Thread(target=music, args=(x,))) #傳遞參數需要傳遞元組的形式,否則會把一個元素拆成多個元素
        else:
            threads.append(threading.Thread(target=movie, args=(x,)))
    for t in threads:
        t.setDaemon(True)  # 設置為守護線程,如果不設置為守護線程會被無限掛起
        t.start()
    t.join() # 使子線程完成之前,這個父線程將會被一直阻塞
    # 這個程序會在主線程執行完結束後直接結束子線程
    print('at time %s, all is over' % ctime())
  • 實際問題:進程之間交換數據
    解決方案:使用queue庫的隊列,創建一個可以被多個線程共享的隊列,這些通過使用put和get操作操作隊列。Queue 對象已經包含了必要的鎖,所以你可以通過它在多個線程間多安全地共享數據。

附一個講解進程和線程很好的網址:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p03_communicating_between_threads.html


# 下載電影之後才能看電影,而且下載好電影之後需要通知看電影的進程
from time import ctime, sleep
# =============     方式1. 基本的單線程執行任務   ==================
from queue import Queue
import threading

def download(data_q):
    while True:
        data = ctime()
        print('put in ', data)
        data_q.put(data)

def consume(data_q):
    while True:
        data = data_q.get()
        print('consume ', data)

if __name__ == '__main__':
    q = Queue()
    threads = []
    for _ in range(2):
        threads.append(threading.Thread(target=download, args=(q,)))
        threads.append(threading.Thread(target=consume, args=(q,)))
    for t in threads:
        t.setDaemon(True)
        t.start()

備註:盡管python支持多線程編程,但是解釋器的C語言實現部分在完全並行執行的時候只有一個線程。因為解釋器被一個全局解釋器GIL保護著,它確保任何時候只有一個python線程執行,所有對於所有的線程的來說表面上看是有多個線程同時進行,但是實際上底層部分只有一個線程在運行。所以GIL影響的就是計算密集任務,在pyyhon情況下,如果是針對計算密集的任務,使用多線程並不能加快處理速度,相反可能會導致不同任務之間的CPU切換占據大量的時間。CIL對於IO任務可以加快速度,因為可以在等待IO的過程,CPU操作別的任務。
在Python多線程下,每個線程的執行方式:
1.獲取GIL
2.執行代碼直到sleep或者是python虛擬機將其掛起。
3.釋放GIL
可見,某個線程想要執行,必須先拿到GIL,我們可以把GIL看作是“通行證”,並且在一個python進程中,GIL只有一個。拿不到通行證的線程,就不允許進入CPU執行。

計算密集任務:需要CPU進行大量計算的任務,CPU在過程中一直在使用。
IO密集任務:磁盤IO和網絡IO是主要的任務,CPU大部分時間實在等待IO操作結束。

有兩種策略可以解決GIL的缺點:

  1. 使用線程池
  2. 使用C擴展編程技術

參考網址:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p09_dealing_with_gil_stop_worring_about_it.html

多核下,想做並行提升效率,比較通用的方法是使用多進程,能夠有效提高執行效率。
原因是:每個進程有各自獨立的GIL,互不幹擾,這樣就可以真正意義上的並行執行,所以在python中,多進程的執行效率優於多線程(僅僅針對多核CPU而言)。

  • 實際問題:如何確定進程已經啟動?
    使用event,同時註意event涉及到所有的事件信息

內存管理和垃圾回收機制

  • 對象的引用計數機制
  • 垃圾回收機制
  • 對象緩沖池

對象的引用計數機制

對象的引用

python的最簡單的賦值語句:a=1
分析這句話,1作為一個對象,a是1對象的引用,整個賦值語句就是利用賦值語句將引用a指向對象1.
python是動態語言類型,將對象和引用分離。

a = 1
b = 1

print(id(a)) # ==>  33710424
print(id(b)) # ==>  33710424 
# 因為python為了優化速度,使用小整數對象池[-1,256], 這些對象都是提前建立好,不會被垃圾回收,所有位於這個區間的整數使用的都是同一個對象。
print(a is b) # ==> True

a = "very good morning"
b = "very good morning"
print(a is b) # ==> False

a = []
b = []
print(a is b) # ==> False

對象的可變性

可變對象:int,float,string,tuple,bool
不可變對象:list,dict

結論一:可變對象list是可以改變某個元素的,不可變對象tuple是不可以改變某個元素的

# 可變對象
a = [1, 2, 3]
a[1] = 4
a # == > [1, 4, 3]
# 不可變對象
b = (1, 2, 3)
b[1] = 4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

結論一:一個變量下,可變對象改變內容後地址是不變的

a = [1, 2, 3]
id(a) # == > 2139167175368
a[1] = 4
id(a) # == > 2139167175368

結論二:一個變量下,可變對象改變內容後地址是不變的

a = [1, 2, 3]
id(a) # == > 2139167175368
a[1] = 4
id(a) # == > 2139167175368

結論三:兩個變量下,可變對象改變,另一個變量的內容改變,因為他們指向同一個地址

a = [1, 2, 3]
b = a
a[2] = 4
id(a) # == > 139679751551744
id(b) # == > 139679751551744
b # == > [1, 2, 4]

結論四:兩個變量下,不可變對象改變,另一個變量的內容不改變,因為他們會創建新的對象

a=(1,2,3)
b=a
a=(4,5,6)
id(a) # == >  139679784892848 # 不同的地址
id(b) # == >  139679785232816 # 不同的地址

結論五:類的變量和全局變量的地址是共用的,只要一個修改是否會影響另一個取決於對象是可變的還是不可變

class Myclass:
    def __init__(self, a):
        self.a = a

    def printa(self):
        print(self.a)

print(id(3))
mclass = Myclass(3) # == >10919392
mclass.printa() # == >3
print(id(mclass.a))# == >10919392

print('-----------------')

print(id(1000)) # == >139712873445136
mclass.a = 1000
mclass.printa() # == >1000
print(id(mclass.a)) # == >139712873445136

print('-----------------')
a = [1,2,3]
print(id(a)) # == >139712842879752
mclass.a = a
mclass.printa() # == >[1, 2, 3]
print(id(mclass.a)) # == >139712842879752

print('-----------------')

mclass.a[1] = 4
mclass.printa() # == >[1, 4, 3]
print(id(mclass.a)) # == >139712842879752
print(a) # == >[1, 4, 3]

print('-----------------')
a[1] = 5
mclass.printa() # == >[1, 5, 3]
print(id(mclass.a)) # == >139712842879752
print(a) # == >[1, 5, 3]

對象的計數

每個對象(可變對象和不可變對象)都有計數,用計數的方式保持和跟蹤對象。
python裏每一個東西都是對象,它們的核心就是一個結構體:PyObject。PyObject是每個對象必有的內容,其中ob_refcnt就是做為引用計數。當一個對象有新的引用時,它的ob_refcnt就會增加,當引用它的對象被刪除,它的ob_refcnt就會減少。當引用計數為0時,該對象生命就結束了。

引用計數機制的優點:
  • 簡單
  • 實時性:一旦沒有引用,內存就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。

    引用計數機制的缺點:

  • 維護引用計數消耗資源
  • 循環引用導致內存泄露

    計數操作:

增加計數引用的情況:

  1. 對象被創建,例如a=23
  2. 對象被引用,例如b=a
  3. 對象被作為參數,傳入到一個函數中,例如 func(a)
  4. 對象作為一個元素,存儲在容器中,例如list1=[a,a]

減少計數引用的情況:

  1. 對象的別名被顯式銷毀,例如del a
  2. 可變對象的別名被賦予新的對象,例如a=24
  3. 一個對象離開它的作用域,例如f函數執行完畢時,func函數中的局部變量(全局變量不會)
  4. 對象所在的容器被銷毀,或從容器中刪除對象

sys.getrefcount() 可以查看a對象的引用計數,但是比正常計數大1,因為調用函數的時候傳入a,這會讓a的引用計數+1。

這裏順便說一下:Python的拷貝

  • 直接賦值,增加對原始對象的引用。
  • 淺拷貝,創建一個新的對象,但被他引用的其他對象沒有進行相應的復制,所以所有針對被新創建對象的引用對象的操作都會直接作用與原始的引用對象。
  • 深拷貝,創建一個新的對象,並且遞歸所有被他引用的對象。所以所有針對被新創建對象的引用對象的操作都會作用與新的引用對象,所以原始的引用對象不會受到影響。
    推薦閱讀網址:https://www.cnblogs.com/wilber2013/p/4645353.html

垃圾回收

# 一個循環引用的存在的問題
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)

ist1與list2相互引用,如果不存在其他對象對它們的引用,list1與list2的引用計數也仍然為1,所占用的內存永遠無法被回收,這將是致命的。

垃圾回收主要解決了循環引用的問題。

  1. 針對計數的機制,當一個對象的引用計數為0的時候,他會被當做垃圾回收
  2. 針對循環引用的機制,設置了一個循環檢測器,定期檢查不可訪問對象的循環並刪除他們

另外gc模塊是開發人員可以設置垃圾回收的工具。包含了執行垃圾回收、設置自動執行垃圾回收的頻率、獲取當前垃圾回收對象的計數。能引發循環引用問題的,都是那種容器類對象,比如 list、set、object 等。對於這類對象,虛擬機在為其分配內存時,會額外添加用於追蹤的PyGC_Head。這些對象被添加到特殊鏈表裏,以便 GC 進行管理。

GC垃圾管理模塊

  1. 顯示調用gc
# 顯示執行gc的垃圾回收機制
deff3():
    # print gc.collect()
    c1=ClassA()
    c2=ClassA()
    c1.t=c2
    c2.t=c1
    del c1
    del c2
    print gc.garbage
    print gc.collect() #顯式執行垃圾回收
    print gc.garbage
    time.sleep(10)
if __name__ == '__main__':
    gc.set_debug(gc.DEBUG_LEAK) #設置gc模塊的日誌
    f3()
# 輸出
gc: uncollectable <ClassA instance at 0230E918>
gc: uncollectable <ClassA instance at 0230E940>
gc: uncollectable <dict 0230B810>
gc: uncollectable <dict 02301ED0>
object born,id:0x230e918
object born,id:0x230e940
4

有三種情況會觸發垃圾回收:

  • 顯示調用gc.collect()函數
  • 隱式觸發,當gc模塊的計數器達到閾值的時候會自動觸發gc.collect()函數
  • 當程序退出的時候

具體閾值的設置:
同 .NET、JAVA 一樣,Python GC 同樣將要回收的對象分成 3 級代齡。GEN0 管理新近加入的年輕對象,GEN1 則是在上次回收後依然存活的對象,剩下 GEN2 存儲的都是生命周期極長的家夥。每級代齡都有一個最大容量閾值,每次 GEN0 對象數量超出閾值時,都將引發垃圾回收操作,垃圾回收後的對象會放在gc.garbage列表裏面等待回收。

#define NUM_GENERATIONS 3
/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
    /* PyGC_Head, threshold, count */
    {{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0},
    {{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0},
    {{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0},
};
gc.get_threshold()  # 獲取各級代齡閾值
#==> (700, 10, 10) # 所有python設置的閾值是一樣的

gc.get_count() # 獲取各個代齡的對象數量
#==> (460, 0, 0)
# del 對循環引用沒用
import gc, weakref
class User(object):pass
def callback(r): 
    print (r, "dead")
gc.disable()   
a = User(); wa = weakref.ref(a, callback)
b = User(); wb = weakref.ref(b, callback)
a.b = b; b.a = a    # 形成循環引用關系。
del a; del b     # 刪除名字引用。
wa(), wb()  

# 對象依然在內存中
# ==> (<__main__.User at 0x7fbc1c72e6d8>, <__main__.User at 0x7fbc1c72e860>)

但是GC無法處理有del的循環引用,上一段代碼之後,調用gc.collect()

# del 對循環引用沒用
import gc, weakref
class User(object):pass
def callback(r): 
    print (r, "dead")
gc.disable()   
a = User(); wa = weakref.ref(a, callback)
b = User(); wb = weakref.ref(b, callback)
a.b = b; b.a = a    # 形成循環引用關系。
del a; del b     # 刪除名字引用。
wa(), wb()  

# 對象依然在內存中
# ==> (<__main__.User at 0x7fbc1c72e6d8>, <__main__.User at 0x7fbc1c72e860>)
gc.collect()  

# 但是存在疑問: 我這裏輸出來是可以回收,但是看別人的博客是不可以回收的。
# ==> 自己的代碼
gc: collectable <traceback 0x7fbc1c788188>
gc: collectable <tuple 0x7fbc20dee0b8>
gc: collectable <NameError 0x7fbc1c798ca8>

# ==> 別人的博客
gc: collecting generation 2...
gc: objects in each generation: 520 3190 0
gc: uncollectable <User 0x10fd51fd0>   # a
gc: uncollectable <User 0x10fd57050>   # b
gc: uncollectable <dict 0x7f990ac88280>  # a.__dict__
gc: uncollectable <dict 0x7f990ac88940>  # b.__dict__
gc: done, 4 unreachable, 4 uncollectable, 0.0014s elapsed.
4

剛查了博客,我的代碼為什麽類都可以實現自動回收了,因為我的class沒有實現__del__,gc處理不了的是自定義了__del__的類對象,遇到這種情況只能顯示調用gc.garbage裏面的對象的__del__來打破僵局。

所以在項目中避免出現大量無用對象浪費內存的方法有以下幾種:

  1. 避免循環引用
  2. 引用gc模塊,啟動gc的模塊的自動清理循環引用的對象機制(這裏要註意時間消耗)
  3. 由於分代收集,所以把需要長期使用的變量幾種管理,並盡快移到二代以後,減少gc檢查的時間消耗
  4. gc模塊唯一處理不了的是循環引用的類都有__del__方法,所以項目中要避免定義__del__方法,如果一定要使用該方法,同時導致了循環引用,需要代碼顯式調用gc.garbage裏面的對象的__del__來打破僵局。

  5. 關掉gc
typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;
} PyGC_Head;

當然,這並不表示此類對象非得 GC 才能回收。如果不存在循環引用,自然是積極性更高的引用計數機制搶先給處理掉。也就是說,只要不存在循環引用,理論上可以禁用 GC。當執行某些密集運算時,臨時關掉 GC 有助於提升性能。

常用的垃圾回收(GC)算法有這幾種引用計數(Reference Count)、Mark-Sweep、Copying、分代收集。在Python中使用的是前者引用計數,工作原理:為每個內存對象維護一個引用計數。因 此得知每次內存對象的創建與銷毀都必須修改引用計數,從而在大量的對象創建時,需要大量的執行修改引用計數操作(footprint),對於程序執行過程 中,額外的性能開銷是令人可怕的。

class User(object):
    def __del__(self):
        print (hex(id(self)), "will be dead")

gc.disable()    # 關掉 GC,如果想要關掉gc只能導入gc管理模塊,顯示關掉gc
a = User()    
del a  # 對象正常回收,引用計數不會依賴 GC。
#==>
#0x7fbc1c7b6048 will be dead

分代回收

Python同時采用了分代(generation)回收的策略。這一策略的基本假設是,存活時間越久的對象,越不可能在後面的程序中變成垃圾。我們的程序往往會產生大量的對象,許多對象很快產生和消失,但也有一些對象長期被使用。出於信任和效率,對於這樣一些“長壽”對象,我們相信它們的用處,所以減少在垃圾回收中掃描它們的頻率。

Python將所有的對象分為0,1,2三代。所有的新建對象都是0代對象。當某一代對象經歷過垃圾回收,依然存活,那麽它就被歸入下一代對象。垃圾回收啟動時,一定會掃描所有的0代對象。如果0代經過一定次數垃圾回收,那麽就啟動對0代和1代的掃描清理。當1代也經歷了一定次數的垃圾回收後,那麽會啟動對0,1,2,即對所有對象進行掃描。

這兩個次數即上面get_threshold()返回的(700, 10, 10)返回的兩個10。也就是說,每10次0代垃圾回收,會配合1次1代的垃圾回收;而每10次1代的垃圾回收,才會有1次的2代垃圾回收。

同樣可以用set_threshold()來調整,比如對2代對象進行更頻繁的掃描。

import gc
gc.set_threshold(700, 10, 5)

python的內存池機制總結

整數對象緩沖池

整數對象緩沖池包括兩個部分:小整數對象緩沖池[-1,256]和大整數對象緩沖池。
小整數對象緩沖池
為了優化速度,提前建立,不會被垃圾回收,所有在這個範圍的引用使用的都是一個對象。
大整數對象緩沖池
所有不在小整數對象池的中的整數對象都是大整數對象處,每次新建之前先檢查小整數對象池和大整數對象池,如果已經存在了直接返回現在對象的內存地址,如果沒有就新建。

特別說明一點:類內自己的新建的對象是在不同的地址空間中的。只有當類內自己的對象是由外部賦值得到的,才會外部共享地址空間。


class C1(object):
    a = 100
    b = 100
    c = 1000
    d = 1000

class C2(object):
    a = 100
    b = 1000

c1 = C1()
c2 = C2()
print(id(c1.c))  # == >139686057838352
print(id(c2.b))  # == >139686027637680

a = 1000
print(id(a))  # == >139686027637712
c1.c = a
c2.b = a
print(id(c1.c))  # == >139686027637712
print(id(c2.b))  # == >139686027637712

string對象緩沖池

python使用intern機制管理字符串,intern機制是在創建一個新的字符串對象時,如果已經有了和它的值相同的字符串對象,那麽就直接返回那個對象的引用,而不返回新創建的字符串對象。Python在那裏尋找呢?事實上,python維護著一個鍵值對類型的結構interned,鍵就是字符串的值。但這個intern機制並非對於所有的字符串對象都適用,簡單來說對於那些符合python標識符命名原則的字符串,也就是只包括字母數字下劃線的字符串,python會對它們使用intern機制。

事實上,即使Python會對一個字符串進行intern操作,它也會先創建出一個PyUnicodeObject對象,之後再檢查是否有值和其相同的對象。如果有的話,就將interned中保存的對象返回,之前新創建出來的,因為引用計數變為零,被回收了。被intern機制處理後的對象分為兩類:mortal和immortal,前者會被回收,後者則不會被回收,與Python虛擬機共存亡。

在《Python源碼剖析》原書中提到使用+來連接字符串是一個極其低效的操作,因為每次連接都會創建一個新的字符串對象,之後再讓這個對象等著被銷毀,極大浪費時間和空間,所以推薦使用字符串的join方法來連接字符串。

list對象緩沖池

Python中的list是一個動態數組,它儲存在一個連續的內存塊中,隨機存取的時間復雜度是O(1),但插入和刪除時會造成內存塊的移動,時間復雜度是O(n)。同時,當數組中內存不夠時,會重新申請一塊內存空間並進行內存拷貝。

為了創建一個列表,Python只提供了一條途徑——PyList_New。這個函數接受一個size參數,從而允許我們指定該列表初始的元素個數。不過我們這裏只能指定元素個數,不能指定元素是什麽。
Python中的list是一個動態數組。所以,在每一次需要申請內存時,PyListObject就會申請一大塊內存,這時申請內存的總大小記錄在allocated中,而實際被使用了的內存的數量則記錄在ob_size中。

推薦參考網址: https://juejin.im/post/595f0de75188250d781cfd12

python進階強化學習