0%

Python小技巧(7)--类的技巧(其一)

类----来源于cookbook第八章

记录第八章中比较有意思的部分


示例的字符串表示

定义魔法方法__repr__(self)__str__(self)

其中前者尽可能描述eval(repr(x)) == x,后者描述一下该对象的作用

1
2
3
4
5
6
7
8

class c:
def __init__(self):
self.x : int = 1

def __repr__(self):
return "{0.x}".format(self)

格式化代码{0.x}用于指代0的属性x

"{0.x}".format(self)中0的含义就是self

让类直接参与format格式化

定义魔法方法__format__(self, code)即可

1
2
3
4
5
6
7
8
9
10
11
12
13

class Data:
def __init__(self):
self.x = 1

def __format__(self, code):
code = "the x is {}" if code == "" else code
return code.format(self.x)


d = Data()
print(format(d)) # 'the x is 1'

上下文管理协议

定义魔法方法__enter__(self)__exit__(self, exc_ty, exc_val, tb)即可

  • 当遇到with语句时,__enter__()首先被触发,其返回值会被放在with..as..中的as
  • 然后开始执行with之中的代码
  • 最后__exit__()函数被调用用于清理,其三个参数分别代表异常类型、异常值和对挂起的异常的追溯。当它返回True时,表示异常已经被清理干净
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

class Con:
def __init__(self):
...

def __enter__(self):
print("enter !")
return [1,2]

def __exit__(self, exc_ty, exc_val, tb):
print(exc_ty, exc_val, tb)
print("exit !")
return True

with Con() as c:
for i in c:
print(i)
raise Exception("end")

print("test over")

# 输出结果
# enter !
# 1
# 2
# <class 'Exception'> end <traceback object at 0x00000169C1261D80>
# exit !

节省内存的方法

只需要为类中增加__slot__这个类属性,就能大大减少内存占用

当存在这个属性的时候,会使用一种更加紧凑的方式进行表示,不再为每个实例创建__dict__字典,而是一个固定长度的小数组

所以只要包含这个属性,就没法动态为类增加删除属性

1
2
3
4
5
6
7
8
9

class Data:
__slots__ = ["year", "month", "day"]

def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day # 这里的属性名要和__slots__的一致

可管理的属性

使用@property来管理一个属性,可以为值的获取、修改和删除增加额外的限制

此时这个类方法已经类似于属性了,不能被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class Data:
def __init__(self, first):
self.first = first # 注意这里和下面的属性的区别
# 此处用first的目的是为了保证在初始化时也能被类型检查

@property
def first(self):
print("get !")
return self._first

@first.setter
def first(self, value):
print("set !")
self._first = value

@first.deleter
def first(self):
print("delete !")
del self._first

a = Data(123) # set !
print(a.first) # get ! \n 123
a.first = 1 # set !
del a.first # delete !

注意类中__init__中的初始化的方式,如果不这样写,会导致在初始化时不进行检查

本质是将一系列方法绑定到一起,property的属性fget, fset, fdel分别对应了绑定的方法

super调用父类的方法

使用super()函数来调用父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class A:
def __init__(self):
print("A init")

def pri(self):
print("A")

class B(A):
def __init__(self):
print("B init")
super().__init__()

def pri(self):
print("B")
super().pri()

b = B() # B init \n A init
b.pri() # B \n A

super()函数可以避免多态继承时重复调用父类构造函数的问题。

其本质是基于方法解析顺序表(MRO),MRO将多个构造函数线性排列,可以使用class.__mro__查看

super()函数本身可以带两个参数,具体而言为super(subclass, self),含义为搜索subclass的父类,并返回父类的代理对象,而self这是在调用代理对象的方式时被传递。

比如一般都是这样调用super(subclass, self).method(),这会调用subclass的父类的method方法,且将self传递进去。

描述符

可以使用描述符来设计一种通用的数据结构,且可以对数据进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]

def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError("not a int")
instance.__dict__[self.name] = value

def __del__(self, instance):
del instance.__dict__[self.name]

class Point:
x = Integer("x")
y = Integer("y")
def __init__(self, x, y):
self.x = x
self.y = y

描述符定义就像上面Integer所示,感觉有点奇怪,下面进行解释。

首先注意到在使用描述符的类中(此处为Point类),变量声明不是位于任何函数中,而是在类中声明,即在类的层次上定义

对于一个实例p = Point(1,2)

1
2
3
4
5

p.x # 等价于调用Point.x.__get__(p, Point)

Point.x #等价于调用Point.x.__get__(None, Point)

因为可以看到,从类中调用描述符和从实例中调用描述符,传入的instance变量是不同的;且这两者调用从功能上来说也本应该是不同的(从类中调用没有实际意义,因为变量还没被初始化),所以在getset中的处理方法也是不同的

属性惰性求值

本质上是利用描述符中类调用和实例调用存在差异的特性来实现的

即利用一个属性重写同名方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

class lazy:
def __init__(self, func):
self.func = func

def __get__(self, instance, cls):
if instance is None:
return self
else:
value = self.func(instance)
# 关键在这里,用一个属性重写了同名的函数
setattr(instance, self.func.__name__, value)
return value

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

@lazy
def area(self):
print("computing...")
return 3.14*self.r**2


c = circle(2)
print(c.r) # 2
print(c.__dict__) # {'r': 2}
print(c.area) # computing... \n12.56
print(c.__dict__) # {'r': 2, 'area': 12.56}
print(c.area) # 12.56

纯虚基类

使用abc模块的ABCMetaabstractmethod来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

from abc import ABCMeta, abstractmethod

class IStream(metaclass = ABCMeta):
@abstractmethod
def read(self, maxbytes = -1):
...

@abstractmethod
def write(self, data):
...

@property
@abstractmethod
def name(self):
...

abstractmethod可以跟其他装饰器一起使用,但是必须是abstractmethod紧靠函数