一. 如何派生内置不可变类型并修改其实例化行为?
实际案例
我们想要自定义一种新类型的元组, 对于传入的可迭代对象, 我们只保留作其中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 # 简
能否在形式上是属性访问, 但实际内部调用方法呢?
解决方案
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
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return relf.r ** 2 ** 3.14159
# 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
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)