python高级编程第二讲:类与对象深度问题与解决技巧

1. 创建大量实例节省内存

场景:在游戏中,定义了玩家类player,每有一个在线玩家,在服务器内则有一个player的实例,当在线人数很多时,将产生大量实例(百万级),如何节省内存?

先看下面代码:

class Player(object):
    def __init__(self,uid,name,status=0,lever=0):
        '''有默认值在实例化时不用传,但是在赋值时还是要写的'''
        self.uid = uid
        self.name = name
        self.status = status
        self.lever = lever

class Player2(object):
    __slots__ = ['uid','name','status','lever']  #关闭动态绑定属性,在python 中 属性都是通过__dict__进行维护的,动态属性会占用内存,此处关闭动态绑定后,我们不能再通过  类.属性的这种方式新增属性
    def __init__(self,uid,name,status=0,lever=0):
        '''有默认值在实例化时不用传,但是在赋值时还是要写的'''
        self.uid = uid
        self.name = name
        self.status = status
        self.lever = lever

p1 = Player(1,'zjk')
p2 = Player2(2,'zs')

上面代码中,在player2这个类中我们增加了 slots魔法方法,此方法的作用是:关闭动态绑定属性,在python 中 属性都是通过dict进行维护的,动态属性会占用内存,此处关闭动态绑定后,并且限定了只有列表中的几个属性,也可以 用元组传递属性,我们不能再通过 类.属性的这种方式新增属性。会节省内存。

查看上面2个类中所差的属性有哪些
代码如下:

print(set(dir(p1))-set(dir(p2)))   #为什么要转为集合,因为list列表,是不能进行差集运算的,也就是 - 操作

执行结果:
{'_weakref_', '_dict_'}
我们可以看出,当我们设置slots后,这个类里的 上面的2个属性就没了,一个是_weakref_ 弱引用,一个是类中的动态绑定属性

查看类占用的内存空间大小 sys.getsizeof()

import sys
print(sys.getsizeof(p1.__dict__))
print(sys.getsizeof(p2.uid))
print(sys.getsizeof(p2.name))

执行结果:
112
28
51
我们看出,没有关闭动态属性的时候,内存要大

2. 跟踪内存

将上面的代码我们进行改造,引入内存跟踪的类,并且将2个类分别实例化100000次,并打印相应的内存大小


import sys
class Player(object):
    def __init__(self,uid,name,status=0,lever=0):
        '''有默认值在实例化时不用传,但是在赋值时还是要写的'''
        self.uid = uid
        self.name = name
        self.status = status
        self.lever = lever

class Player2(object):
    __slots__ = ['uid','name','status','lever']  #关闭动态绑定属性,在python 中 属性都是通过__dict__进行维护的,动态属性会占用内存,此处关闭动态绑定后,我们不能再通过  类.属性的这种方式新增属性
    def __init__(self,uid,name,status=0,lever=0):
        '''有默认值在实例化时不用传,但是在赋值时还是要写的'''
        self.uid = uid
        self.name = name
        self.status = status
        self.lever = lever


import tracemalloc
tracemalloc.start()
p1 = [Player(1,'zjk') for _ in range(100000)]
p2 = [Player2(2,'zs') for _ in range(100000)]

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

执行结果:
G:/pythonlianxi/spython/logic/python高级/第二讲/demo1.py:6: size=10.7 MiB, count=199993, average=56 B
G:/pythonlianxi/spython/logic/python高级/第二讲/demo1.py:24 p2: size=7837 KiB, count=100005, average=80 B
G:/pythonlianxi/spython/logic/python高级/第二讲/demo1.py:23 p1: size=6274 KiB, count=100001, average=64 B
G:/pythonlianxi/spython/logic/python高级/第二讲/demo1.py:26: size=448 B, count=1, average=448 B
D:\python36\lib\tracemalloc.py:522: size=64 B, count=1, average=64 B

看到此结果我们可能有疑问,为什么 p2 关闭了动态属性为什么占用的空间还比p1大, 其实当 我们创建p1的时候,程序是没有把 dict占用的空间写在一起,也就是上面结果中的 10.7M,我们将代码调整下,只看p1的内存占用大小,大家就能看明白了


import sys
class Player(object):
    def __init__(self,uid,name,status=0,lever=0):
        '''有默认值在实例化时不用传,但是在赋值时还是要写的'''
        self.uid = uid
        self.name = name
        self.status = status
        self.lever = lever

class Player2(object):
    __slots__ = ['uid','name','status','lever']  #关闭动态绑定属性,在python 中 属性都是通过__dict__进行维护的,动态属性会占用内存,此处关闭动态绑定后,我们不能再通过  类.属性的这种方式新增属性
    def __init__(self,uid,name,status=0,lever=0):
        '''有默认值在实例化时不用传,但是在赋值时还是要写的'''
        self.uid = uid
        self.name = name
        self.status = status
        self.lever = lever


import tracemalloc
tracemalloc.start()
p1 = [Player(1,'zjk') for _ in range(100000)]
# p2 = [Player2(2,'zs') for _ in range(100000)]

snapshot = tracemalloc.take_snapshot()
# top_stats = snapshot.statistics('lineno')
top_stats = snapshot.statistics('filename')

for stat in top_stats[:10]:
    print(stat)

执行结果:
G:/pythonlianxi/spython/logic/python高级/第二讲/demo1.py:0: size=16.8 MiB, count=299996, average=59 B
D:\python36\lib\tracemalloc.py:0: size=64 B, count=1, average=64 B

我们再调整代码,只打印p2的

p2 = [Player2(2,'zs') for _ in range(100000)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('filename')

执行结果:
G:/pythonlianxi/spython/logic/python高级/第二讲/demo1.py:0: size=7837 KiB, count=100003, average=80 B
D:\python36\lib\tracemalloc.py:0: size=64 B, count=1, average=64 B

由此结果我们看出,关闭动态属性和没有关闭动态属性占用内存的有差别

当slots 为空的时候是不允许绑定任何的属性,一旦绑定了程序就会抛出异常

关于内存跟踪和分配的用法我们可以参考文章 :https://www.rddoc.com/doc/Python/3.6.0/zh/library/tracemalloc/

3. with 和上下文管理协议

我们常用的with open 文件的方法其实 用到的是上下文管理协议,with就相当于是上下文管理器

  • 我们自己来模拟一个上下文管理器:
class Demo(object):
    def __enter__(self):
        #获取资源
        print("start")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        #释放资源
        print("exit")
        
    def info(self):
        print("info")

with Demo() as f:
    f.info()

注:此时我们要想实现上下文管理器,我们需要在类中实现2个方法,一个是 enter,一个是 exit(),并且enter 中要 写 return self 否则最后在实例调用 info方法时会报错

  • 通过装饰器的方法来完成
    上面第一种实现方法必须是类才能实现我们想要的效果,比较麻烦,我们还可以通过装饰器的方法来完成,下面就来完成通过装饰器将函数变成上下文管理器,想要实现,需要引入 contextlib 类
import contextlib

@contextlib.contextmanager
    #此装饰器是将函数装饰成上下文管理器
def file_open(filename):
    print("file is open")
    yield # 生成器 这一步一定要写,可以写空字典也可以不写  yiels 前后执行的类似于 __enter__中的方法
    # yield 后面相当于执行的是 exit中的
    
    print("file is close")
    
with file_open('demo1.py') as f:
    print('file is operision')

执行结果:
file is open
file is operision
file is close

4. 创建可管理的对象属性

我们常规的作法就是直接去调类的属性来进行赋值和取值,但是此种方法不安全,一旦别人知道了我们的代码,就有可能被别人用来搞破坏,所以我们需要将我们不希望被别人知道 的属性来保护起来,丢给别人一个看似是属性,实际则是内部在调用方法来执行的效果

  • 们先看原始的方法来实现效果的代码
class Person():
    
    def get_name(self):
        return self.name
    
    def set_name(self,name):
        self.name= name

p = Person()

p.set_name('zs')
print(p.get_name())

通过用隐藏属性和setter 、getter等 来实现效果,但是用起来比较麻烦

  • 所以我们再换一种方法,让外界看起来形式上是属性,其实内部是靠调用方法来实现,我们需要用到 property()方法,方法需要传入 set,get 方法
class Person():
    def __init__(self,age):
        self.age = age
        
    def set_age(self,age):
        if not isinstance(age,int):
           raise TypeError('age必须是int类型')
        self.age = age
        
    def get_age(self):
        return self.age
    #定义一个变量用来存储赋值和取值的方法,并且此变量在外界看起来是像直接调用的属性
    R = property(get_age,set_age)

p = Person(18)

p.R=100  # 这里是设置值
print(p.R)   # 这里是取值

此种写法已经满足了我们的需求,但是由于此种方法比较麻烦,所以我们还可以用 property另外一种装饰器的方法来实现,只需要在相应的方法上加上装饰器就可以 实现我们要的效果

改造后的代码:

class Person():
    def __init__(self,age):
        self.age = age
        
    @property
    def s(self):
        return self.age
    
    @s.setter
    def s(self,age):
        if not isinstance(age,int):
           raise TypeError('age必须是int类型')
        self.age = age
p = Person(18)
p.s=100  # 这里是设置值
print(p.s)   # 这里是取值

通过装饰器方法我们可以快速的实现我们的需求,同时对外也保护了我们不想让外界直接访问的属性,安全

5. 如何让2个类进行比较

  • 第一种:实现方法代码示例:
import math
class Rect(object):
    '''计算矩形面积'''
    def  __init__(self,w, h):
        self.w = w
        self.h = h
        
    def area(self):
        #计算面积
        return  self.w * self.h
    
    def __lt__(self, other):
        return self.area() < other.area()
    
    def __gt__(self, other):
        return self.area() > other.area()
    
    def __le__(self, other):
        return self.area() <= other.area()
    
    def __ge__(self, other):
        return self.area() >= other.area()
    
# rect1 = Rect(1,2)
# rect2 = Rect(3,4)
# print(rect1 < rect2)
# ''' 实现单个类不同实例之间的比较'''

#我们再来实现一个圆类的面积计算
class Circle(object):
    def __init__(self,r):
        self.r = r
    
    def area(self):
        return self.r ** 2 * math.pi
    
    def __lt__(self, other):
        return self.area() < other.area()
    
    def __gt__(self, other):
        return self.area() > other.area()
    
    def __le__(self, other):
        return self.area() <= other.area()
    
rect1= Rect(1,4)
circle1 = Circle(3)

print(rect1 < circle1)

执行结果为:
True
上述代码中2个类的比较 > < >= <= 的内部实现其实是靠相应的 魔法来实现,但是上面的代码实现起来比较麻烦,想要做判断,需要将所有将会用到比较方法都 一一列举出来,非常的累,所以我们还需要对代码进行改造,我们可以使用 functools 中的 total_ordering 方法方法来实现,用了魔法方法后,只需要实现比较方法中的任意2个就可以了

  • 第二种:实现方法代码示例:
import math
from functools import total_ordering

@total_ordering
class Rect(object):
    '''计算矩形面积'''
    def  __init__(self,w, h):
        self.w = w
        self.h = h
        
    def area(self):
        #计算面积
        return  self.w * self.h
    
    def __lt__(self, other):
        return self.area() < other.area()
    
    def __gt__(self, other):
        return self.area() > other.area()

    
@total_ordering
class Circle(object):
    def __init__(self,r):
        self.r = r
    
    def area(self):
        return self.r ** 2 * math.pi
    
    def __lt__(self, other):
        return self.area() < other.area()
    
    def __gt__(self, other):
        return self.area() > other.area()
  
rect1= Rect(1,4)
circle1 = Circle(3)

print(rect1 < circle1)

执行结果和上面的一样,我们可以少写很多代码,而且实现也简单

  • 第三种:通过抽象基类的方法来
    由于上面的方法中,代码冗余度比较高,所以我们可以将其封装成一个抽象基类来完成,
    实现方法代码示例:

from  functools import total_ordering
from abc import ABCMeta,abstractclassmethod
import math

@total_ordering
class shape(metaclass=ABCMeta):
#定义一个是抽象类,并且在类中定义一个求面积的方法,子类要继承此抽象类,并对area方法进行重写
    @abstractclassmethod
    def area(cls):
        pass
    #抽象父类中实现比较运算,子类就不需要再写了
    def __lt__(self, other):
        return self.area() < other.area()
    
    def __gt__(self, other):
        return self.area() > other.area()
    
@total_ordering
class Rect(shape):
    def __init__(self,w,h ):
        self.w = w
        self.h = h
    def area(self):
        return self.w * self.h
    
@total_ordering
class Circle(shape):
    def __init__(self,r):
        self.r = r
        
    def area(self):
        return self.r ** 2 * math.pi
    
rect1 = Rect(3,3)
c1= Circle(3)

print(c1 > rect1)

6. 通过实例方法名字的字符串调用方法(了解)

我们定义一个类,用来分别实现不同图形的面积的方法,我们实现一个类,在一个函数中实现求不同的面积
lib1 类的代码如下:

class Triangle:
    def __init__(self, a, b, c):
        self.a, self.b, self.c = a, b, c

    def get_area(self):
        a, b, c = self.a, self.b, self.c
        p = (a + b + c) / 2
        return (p * (p - a) * (p - b) * (p - c)) ** 0.5


class Rectangle:
    def __init__(self, a, b):
        self.a, self.b = a, b

    def getArea(self):
        return self.a * self.b


class Circle:
    def __init__(self, r):
        self.r = r

    def area(self):
        return self.r ** 2 * 3.14159

下面我们用代码实现相应的功能

from lib1 import Triangle,Rectangle,Circle

def get_area(shape):
    method_name = ["get_area","getarea","area"]
    
    for name in method_name:
        f = getattr(shape,name,None)   #类似反射,取得图形中的计算面积的方法,然后赋值给 f
        
        if f:   # 如果有此方法
            return f()  # 调用找到的方法
             
shape1 = Triangle(2,3,4)
shape2 = Circle(2)
shape3 = Rectangle(4,5)

shape_list = [shape1,shape2,shape3]

area_list = map(get_area,shape_list)

print(list(area_list))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容