Python基础拾遗

112 阅读5分钟

数据类型

graph LR
1[基本数据类型]-->a1[不可变]
1[基本数据类型]-->a2[可变]

a1-->b1[Number]
a1-->b2[String]
a1-->b3[Tuple]
a2-->c1[List]
a2-->c2[Set]
a2-->c3[Dictionary]

b1-->d1[int,float,bool,complex]

垃圾回收机制

Python的垃圾回收机制是基于简单的引用计数+清理来完成的。

引用计数

引用计数追踪到底有多少引用指向了这个对象。 如果我们程序中的一个变量或其他对象引用了目标对象,Python将会增加这个计数值,而当程序停止使用这个对象,则Python会减少这个计数值。当一个对象的引用数变成0的时候才会立即删除掉。

以下情况是导致引用计数加一的情况:

  • 对象被创建,例如a=2
  • 对象被引用,b=a
  • 对象被作为参数,传入到一个函数中
  • 对象作为一个元素,存储在容器中

下面的情况则会导致引用计数减一:

  • 对象别名被显式销毁 del
  • 对象别名被赋予新的对象
  • 一个对象离开他的作用域
  • 对象所在的容器被销毁或者是从容器中删除对象

而对于循环引用这个条件永远不会成立。

标记-清除(Mark and Sweep)算法

  1. 把所有对象放在一个链表中,为这些对象设置ref_count变量和gc_ref变量,ref_count记录对象当前引用计数,gc_ref是ref_count的一个副本。
  2. 逐个遍历链表中的对象,并且将当前对象所引用的所有对象的gc_ref减一。这一步操作就相当于解除了循环引用对引用计数的影响(比如自己引用自己,自己的gc_ref减一)。
  3. 再次扫描所有的容器对象,如果对象的gc_ref值为0,那么这个对象就被标记为不可达;如果对象的gc_ref不为0,那么这个对象就会被标记为可达,同时会递归式的将从该节点出发可以到达的所有节点标记为可达(这一步可以将误标为不可达的对象标为可达)。
  4. 回收被标为不可达的对象

步骤一:

graph LR

A[A: 1] --> B
B[B: 2] --> C[C: 1]
C --> D[D: 1]
D --> E[E: 1]
D --> B


subgraph linklist
    a[A: B] --> b[B: C] --> c[C: D] --> d[D: B,E]
end

步骤二:

遍历,遍历到A时,A引用对象(B)计数减一,以此类推

graph LR

A[A: 1] --> B
B[B: 0] --> C[C: 0]
C --> D[D: 0]
D --> E[E: 0]
D --> B


subgraph linklist
    a[A: B] --> b[B: C] --> c[C: D] --> d[D: B,E]
end

步骤三:

标记计数为0的元素为不可达

graph LR

A[A: RCH] --> B
B[B: nRCH] --> C[C: nRCH]
C --> D[D: nRCH]
D --> E[E: nRCH]
D --> B


subgraph linklist
    a[A: B] --> b[B: C] --> c[C: D] --> d[D: B,E]
end

在上一步中,若A的计数为0,则显然整个链表元素被释放,但A仍然在使用,因此从该节点出发,递归遍历全部引用元素标记为可达。

graph LR

A[A: RCH] --> B
B[B: RCH] --> C[C: RCH]
C --> D[D: RCH]
D --> E[E: RCH]
D --> B


subgraph linklist
    a[A: B] --> b[B: C] --> c[C: D] --> d[D: B,E]
end

分代回收

对于程序,存在一定比例的内存块的生存周期比较短;而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。生存期较短对象的比例通常在 80%~90% 之间,这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。这样在执行标记-清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度。

总体来说,在Python中,主要通过引用计数进行垃圾回收;通过 “标记-清除” 解决容器对象可能产生的循环引用问题;通过 “分代回收” 以空间换时间的方法提高垃圾回收效率。

REFFER

浅/深拷贝

赋值: 对象的引用,也就是给对象起别名

浅拷贝 copy(): 拷贝父对象,但是不会拷贝对象的内部的子对象。

深拷贝 deepcopy(): 拷贝父对象. 以及其内部的子对象

import copy
lst = [[0], [0], [0]]
a = copy.copy(lst)
b = copy.deepcopy(lst)
lst[0][0] = 1
print(a)  # [[1], [0], [0]] 只拷贝存储里程列表引用的外层列表
print(b)  # [[0], [0], [0]]

is和==

Python中对象包含的三个基本要素,分别是:id(身份标识)、type(数据类型)和value(值)。

==是python标准操作符中的比较操作符,用来比较判断两个对象的value(值)是否相等,默认会调用对象的__eq__()方法。

is比较的是两个对象的id值是否相等,也就是比较两个对象是否为同一个实例对象,是否指向同一个内存地址。

a = [1,2,3]
b = a[:]
if a is b:  # False
    print('0')
if a == b:  # True
    print('1')

当对象是数值类型或者字符串时,当两个对象的值相等时,它们用is判断的结果就为True,这是因为它们的id是相等的,在Python中,对于小整数和字符串类型,在创建一个数据对象时,会优先使用缓存中的数据,如果缓存中存在,就会使用同一个数据,对于大整数、浮点数、元组、列表、字典、集合这些数据类型,会重新创建一块内存用于存储数据对象,但它们的元素如果是小整数或字符串时,也会优先使用缓存数据

a = 'ab'
b = 'ab'
c = 'abc'
if a is b:  # True
    print('0')
if a == b:  # True
    print('1')

b = b + 'c'
if c is b:  # False 不打印3
    print('3')

装饰器

def fn2(fn):
    return 1

@fn2  # 意味着,代码中遇到fn1时,将执行fn2(fn1)
def fn1():
    return 0

# 此时fn1指代的并不是函数fn1了,而是fn2(fn1),即为1
print(fn1)  

@fn2就是一个装饰器。它有什么用呢?主要是可以不改变函数fn1()的代码为其添加新功能。如何实现?我们可以定义一个函数fn2()fn2()接受函数作为参数,可以fn2()中运行fn1(),同时实现其他功能。

def fn2(fn):
    print('我是额外的功能')
    fn()
    return 1

@fn2
def fn1():
    return 0

print(fn1())  # 报错,fn1此时是1,显然调用1()是不正确的

上面这样会有问题,代码中的fn1全部变成了int,我们不愿意修改已有的代码,那么可以让fn2直接返回fn即可,则实际执行fn1()时,变为执行fn2(fn1)()

def fn2(fn):
    print('我是额外的功能')
    return fn

@fn2
def fn1():
    return 0

print(fn1())

输出:

我是额外的功能
0

若源函数是带参数的,可以这样

def fn2(fn):  # 装饰器
    print("一些额外的功能")
    def wrapper(a):  # 定义一个函数接收参数(fn1)
        fn(a)  # 用参数执行源函数
    return wrapper  # 返回定义的函数

@fn2
def fn1(a):
    print(a)

fn1(9)

输出:

一些额外的功能
9

REFFER

生成器

迭代器

全局变量

GIL

协程

闭包