【Python知识点梳理】10.Python的垃圾回收机制、代码规范及命令行参数

Python的垃圾回收机制、代码规范及命令行参数



1、Python的垃圾回收机制

  垃圾回收机制的必要性:程序长时间占用的内存需要释放。
  内存泄漏:由于开发人员的疏忽或则错误引起的程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是意味着代码在分配了某段内存后,因为设计错误,失去了对这段内存的控制,从而造成了内存的浪费。常见的有循环引用导致的内存泄漏问题。

# 内存泄漏测试
import gc

class ClassA(object):
    def __init__(self):
        print("对象产生,id:%s" % str(id(self)))


def f2():
    while True:
        c1 = ClassA()
        c2 = ClassA()
        c1.t = c2   # 动态添加t属性(实例属性)
        c2.t = c1   # 动态添加t属性(实例属性)
        del c1
        del c2
        # gc.collect()


gc.disable()  # Python默认是开启垃圾回收机制的,可以通过下面的代码关闭
f2()
# 执行f2(),进程占用的内存会不断增大。
# 创建了c1、c2后这两块内存的引用计数都是1,执行c1.t = c2、c2.t = c1后,这两块内存的引用计数都是2
# 在del c1后,引用计数为1,由于不是0,所以c1对象不会被销毁。
# Python默认是开启垃圾回收机制的,关闭后,导致内存泄漏

2、Python的引用计数机制

  概述:引用计数机制(Garbage Collection),Python内部使用引用计数,来保持追踪内存中的对象,Python内部记录了对象有多少个引用,即引用计数,当对象被创建时就创建了一个引用计数,当对象不再需要时,这个对象的引用计数为0时,它被垃圾回收。
  引用计数是一种垃圾收集机制,而且也是一种最直观、最简单的垃圾收集技术。Python的垃圾回收采用的是引用计数机制为主和分代回收机制、标记-清除机制为辅的结合机制,当对象的引用计数变为0时,对象将被销毁,除了解释器默认创建的对象外(默认对象的引用计数永远不会变成0)。
  引用计数机制的任务:
 1.为新生成的对象分配内存;
 2.识别哪些是垃圾对象;
 3.从垃圾对象中回收内存。
  Python中的每一个东西都是对象,它们的核心就是一个结构体:PyObject

typedef struct object {
    int ob_refcnt;
    struct_typeobject * ob_refcnt
}PyObject;

  PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数的。当一个对象有新的引用时。它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少,当引用计数为0时,该对象就没有了。

# 查看一个变量的引用次数:sys.getrefcount( )
import sys

a = []
print(sys.getrefcount(a))  # 2次

b = a
print(sys.getrefcount(a))  # 3次

c = b
d = b
e = c
f = e
g = d
print(sys.getrefcount(a))  # 8次

  在python中可以通过sys.getrefcount()函数查看一个变量的引用次数。需注意:
 1.getrefcount()本身也会引入一次计数;
 2.在函数调用发生的时候,会产生额外的两次引用,一次来自函数栈,另一个是函数参数。

import sys

def func(a):
    print(sys.getrefcount(a))

a = []
func(a)
b = a
c = b
print(sys.getrefcount(a))

3、Python中的循环数据结构及引用计数

  引用计数的缺点:
 1.维护引用计数会消耗资源;
 2.循环数据结构及引用计数。

import os
import sys
import psutil  # 获取系统运行的进程和系统利用率

def ShowMemorySize(tag):
    pid = os.getpid()        # 获取进程id
    p = psutil.Process(pid)  # 获取进程对象

    info = p.memory_full_info()  # 占用的内存信息
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(tag, memory))


# 验证循环引用的情况
def func():
    ShowMemorySize('初始化')
    a = [i for i in range(10000000)]
    b = [i for i in range(10000000)]
    a.append(b)  # 相互的引用
    b.append(a)  # 相互的引用
    ShowMemorySize('创建列表对象a、b之后')
#     print(sys.getrefcount(a))
#     print(sys.getrefcount(b))
    del a  # 即使显式删除,但是内存没有释放
    del b


func()
ShowMemorySize('完成之后的情况')  # 程序执行完成后依然占据内存
# 程序执行完,但是内存没有释放,
# 如果这种错误出现在生产环境中,a和b一开始占用的空间不是很大,但经过长时间运行后
# Python 所占用的内存一定会变得越来越大,后果不堪设想。

  此时程序中的例子成了一个"孤岛",一组未使用的、互相指向的对象,都没有外部指向。换句话说,因为所有的引用计数都是1而不是0,Python的引用计数机制不能够处理相互指向自己的对象。

import os
import gc
import sys
import psutil  # 获取系统运行的进程和系统利用率

def ShowMemorySize(tag):
    pid = os.getpid()        # 获取进程id
    p = psutil.Process(pid)  # 获取进程对象

    info = p.memory_full_info()  # 占用的内存信息
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(tag, memory))


# 验证循环引用的情况
def func():
    ShowMemorySize('初始化')
    a = [i for i in range(10000000)]
    b = [i for i in range(10000000)]
    a.append(b)  # 相互的引用
    b.append(a)  # 相互的引用
    ShowMemorySize('创建列表对象a、b之后')


func()
gc.collect()  # 可以通过显示手动gc.collect()释放资源
ShowMemorySize('完成之后的情况')

  信息-标记清除:一种基于追踪回收技术实现的垃圾回收算法(Tracing GC)。它分为两个阶段:
 1.第一阶段是标记阶段,GC会把所有的活动对象打上标记(垃圾检测);
 2.第二阶段是把那些没有标记的非活动对象进行回收(垃圾回收)。
  首先初始化所有对象并标记为白色,确定根节点对象(根节点对象不会被删除),将它们标记为黑色(表示对象有效)。然后将有效对象引用的对象标记为灰色(表示对象可达,但是它们所引用的对象还没检查),检查完灰色对象引用的对象后,将灰色对象标记为黑色,重复直到不存在灰色节点为止。最后白色节点都是需要清除的对象。

请添加图片描述

  分代回收:一种以空间换时间的操作方式(Generationa GC)。Python将所有对象分为三代,分别为年轻代(第0代,Generationa Zero)、中年代(第1代)、老年代(第2代)。它们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。

import objgraph  # 查看内存调用关系

a = [1, 2, 3, 4]
b = [5, 6, 7, 8]
c = [1, 2, 3, a, b]

a.append(b)
b.append(a)
a.append(c)

objgraph.show_backrefs([a])

请添加图片描述


4、Python中的GC模块

  有三种情况会触发垃圾回收机制:
 1.当gc模块的计数器达到阈值的时候,自动回收垃圾;
 2.调用gc.collect(),手动回收垃圾;
 3.程序推出的时候,Python解释器来回收垃圾。
  gc模块中有一个长度为3的列表计算器,可以通过gc.get_count()获取。

import gc

class A(object):
    pass


print(gc.get_count())

a = A()
print(gc.get_count())

del a
print(gc.get_count())

  (244, 10, 1):244是指距离上一次一代检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数器的增加。10是指距离上一次二代检查,一代垃圾检查的次数。10是指距离上一次三代检查,二代垃圾检查的次数。
  gc模块中有一个自动垃圾回收的阈值,可以通过gc.get_threshold()获取到长度为3的元组。每一次计数器的增加,gc模块就会检查增加后的计数器是否达到阈值数目,到达阈值数目就会执行对应的代数的垃圾检查,然后重置计数器。

import gc
print(gc.get_threshold())

  假设阀值是(700,10,10):
  当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查一代对象的垃圾,并重置计数器为(0,4,0);
  当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查一、二代对象的垃圾,并重置计数器为(0,0,1);
  当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)。


5、Python的内存优化

  小整数与大整数对象池:
  Python为了优化速度,使用了小整数池对象,避免为整数频繁申请和销毁内存空间。
  Python对小整数的定义是[-5,256],这些整数对象是提前建立好的,不会被垃圾回收。
  每一个大整数都会创建一个新的对象。

# 小整数
a = 1
print("a:", id(a))

b = 1
print("b:", id(b))

del a
del b

c = 1
print("c:", id(c))
# 大整数
a = 10000
print("a:", id(a))

b = 10000
print("b:", id(b))

6、Python的pep8规范

  PEP8规范,简单说就是一种编码规范,是为了让代码“更好看”,更容易被阅读。 较为重要的注意事项总结如下:

1.缩进:每级缩进使用四个空格(绝对不要混用Tab和空格)2.换行:限制每行的最大长度为79个字符;使用 "\"或"()"控制换行。

3.空行:顶层函数和类之间使用两个空行;类的方法之间用一个空行;在函数中使用空一行表示不同逻辑段落。

4.导入:import位于文件的顶部;不要使用 from foo imort *(不要使用隐式的相对导入)5.空格:在list,dict,tuple,set中的元素后的","后面加一个空格;dict":"后面加一个空格;注释符号#后面加一个空格;操作符两端加一个空格;在参数列表里的"="两端不需要空格。

6.注释:注释要保持与时俱进,一句后面两个空格跟注释符号#。

7.命名规范:使用有意义的,英文单词或词组,绝对不要使用汉语拼音;不要使用大小写的L、大写的O作为变量名;类名首字母大写,内部类加上前导下划线;函数名应该小写,增强可读性可以使用下划线分割。

8.其他:别用"=="进行布尔值和True或者False的比较应该用is

  Python之禅:作为一个Python开发者,如果不知道什么是Python之禅,那是不可饶恕的。

# 打开Python IDE,输入import this会出现一段文字,这就是《The Zen Of Python》:
import this

请添加图片描述

Python之禅,by Tim Peters
 
优美胜于丑陋(Python 以编写优美的代码为目标)
明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)
简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)
复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)
扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)
间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)
可读性很重要(优美的代码是可读的)
即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)
不要包容所有错误,除非你确定需要这样做(精准地捕获异常,不写except:pass风格的代码)
当存在多种可能,不要尝试去猜测
而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)
虽然这并不容易,因为你不是 Python 之父(这里的Dutch是指 Guido)
做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)
如果你无法向人描述你的方案,那肯定不是一个好方案;反之亦然(方案测评标准)
命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

7、Python的命令行参数

  Python可以通过sys模块中的sys.argv来获取命令行参数。

# 创建一个test_1.py的文件
# test_1.py
import sys

print("参数个数为:", len(sys.argv),"个参数")
print("参数列表:", str(sys.argv))

  在命令行中执行:

python test_1.py
python test_1.py 1 2 3 4 5 6 7

请添加图片描述
  argv返回命名行参数是一个列表,第一个元素是.py文件的文件名。sys.argv支持Python的切片操作,可以获取参数,不需要文件名。sys.argv只提供了比较简单的命令参数获取方式,并没有提供命令提示。

# 创建一个test_1.py的文件
# test_1.py
import sys

print("参数个数为:", len(sys.argv),"个参数")
print("参数列表:", str(sys.argv[1:]))

  在命令行中执行:

python test_1.py 1 2 3 4 5 6 7

在这里插入图片描述
  argparse模块:可以轻松编写用户友好的命令行界面。模块定义了它需要的参数,可以顺利解析参数。可以自动生成帮助信息和用法信息。在用户给出无效参数时发出错误。
 基本结构:
 1.引入包;
 2.创建参数对象;
 3.添加参数;
 4.解析对象。
请添加图片描述

# 1 基本结构
import argparse                    # 引入argparse包

parser = argparse.ArgumentParser()  # 创建参数对象
parser.add_argument()              # 添加参数
args = parser.parse_args()         # 类似于类的实例化,解析对象
# 2 创建对象参数
# 类似于创建一个类parser = argparse.ArgumentParser()。
# 需要注意:可以把添加描述信息parser.description = '……' 放到创建对象参数里面,描述信息就是–help时的提示信息
import argparse

parser = argparse.ArgumentParser(description='parser demo')

# parser = argparse.ArgumentParser()
# parser.description = 'parser demo'  # 分开写

parser.add_argument('A', help='argument for A')
args = parser.parse_args()

请添加图片描述

# 3 添加参数
# name or flags(位置参数):必须要写的参数。
import argparse

parser = argparse.ArgumentParser(description='parser demo')
parser.add_argument('a', help='argument for a', type=int)
parser.add_argument('b', help='argument for b', type=int)
args = parser.parse_args()
multi = args.a * args.b
print(multi)

请添加图片描述

# 可选参数参数:以"-"开头的参数是可选的参数。
import argparse

def main():
    parser = argparse.ArgumentParser(description="Demo of argparse")
    parser.add_argument('-n', '--name', default='Lucy')  # "-n","-name"表示同一个参数
    parser.add_argument('-s', '--sex', default='male')  # default表示在默认参数
    args = parser.parse_args()
    print(args)
    name = args.name
    sex = args.sex
    print('Hello {}  {}'.format(name, sex))

if __name__ == '__main__':
    main()

在这里插入图片描述

# choices:参数值只能从几个选项里面选择
import argparse

def get_parser():
    parser = argparse.ArgumentParser(description='choices demo')
    # 输入的值只能从alexnet和vgg中选择
    parser.add_argument('-arch', required=True, choices=['alexnet', 'vgg'])
    # required: 表示这个参数是否一定需要设置
    return parser


if __name__ == '__main__':
    parser = get_parser()
    args = parser.parse_args()
    print('the arch of CNN is {}'.format(args.arch))

在这里插入图片描述

# action:在命令行遇到此参数时要采取的基本操作类型。
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--foo', action='append')
args = parser.parse_args()
print(args.foo)

在这里插入图片描述

# 4 解析参数
# ArgumentParser通过该parse_args()方法解析参数。
# 这将检查命令行,将每个参数转换为适当的类型,然后调用相应的操作。
import argparse

# 创建一个解析器对象
parse = argparse.ArgumentParser(prog="登陆系统", description="系统自定义命令行文件")

# 添加位置参数(必选参数)
parse.add_argument("LoginType", type=str, help="登陆系统类型")

# 添加可选参数
parse.add_argument("-u", dest="user", type=str, help="用户名")
parse.add_argument("-p", dest="pwd", type=str, help="密码")

# 解析参数
result = parse.parse_args()

# 进行简单逻辑判断
if (result.user == "root" and result.pwd == "111111"):
    print("Login Success!")
else:
    print("Login Fail!")

请添加图片描述


总结

  GC作为现代编程语言的自动内存管理机制,专注于两件事:1.找到内存中无用的垃圾资源; 2.清除这些垃圾并把内存让出来给其它对象使用。
  GC彻底把程序员从资源管理的重担中解放出来,投入更多的精力和时间在业务逻辑上。但这并不意味着就可以不去了解GC,毕竟多了解GC的知识有利于写出更健壮的代码。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器视觉小学徒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值