Python优化之__slots__

目录

类中的__dict__

掀起字典的盖头来

开始主题:__slots__

为什么__slots__可以

更详细一点的使用方法

后续计划


类中的__dict__

        每个python类都是隐形的继承自object类,而object类中有一个自带的魔法函数__dict__,也就是说每个python类也都有这个__dict__方法。那么这个方法是干什么用的呢?先一起来试一下!

class TestA(object):

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

test_a = TestA("Jaxx")
print(test_a.name)
print(test_a.__dict__)

# 结果
Jaxx
{'name': 'Jaxx'}

        可以看到输出的结果是一个字典,key和value分别是TestA类的属性和值。而我们在调用test_a.name时,实际上等同于test_a.__dict__["name"]。也就是说每个类在实例化后都有一个字典用来保存对象的实例属性,我们在继续来看看Python中的字典这一数据结构。

掀起字典的盖头来

        字典的底层结果是哈希表(Hash Table),字典中的每个键都占用一个单元(bucket), 一个单元分为两部分, 分别是对键的引用和对值的引用, 使用hash函数获得键的散列值, 散列值对数组长度取余, 取得的值就是存放位置的索引,举个小例子:

  1. 假如我们要把 "name" = "Jaxx"这对值放入一个字典,首先是需要将"name"进行hash计算,得到一个散列值假如说是10001010010101001000110(这也就说明了为什么字典的键必须可以hash)
  2. 用散列值的最右边 3 位数字作为偏移量,即“110”,十进制是数字 6。我们查看偏移量 6,对应的 bucket 是否为空。
  3. 如果为空,则将键值对放进去。
  4. 如果不为空,则依次取移 3 位作为偏移量,即“000”,十进制是数字0,循环此过程,直到找到为空的 bucket 将键值对放进去。
  5. python 会根据散列表的拥挤程度扩容。“扩容”指的是:创造更大的数组,将原有内容拷贝到新数组中。接近 2/3 时,数组就会扩容。
  6. 扩容后,偏移量的数字个数增加,如数组长度扩容到16时,可以用最右边4位数字作为偏移量。同时扩容也就可能会引发散列冲突(散列冲突:由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。

所以说,字典的内存开销实际是很大的,典型的用空间换时间。

说了这么多,其实是想论证,由于Python类使用了__dict__方法,所以在内存空间的消耗上必然也不少,尤其是多属性、创建多个对象的时候。那么有没有什么办法解决这个问题呢?答案就是本文的主题:__slots__

开始主题:__slots__

还是用文章开始的那个例子:

class TestA(object):
    __slots__ = "name"

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


test_a = TestA("Jaxx")
print(test_a.name)
print(test_a.__dict__)

        运行后我们会发现,第一个print可以正常输出结果:Jaxx,而运行到第二个print时却报错了:AttributeError: 'TestA' object has no attribute '__dict__',很明显在类中使用了__slots__后,类实例中就没有了__dict__,也就是说此时类并不会去创建一个字典来存储属性了。那么此时对资源的消耗会更小吗?再一起试一下!

这里我用到了一个统计运行内存的包:ipython_memory_usage

        注意看底部的输出部分,没有使用__slots__时的内存使用为0.3438MB,而使用了之后的内存使用降低到了0.0156MB,足足3倍,不可谓不大!尤其是在多属性、多实例时我想这个差别应该会更大。

        在前面两节的中,我们知道了普通类是使用字典来管理属性,那么__slots__是通过什么方式来节省内存的呢?

为什么__slots__可以

关于这一部分,我能力有限就不献丑了,可以看这位大佬的文章:python slots源码分析,简单来说就是在使用了__slots__之后,slots是直接存储在实例内存上面的,而属性的具体位置的偏移值信息则以member存储在类对象上。后续将会根据这个member创建具体的descriptior,而实际上读写这个属性都是通过descriptior实现的。相对于使用字典的方式来说,少了哈希的这一过程,所以属性的访问速度必然会更快一些了。

总结一下使用__slots__的优点:

  1. 节省内存。因为不使用字典,所以也就不涉及到字典的扩容问题。
  2. 更快的属性访问速度,原因上面已经说了。
  3. 类属性赋值的限制,这个是什么意思呢?先看下面的例子:
class TestC:

    def __init__(self, name) -> None:
        self.name = name
        print(f"TestC -> {self.name}")

c = TestC("Tom")
c.age = 16
c.age

在上面的代码中,构造函数中有一个实例属性name,在实例化类之后我们有增加了一个属性age并且成功赋值,在使用c.age调用时也就可以成功的访问了。从原理上来看,因为类是使用__dict__来存储属性的,所以c.age相当于c.__dict__['age']=16。再来试一下使用__slots__会怎样:

class TestC:
    __slots__ = 'name'
    def __init__(self, name) -> None:
        self.name = name
        print(f"TestC -> {self.name}")

c = TestC("Tom")
c.age = 16
c.age

代码增加了一行__slots__ = 'name',而运行结果就直接报错了:

 原理也很简单,因为__slots__是使用定长列表来存储的类属性,所以也就没办法直接在类外属性赋值了(也更符合封装的意义)。

更详细一点的使用方法

上面的内容在使用__slots__时,因为属性都只用了一个来举例,所以直接__slots__ = 'name'就完成了,那么有多属性的时候呢?答案是使用List或者tuple,严格来说__slots__接受的值应该是可迭代的对象(字符串也属于可迭代的)。

class TestC:
    __slots__ = ['name', 'age']
    def __init__(self, name, age) -> None:
        self.name = name
        self.age = age
        print(f"TestC -> {self.name}")
        print(f"TestC -> {self.age}")


c = TestC("Tom", 19)
print(c.age)
c.age = 16
c.age

# 结果:
TestC -> Tom
TestC -> 19
19
16

需要注意的是,在父子类继承的时候的一些情况:

  1. 这是第一种情况,父类中有__slots__方法,子类中没有。结果是在子类中可以访问到继承自父类的__slots__属性,同时也有_dict__
class Father:
    __slots__ = "name"

    def __init__(self, name) -> None:
        self.name = name

class Son(Father):
    
    def __init__(self, name) -> None:
        super().__init__(name)


son = Son("Jaxx")
print(son.__slots__)
print(son.__dict__)

# 结果:
name
{}
  1. 第二种情况,父类中没有,子类中有。结果是子类有自己的__slots__,同时也继承了父类的__dict__
class Father:

    def __init__(self, name) -> None:
        self.name = name

class Son(Father):
    __slots__ = "name"
    
    def __init__(self, name) -> None:
        super().__init__(name)


son = Son("Jaxx")
print(son.__slots__)
print(son.__dict__)

# 结果
name
{}
  1. 第三种情况,父类中有,子类中也有。结果是子类的__slots__覆盖了父类的。
class Father:
    __slots__ = "name"

    def __init__(self, name) -> None:
        self.name = name

class Son(Father):
    __slots__ = "age"
    def __init__(self, name, age) -> None:
        super().__init__(name)
        self.age = age


son = Son("Jaxx", 14)
print(son.__slots__)
print(son.name, son.age)

# 结果:
age
Jaxx 14

        OK,现在已经把__slots__原理、对比、实际使用方法都大概的介绍了一遍,也算是自己的一个学习的过程吧!回过头来想想自己目前在用的接口测试框架,每条用例都会实例化一个requests对象(二次封装后的),这不正好可以进行优化吗?不多说了,我要去改了!!

后续计划

本文的标题是《Python优化之__slots__》,而优化方法显然不止使用__slots__这一种。再加上目前公司的框架已经处于正常的运行维护期,需要考虑的就是各种的优化补充了,所以我的后续计划就是优化+文档输出,希望可以坚持下去!

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值