6. 类与对象深度深度问题与解决技巧

一. 如何派生内置不可变类型并修改其实例化行为?

实际案例

我们想要自定义一种新类型的元组, 对于传入的可迭代对象, 我们只保留作其中int类型且值大于0的元素, 例如:

IntTuple([1, -1, 'abc', 6, ['x', 'y'], 3]) => (1, 6, 3)

如何继承 内置 tuple  实现 IntTuple?

原理

class A:
  def __new__(cls, *args):
    print('In A.__new__', cls, args)
    return object.__new__(cls)     # 所有实例 均由 它创建, 返回的结果 就是 __init__的self

  def __init__(self, *args):
    print('In A.__init__', args)


a = A(1, 2)

# 流程
# a = A.__new__(A, 1, 2)  获取对象,但没有进行初始化
# A.__init__(a, 1, 2)

# 举例
# 列表在 init之后 才会有值
l = list.__new__(list, 'abc')    # 未初始化的[] 空列表
list.init(l, 'abc')    # 未初始化l    得到 ['a', 'b', 'c']

# 但是  元组 在new 之后, 就有值了
t = tuple.__new__(tuple, 'abc')    # t= ('a','b','c')
tuple.__init__(t, 'abc')           # t 没有什么变化

代码实现实例

class IntTuple(tuple): # tuple 在new中就初始化完成了, 所以要重写new
    def __new__(cls, iterable):
        # 过滤iterable
        f_it = (e for e in iterable if isinstance(e, int) and e > 0)
        return super().__new__(cls, f_it)


int_t = IntTuple([1, -1, 'abc', 6, ['x', 'y'], 3])
print(int_t)


二. 如何创建大量实例节省内存?

实际案例

在某网络游戏中, 定义了玩家类 Player(id, name, level,...)
每有一个在线玩家, 在服务器程序内则有一个 Player 的实例,
当在线人数很多时, 将产生大量实例。 (如百万级)


如何降低这些大量实例的内存开销?

解决方案

  • 定义类的 slots 属性,声明实例有哪些属性(关闭动态绑定)
class Player1:
    def __init__(self, uid, name, level):
        self.uid = uid
        self.name = name
        self.level = level

class Player2:
    __slots__ = ['uid', 'name', 'level']
    def __init__(self, uid, name, level):
        self.uid = uid
        self.name = name
        self.level = level

p1 = Player1('0001', 'Jim', '20')
p2 = Player2('0001', 'Jim', '20')

set(dir(p1)) - set(dir(p2))    
# 发现p1属性 多于p2   
# 多了  __dict__,  和__weakref__两个属性
# 主要在__dict__  属性上 会多消耗内存
# __dict__ 动态维护 实例属性  像self.uid  self.name  这些都是它维护的, 
# __dict__ 可以动态添加属性  如  p1.newadd = 1   会自动在p1.__dict__的字典中加入 'newadd': 1


import sys
sys.getsizeof(p1.__dict__)   # 864
sys.getsizeof(p1.name)       # 52
sys.getsizeof(p1.level)      # 28
sys.getsizeof(p1.uid)        # 53
# 可以看到 __dict__浪费了一部分内存, 如果实例比较少, 问题不大, 但实例非常多, 会非常浪费内存

# 如果使用了__slots__   就提前确定的内存, 无法对实例动态添加属性了  像 p2.newadd = 1 就会报错,无法实现。 
# 原有属性不受影响   p2.name = "newname"  都是可行的




三. 如何让对象实现上下文管理?

实际案例

我们实现了一个 telnet 客户端的类 TelnetClient, 调用实例的 connect(), login(), interact() 方法启动与服务器交互,
交互完毕后需调用 cleanup() 方法, 关闭已连接的 socket, 以及将操作系统历史记录写入文件并关闭。

能否让 TelnetClient 的实例支持上下文管理协议, 从而替代手工调用 connect(),  cleanup() 方法?

解决方案

  • 利用 __enter__和__exit__实现with
from sys import stdin, stdout
import getpass
import telnetlib
from collections import deque

class TelnetClient:
    def __init__(self, host, port=23):
        self.host = host
        self.port = port 

    def __enter__(self):
        self.tn = telnetlib.Telnet(self.host, self.port)
        self.history = deque([])  # 双端队列保存历史记录
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        print('IN __exit__', exc_type, exc_value, exc_tb) # 如果有异常, 分别是类型  值  调用栈, 没有异常都是None

        self.tn.close()
        self.tn = None

        with open('history.txt', 'a') as f:
            f.writelines(self.history)

        return True

    def login(self):
        # user
        self.tn.read_until(b"login: ")
        user = input("Enter your remote account: ")
        self.tn.write(user.encode('utf8') + b"\n")

        # password
        self.tn.read_until(b"Password: ")
        password = getpass.getpass()
        self.tn.write(password.encode('utf8') + b"\n")
        out = self.tn.read_until(b'$ ')
        stdout.write(out.decode('utf8'))

    def interact(self):
        while True:
            cmd = stdin.readline()
            if not cmd:
                break

            self.history.append(cmd)
            self.tn.write(cmd.encode('utf8'))
            out = self.tn.read_until(b'$ ').decode('utf8')

            stdout.write(out[len(cmd)+1:])
            stdout.flush()

# client = TelnetClient('192.168.0.105')
# client.connect()
# client.login()
# client.interact()
# client.cleanup()

with TelnetClient('192.168.0.105') as client:
    raise Exception('TEST')   # 测试异常的打印
    client.login()
    client.interact()

print('END')



四. 如何创建可管理的对象属性?

实际案例

在面对对象编程中, 我们把方法(函数)看作对象的接口。 直接访问对象的属性是不安全的,
或者设计上不够灵活。 但是使用调用方法在形式上不如访问属性简洁


circle.get_radius()
circle.set_radius(5.0)  # 繁


circle.radius
circle.radius = 5.0     # 简


能否在形式上是属性访问, 但实际内部调用方法呢?

解决方案

  • 使用property
import math

class Circle:
    '''
    分别使用property的两种使用方法, 实现 面积s 和 半径r 的操作
    '''
    def __init__(self, radius):
        self.radius = radius

    def get_radius(self):
        return round(self.radius, 1)

    def set_radius(self, radius):
        if not isinstance(radius, (int, float)):
            raise TypeError('wronge type')
        self.radius = radius

    # property用法一
    # @property  和  @函数名.setter装饰器
    @property
    def S(self):
        return self.radius ** 2 * math.pi

    @S.setter
    def S(self, s):
        self.radius = math.sqrt(s / math.pi)

    # property 用法二  
    # 参数分别是  属性的访问 属性的赋值  属性的删除   都是函数参数
    R = property(get_radius, set_radius)

c = Circle(5.712)

c.S = 99.88
print(c.S)    # 99.880000000
print(c.R)    # 5.6
print(c.get_radius())     # 5.6

五. 如何让类支持比较操作?

实际案例

有时我们希望自定义类的实例间可以使用  <, <=, >, >=, ==, != 符号进行比较, 我们自定义比较的行为。
例如, 有一个矩形的类, 比较两个矩形的实例时, 比较的是它们的面积:

class Rectangle:
    def __init__(self, w, h):
        self.w = w
        self.h = h
    def area(self):
        return self.w*self.h
        

rect1 = Rectangle(5, 3)
rect2 = Rectangle(4, 4)

想要支持比较操作: rect1 > rect2     怎么办?

解决方案

  • 利用 __lt__小于,__eq__等于等魔法方法
  • 比较的方法写在抽象类中, 让其他类继承即可减少 代码编写量
  • 使用total_ordering装饰器装饰抽象基类来简化实现过程
from functools import total_ordering

from abc import ABCMeta, abstractclassmethod

@total_ordering   # 修饰后, 只要实现__eq__ 和 剩下的比较操作中 随便实现一个, 就能实现所有比较
class Shape(metaclass=ABCMeta):  # 定义抽象类
    @abstractclassmethod   # 定义抽象方法 
    def area(self):
        pass

    def __lt__(self, obj):
        print('__lt__', self, obj)
        return self.area() < obj.area()

    def __eq__(self, obj):
        return self.area() == obj.area()

class Rect(Shape):
    def __init__(self, w, h):
        self.w = w
        self.h = h

    def area(self):
        return self.w * self.h

    def __str__(self):
        return 'Rect:(%s, %s)' % (self.w, self.h)

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

    def area(self):
        return self.r ** 2 * math.pi


rect1 = Rect(6, 9) # 54
rect2 = Rect(7, 8) # 56
c = Circle(8)  # 201.06

print(rect1 < c)  # True
print(c > rect2)  # True


六. 如何使用描述符对实例属性做类型检查?

实际案例

在某些项目中, 我们实现了一些类, 并希望能像静态语言那样(C, C++, Java) 对它们的实例属性做类型检查


p = Person()
p.name = 'Bob'      #必须是str
p.age = 18          #必须是int
p.height = 1.78     #必须是float


要求:
1. 可对实例属性指定类型
2. 赋予不正确类型时抛出异常

解决方案

  • 使用 __ dict __ 的特性
  • 综合使用 __ se t__, __ get __, __ delete __
class Attr:
    def __init__(self, key, type_):
        self.key = key
        self.type_ = type_

    def __set__(self, instance, value):
        print('in __set__')
        if not isinstance(value, self.type_):
            raise TypeError('must be %s' % self.type_)
        instance.__dict__[self.key] = value

    def __get__(self, instance, cls):
        print('in __get__', instance, cls)
        return instance.__dict__[self.key]

    def __delete__(self, instance):
        print('in __del__', instance)
        del instance.__dict__[self.key]

class Person:
    name = Attr('name', str)
    age = Attr('age', int)

p = Person()
p.name = 'cannon'   # 会调用__set__方法
p.age = '26'   # '32'不是int, 会报错



七. 如何在环状数据结构中管理内存?

实际案例

在python中, 垃圾回收器通过引用计数来回收垃圾对象,但在某些环状数据结构中(树, 图...),
存在对象间的循环引用, 比如树的父节点引用子节点, 子节点引用父节点。 此时同时 del掉引用父子节点, 两个对象不能被立即回收( 引用计算无法变为0)


如何解决此类的内存管理问题?

解决方案

  • 使用弱引用 (不会增加引用计数的引用)
  • 使用标准库weakref.ref() 创建弱引用
  • 对于链表, 可以右node引用计数为1, 左引用计数为0
  • 下面例子中 如果left是弱引用, 要得到引用就得head.left() , 为了不要(), 使用property
import weakref
class Node:
  '''
  链表
  '''
  def __init__(self, data):
      self.data = data
      self._left = None
      self.right = None

  def add_right(self, node):
      self.right = node                 # 右node  引用为1
      node._left = weakref.ref(self)   # 左node  变为弱引用

  @property
  def left(self):     # property使 left和right 都是引用, 避免一边是弱引用,一边是引用的不对称情况
      return self._left()  # 弱引用要  加()

  def __str__(self):
      return 'Node:<%s>' % self.data

  def __del__(self): # 引用计数为0 时 调用
      print('in __del__: delete %s' % self)

def create_linklist(n):
    head = current = Node(1)   # 创建头节点
    for i in range(2, n + 1):
        node = Node(i)
        current.add_right(node)
        current = node
    return head

head = create_linklist(100)
print(head.right, head.right.left)
input()
head = None  # head.right引用没了,导致后面一系列节点的引用为0, 触发__del__

八. 如何通过方法名字的字符串调用方法?

实际案例

在某项目中, 我们的代码使用了三个不同库中的图形类:
    Circle, Triangle, Rectangle
    
它们都有一个获取图形面积的接口(方法), 但接口名字不同。我们可以实现一个统一的获取面积的函数,
使用每种方法名进行尝试, 调用相应类的接口。

解决方案

  • lib1
# lib1
class Circle:
    def __init__(self, r):
        self.r = r
    
    def area(self):
        return relf.r ** 2 ** 3.14159

  
  • lib2
# lib2
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
  • lib3
# lib3
class Rectangle:
    def __init__(self, a, b):
        self.a, self.b = a, b

    def getArea(self):
        return self.a * self.b
  • 对Circle, Rectangel, Triangle 求面积
  • 方法一: 使用内置函数getattr, 通过名字获取方法对象, 然后调用
  • 方法二: 使用标准库operator 下的methodcaller函数调用
from lib1 import Circle
from lib2 import Triangle
from lib3 import Rectangle
from operator import methodcaller

def get_area(shape, method_name = ['area', 'get_area', 'getArea']):  # 方法的名字都先放入一个列表中
    for name in method_name:
        if hasattr(shape, name):
            return methodcaller(name)(shape)
        # 或者
        # f = getattr(shape, name, None)
        # if f:
        #     return f()


shape1 = Circle(1)
shape2 = Triangle(3, 4, 5)
shape3 = Rectangle(4, 6)

shape_list = [shape1, shape2, shape3]
# 获得面积列表
area_list = list(map(get_area, shape_list))
print(area_list)

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/83279499