看啥推荐读物
专栏名称: Smoking_Cat
目录
相关文章推荐
今天看啥  ›  专栏  ›  Smoking_Cat

深入理解Python[3] 当我们在讨论变量时,我们在讨论什么

Smoking_Cat  · 掘金  ·  · 2018-12-05 03:08
阅读 14

深入理解Python[3] 当我们在讨论变量时,我们在讨论什么

变量名是内存地址的引用(Reference)

这个概念其实大多数人都晓得了,就简单提一下

当我们给一个变量赋值时,Python会在内存地址中新建一个对象,然后变量名只是指向了这个对象的内存地址,变量名只是一个引用而已

引用计数(Reference Counting)

定义

我们先看看维基百科对引用计数的定义

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。 使用引用计数技术可以实现自动资源管理的目的。 同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

字面意思还是很好理解这个概念的

my_var = 10
#这时候指向存储“10”这个对象的引用为1
my_var_2 = my_var 
#此时引用数目变成了2
my_var = "hello"
#引用数目变成1
my_var_2 = "goodbye"
#引用数目变成0,内存得以释放
复制代码

获取引用数

Python中有两种方法获取对象的应用数目

一个是sys.getrefcount(spam)

还有一个是ctypes.c_long.from_address(address).value

import sys, ctypes
spam = [4,5,6]

r1 = sys.getrefcount(spam)
r2 = ctypes.c_long.from_address(id(spam)).value

print(r1,r2)
复制代码

输出

2 1

这里要注意下,当我们使用函数时,有一个实参传给形参的过程,在这个过程中,形参也是指向了同样的地址,所以在函数终结之前,会有两个引用数

而第二个虽然也是用了id函数,但是id函数调用结束后,行参就被“销毁”了,所以这时候再检测内存地址时,就只有一个引用数了

垃圾回收(Garbage Collector)

循环引用(Circular References)

我们先来看一个简单的例子

定义一个my_var变量,指向ObjectA的内存地址,而ObjectA里面又有一个变量var_1指向ObjectB

如果我们将my_var 指向其他内存地址,比如说my_var = None,这时候ObjectA的内存地址没有被引用,因此被销毁,这导致var_1也被跟着一起销毁,ObjectB的内存地址也在此时没被引用,被销毁

上面一切都没有问题,但是我们看看下面这个情况

与之前不同的是,ObjectB内的变量var_2如果指向ObjectA的话,如果取消my_var 对ObjectA的引用,按照内存管理的规则,最终会导致一个内部循环

这些对象无法被外界获取,但是又实实在在地存在内存中,大量的积累就会导致内存泄漏的情况,这并不是我们希望的,所以就有了垃圾回收的机制

垃圾回收(Garbage Collector)

  1. 可以通过导入gc模块来控制
  2. 默认情况下是开启的
  3. 如果能确保自己不会写出循环引用的代码可以选择将其关闭,以此来释放资源(其实这释放的量很小啦)

下面我们通过代码来看看这个机制

import ctypes, gc
gc.disable()
# 关闭垃圾回收

def ref_count(address):
    return ctypes.c_long.from_address(address).value

def object_by_id(object_id):
    for obj in gc.get_objects():
        if id(obj) == object_id:
            return "Object exists"
    return "Not Found"

class A():
    def __init__(self):
        self.b = B(self)
        print('A: self: {0} , b: {1}'.format(hex(id(self)),hex(id(self.b))))

class B():
    def __init__(self, a):
        self.a = a
        print('B: self: {0} , a: {1}'.format(hex(id(self)),hex(id(self.a))))
        
my_var = A()

复制代码

我们建立来两个类,A中的b变量指向B的实例,而B中的a变量又指向A本身,最后建立A的实例,正好符合上面的循环

我们现在看下A,B的实例个有多少个引用


a_id = id(my_var)
b_id = id(my_var.b)

print("ref_count_of_a:{0}, ref_count_of_b:{1}".format(ref_count(a_id),ref_count(b_id)))
复制代码

输出

B: self: 0x1068480f0 , a: 0x1068480b8
A: self: 0x1068480b8 , b: 0x1068480f0
ref_count_of_a:2, ref_count_of_b:1
复制代码

没问题

现在我们令my_var = None

再来打印输出结果 输出

B: self: 0x10b0ec128 , a: 0x10b0ec0f0
A: self: 0x10b0ec0f0 , b: 0x10b0ec128
ref_count_of_a:1, ref_count_of_b:1
复制代码

由于之前关闭了垃圾回收,所以这里的内部循环就没有被销毁

这时我们即时执行下垃圾回收gc.collect(),就会输出我们想要的结果了

ref_count_of_a:0, ref_count_of_b:0
复制代码

动态类型与静态类型(Dynamic Typing and Static Typing)

Python的定义变量时是动态的,也就是说一个变量名可以直接从一个字符串对象重新指向数字对象,像其它的静态类型的语言,比如Java,如果重新给一个字符串变量赋值数字,就会出错

String my_var = "hello"
mv_var = 123 //(会报错,因为Java的变量是静态类型)
复制代码

有个点提一下,当我们给变量重新赋值时,Python做的事情是在内存中新建一个对象,而不是在原有内存地址上改变对象内容

可变性(Mutability)

对象内部的数据(state)如果可以改变,就叫可变(Mutable)的;否则就是不可变的(Immutable)

Python中不可变的数据类型

  • Numbers(int,float,Booleans,etx)
  • Strings
  • Tuples
  • Frozen Sets
  • User-Defined Classes

可变的数据类型

  • Lists
  • Sets
  • Dictionaries
  • User-Defined Classes

⚠️ 元组是不可变的,但是却可以包含可变的元素,例如如下例子

a = [1,2]
b = [3,4]
t = (a,b)  # 此时t = ([1,2],[3,4])

a.append(3)
b.append(5) # 此时t = ([1,2,3],[3,4,5])

复制代码

共享引用(Shared References)和可变性(Mutablility)

如果你在Python中这样定义

a = 10
b = 10
# id(a) 等于 id(b)
复制代码

Python的内存管理会让两个变量名自动共享同一个内存地址(相当于执行来b),这是对于部分简单的不可变的对象而言

而如果定义任何两个可变的对象,Python则不会这么做

a = [1,2,3]
b = [1,2,3]
# id(a) 不等于 id(b)
复制代码

一切皆为对象

“一切都是对象”在别的编程语言中可能还有点牵强;但是在Python中,这是真真切切的道理

除了简单的数据类型,像一些运算符(+,-,*,/,is,...)这些也是某个类的实例

我们平常接触的function,class(类本身,不是实例)也都是Function Type, Class Type的实例

所以可以得到一个这样的结论

所有的对象(包括函数)都给可以赋值给一个变量

而所有的对象(包括函数)有可以作为参数传递给函数

函数又可以返回一个对象(包括函数)

可以参考如下例子

def square(a):
    return a ** 2

def cube(a):
    return a ** 3

def select_function(fn_id):
    return square if fn_id == 1 else cube

f = select_function(1)
print(f(3))

f = select_function(3)
print(f(3))

def exec_function(fn, n):
    return fn(n)

print(exec_function(cube,5))

复制代码

输出

9
27
125
复制代码

驻留(Interning)

概念

按需复用对象(reusing objects on-demand)

整数驻留

在启动时,Python(CPython)会预载一定范围的整数类型([-5,256])

任何这个范围内的整数在创建时,都会产生一个共享引用,从而减少内存的占用

字符串驻留

当Python代码被编译的时候,有写标识符(identifier)会被驻留,其中包括变量名称,函数名称,类名称等

如果一个字符串长得像标识符(identifier),哪怕是一个无效的的标识符,例如1spam这样的字符串,也有可能会被驻留

通过sys.intern()方法可以强制驻留特定的字符串

为什么需要驻留

速度优化!

如果让Python比较两个字符串是否相等,python是要从字符串的第一个字符开始进行逐个比较的,如果字符串相当长,那么基于逐个比较的方法速度就会特别慢,而驻留之后只需要比较内存地址,可以极大地优化速度

我们来做一个测试

import time, sys

def compare_using_equals(n):
    a = 'a long string that is not interned' * 500
    b = 'a long string that is not interned' * 500
    for i in range(n):
        if a == b:
            pass


def compare_using_interning(n):
    a = sys.intern('a long string that is not interned' * 500)
    b = sys.intern('a long string that is not interned' * 500)
    for i in range(n):
        if a is b:
            pass

e_start = time.perf_counter()
compare_using_equals(10000000)
e_end = time.perf_counter()

i_start = time.perf_counter()
compare_using_interning(10000000)
i_end = time.perf_counter()

print('Compare using equals finished test in {0} seconds \n Compare using interning finished test in {1} seconds'.format(e_end-e_start,i_end-i_start))

复制代码

我们看下输出

Compare using equals finished test in 8.94854034400123 seconds 
Compare using interning finished test in 0.4564070480009832 seconds
复制代码

我们可以看出,驻留的效率比逐个比较的效率快了近20倍

一些其它的优化

常量表达式

当我们输入a = 24*60时,python会提前计算数值,并在编译的时候直接替换该数值

当常量序列表达式的结果的长度小于20时,也会被提前计算

def my_func():
    a = 24*60
    b = (1,2) * 5
    c = 'abc' * 3
    d = 'ab' * 11
    e = 'the quick brown fox' * 5
    f = ['a', 'b'] * 3

print(my_func.__code__.co_consts)
复制代码

输出

(None, 24, 60, 1, 2, 5, 'abc', 3, 'ab', 11, 'the quick brown fox', 'a', 'b', 1440, (1, 2), (1, 2, 1, 2, 1, 2, 1, 2, 1, 2), 'abcabcabc')
复制代码

从上面的结果看到,a,b,c的值被计算后放在了co_consts里面,而d,e的值大于20了,f的值是可变的,所以这三个并没有放到co_consts里面

成员测验

当我们写

def my_func(e):
    if e in [1,2,3]:
        pass
复制代码

这样的代码时,python会将阵列转为元组。

同样的,如果是数组,python也会自动将其转换为冷冻数组(frozenset)

def my_func(e):
    if e in [1,2,3]:
        pass

print(my_func.__code__.co_consts)
复制代码

输出(None, 1, 2, 3, (1, 2, 3))

def my_func(e):
    if e in {1,2,3}:
        pass

print(my_func.__code__.co_consts)
复制代码

输出(None, 1, 2, 3, frozenset({1, 2, 3}))

在成员测试中,set的效率要远远高于阵列或者元组,来做一个测验

import string, time

def membership_test(n,container):
    start = time.perf_counter()
    for i in range(n):
        if 'z' in container:
            pass
    end = time.perf_counter()
    return(end-start)

print('list: %s' % membership_test(10000000,list(string.ascii_letters)),
      'tuple: %s' % membership_test(10000000,tuple(string.ascii_letters)),
      'set: %s' % membership_test(10000000,set(string.ascii_letters)),
      sep='\n')

复制代码

输出

list: 6.4466956019969075
tuple: 6.477438930000062
set: 0.6009954499968444
[Finished in 13.6s]
复制代码

从上面看出,set的速度明显要快很多




原文地址:访问原文地址
快照地址: 访问文章快照