python进阶强化学习

Posted 小小小的程序媛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python进阶强化学习相关的知识,希望对你有一定的参考价值。

最近学习了慕课的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\',\'jim123456@qq.com\')
student2 = (\'Jone\',16,\'male\',\'jone123456@qq.com\')
student3 = (\'Siliy\',16,\'female\',\'S123456@qq.com\')
# 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\',\'fdasfas@qq.com\') # 可以直接使用元组放入元素
  • 实际问题:统计词频
    解决方案: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进阶强化学习的主要内容,如果未能解决你的问题,请参考以下文章

强化学习与Deep Q-Learning进阶之Nature DQN

Python爬虫进阶一之爬虫框架概述

2Python进阶强化训练之csv|json|xml|excel高

爬虫进阶Python爬虫进阶一之爬虫框架概述

深度学习理论与实战PyTorch实现

收藏 | DeepMind&UCL新课《深度强化学习》2021版上线