Scott's Blog

学则不固, 知则不惑

0%

了解 Python Magic Method

这篇文章介绍了魔法方法,并带你使用魔法方法实现自定义的序列。

魔法方法

魔法方法是为了增强某个类的特性,它有约定俗成的名字,你只需要实现这个方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Company(object):
def __init__(self, employee_list):
self.employee = employee_list

# 让此类的实例,支持被 for 循环访问内部 employee
def __getitem__(self, item):
return self.employee[item]

company = Company(["Apple", "MicroSoft"])

# 支持 for 循环
for company_name in company:
print(company_name)

# 支持切片
company[:2]

有哪些魔法方法

首先可以分为非数学运算与数学相关。

非数学运算中又有:

1
2
3
4
5
6
7
8
9
10
- 字符串表示,__repr__, __str__
- 集合序列相关,__len__, __get/set/delitem__, __contains__
- 迭代相关, __iter__, __next__
- 可调用, __call__
- with 上下文管理器, __enter__, __exit__
- 数值转换,__abs__, __int/float/bool/...__, __hash__
- 元类相关, __new__, __init__
- 属性相关,__get/setattr__, __get/setattribute__. __dir__
- 属性描述符, __get__, __set__, __delete__
- 协程, __await__, __aiter__, __anext__, __aenter__, __aexit__

数学运算则有一元、二元运算符,算数运算符,位运算符等等,暂时不做过多介绍。

在 Python 中,len 方法有其特殊性。当 len 作用在内置类型如 set, list, dict 上的时候,因为这些结构都是用 C 语言实现的,性能非常高,当 len 计算这些数据结构的长度的时候,会直接读取这个数据结构的长度值(C 会维护一个长度值),而不会遍历该树结构。

应用:自定义序列

序列类型

在了解如何自定义序列类之前,我们先看 python 有哪些内置的序列类,我们将其中分为这几类:

1
2
3
4
5
6
7
8
# 一个容器,可以往里面放东西
- 容器序列:list, tuple, deque
# 非容器
- 扁平序列:str, bytes, bytearray, array.array
# 序列内部元素可以变化
- 可变序列: list, deque, bytearray, array
# 序列内部元素不可变化
- 不可变:str, tuple, bytes

上面这些序列类,你都可以通过 for 循环去访问其内部的元素。

要想实现序列类,则需要实现序列的协议。

可以通过 _collections_abc 了解要实现序列协议所需要的函数。

在序列中,一般都支持 + += extend 方法,但你知道他们的区别吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 初始化列表
l = [1, 2]

# +, c = [1, 2, 3, 4]
c = a + [3, 4]

# +=, a = [1, 2, 3, 4]
a += [3, 4]

# 如果将 += 后面换成元组呢?
# 结果是一样的
a += (3, 4)

# 如果是 + 呢?
# 会报错
c = a + (1, 2)

+= 支持任意序列类型,其背后原理是调用一个魔法函数 __iadd__, 其中又是依赖 __extend__ 方法,内部使用的是 for 循环对元素取值并相加,所以只要是可以迭代的类型,都支持用 += 操作。

另外要注意 list 的 appendextend 方法的区别,extend 是将数组内的值一个一个放入另一个数组,而 append 是将整个数组放入另一个数组。

1
2
3
4
5
6
arr = [1, 2]

arr.extend([3, 4]) # [1, 2, 3, 4]

arr.append([3, 4]) # [1, 2, [3, 4]]
arr.append((3, 4)) # [1, 2, (3, 4)]

实现可切片对象

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import numbers

class DCGroup:
def __init__(self, dpt, industry, staff):
self.dpt = dpt
self.industry = industry
self.staff = staff

# 实现序列协议需要的方法
def __reversed__(self):
self.staff.reverse()

# 这个是实现切片的关键,若没有,切片操作会报错
def __getitem__(self, item):
# 若这样返回,则直接叫切片操作交给了内置的 list 操作
# return self.staff[item]

# 但如果你想要返回的东西是一个 Group 对象呢?
# 这样你就可以一直切片

# 这就需要理解 传入的 item(本质上是一个 slice 对象)
# 如果是根据 index 访问,则是 int 值

cls = type(self)
if isinstance(item, slice):
return cls(
dpt=self.dpt,
industry=self.industry,
staff=self.staff[item]
)
elif isinstance(item, numbers.Integral):
return cls(
dpt=self.dpt,
industry=self.industry,
staff=[self.staff[item]]
)


def __len__(self):
return len(self.staff)

def __iter__(self):
return iter(self.staff)

def __contains__(self, item):
if item in self.staff:
return True
else:
return False

dc = DCGroup(dpt='DC', industry='IMF', staff=['Scott', 'Austin'])

# 使用
dc[:1].staff # ['Scott']
'Scott' in dc # True
len(dc) # 2

for user in dc:
print(user)

reversed(dc)

拓展:维护已排序序列

bisect 是用来处理已排序的升序序列的包,使用的是二分查找。

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

# insort 插入
int_list = []
bisect.insort(int_list, 3)
bisect.insort(int_list, 2)
bisect.insort(int_list, 1)
bisect.insort(int_list, 5)
bisect.insort(int_list, 9)

print(int_list)

# 查找插入的位置会是什么下标
print(bisect.bisect(int_list, 3))

print(bisect.bisect_left(int_list, 3))
print(bisect.bisect_right(int_list, 3))

# 输出
# [1, 2, 3, 5, 9]
# 3
# 2
# 3

Python 中还有其他的数据结构,比如 array, deque.

list 相当于容器,可以存放任意类型,而array 只能存放指定数据类型。

1
2
# array 非常快,list 很灵活
my_array = array.array("i")