“欸?为什么我的编辑器给我的class标红了啊???”

“你不知道吗?Python的新版本移除了class关键字啊?”

“啊???”

一个早晨

由于不明原因,我突然没法在Python里使用class关键字了!!!这怎么能够接受呢?我的面向对象编程啊!不行不行,没有class我还怎么写代码!突然,一个声音出现在这小小的房间里。

“哦哈哈哈哈,还记得我吗?”

“你是谁??”

“闭包——包——嗷——”神秘人悄然离去,只剩下长长的尾音。

不行!你还我class

第一步!模拟类的行为

闭包这位同学有一个神奇的性质,就是它会保存自己所存在的上下文环境。在某种意义上,其可以很大程度模拟面向对象的行为。下面就是一个简单的闭包的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def f():
a = 0
def g():
nonlocal a
a += 1
print(a)

return g

g1 = f()
g2 = f()

g1()
g1()
g1()
g2()
g2()
g2()

上述代码的输出如下:

1
2
3
4
5
6
1
2
3
1
2
3

可以看到,g1g2各自维护住了自己所在上下文中的a的值,并且没有产生干扰。基于闭包的这个特点,我们可以实现一个简单的类。

第二步!造一个简单的类

通过上面的例子可以看到,通过一个函数返回其内部的函数对象就可以简单地构造出一个闭包。因此,我们先搭好框架。比如我们模拟一个Dog类:

1
2
3
4
5
def Dog(name: str, age: int):
def this():
pass

return this

上述就是一个极其简单的闭包。通过进行函数调用:dog = Dog("Doge", 5),我们拿到了Dog()返回的一个函数对象;又由于闭包的性质,该函数对象外层环境中的局部变量nameage又会被维持住,这就构成了一个简单的闭包。

类一般都具有一部分成员函数,或者称为对象的方法。在这里我们也可以这样做。

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
def Person(name: str, age: int):
def self():
pass

def to_string():
return f"Person(name = {name}, age = {age})"

def get_name():
return name

def get_age():
return age

def set_age(new_age: int):
nonlocal age
age = new_age

def set_name(new_name: str):
nonlocal name
name = new_name

self.to_string = to_string
self.get_name = get_name
self.get_age = get_age
self.set_name = set_name
self.set_age = set_age

return self

首先,我们不使用this而改为self,这符合我们在Python中一贯的使用习惯。同时,我们声明一系列的方法,包括set_nameget_name等。为了便于调用,我们将这些方法的函数对象绑定到self上,这样,我们就可以使用person.get_name()来进行调用了。简单使用一下:

1
2
3
4
5
6
7
p1 = Person("John", 30)
print(p1.to_string())
print(p1.get_name())
print(p1.get_age())
p1.set_name("Alice")
p1.set_age(25)
print(p1.to_string())

其结果如下:

1
2
3
4
Person(name = John, age = 30)
John
30
Person(name = Alice, age = 25)

哦吼!初具雏形。这还需要class关键字干嘛?想到这里,我端起电脑旁的盐汽水,微微抿了一口。 (至于为什么只是抿了一口,那当然是因为优雅)这时,一个声音在我耳畔响起:

“你这算什么?不能继承的类也能叫类?”

“你再骂!”

话虽这么说,但是没有继承的类就像没有醋的饺子,总是缺少了一点灵魂的。

第三步!灵魂继承,浇给

我们先拿到两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def Animal(self, *args, **kwargs):
self.name = kwargs.get("name", "Unknown")

def to_string():
return f"Animal(name = {self.name})"

self.to_string = to_string

return self

def Dog(self, *args, **kwargs):
self.age = kwargs.get("age", 10)

def to_string():
return f"Dog(name = {self.name}, age = {self.age})"

self.to_string = to_string

return self

这两个类都符合我们定义的最基础的类,这里就不多解释了。至于我们为何需要格外加一个self的形参,其在继承中将会起到一个重要的作用。这个例子中,Dog类需要继承Animal类,包括父类的成员变量和成员函数。因此,我们需要对Dog类进行改写。

一种显然的方法是修改Dog类的内部逻辑,使之接受到self形参对应的Animal实例后,依次继承其中的成员变量和成员函数。但这种做法太过臃肿了:如果我们还需要定义一个Cat类,我们是不是也要这要修改Cat类内部呢?这两部分代码是不是几乎一模一样呢?如果我们修改了基类Animal的成员变量,那么我们是不是要修改所有继承自Animal类的类定义呢?

好在,Python中的装饰器能够帮我们进行这部分的代码复用。我们使用@extend()装饰器来表达继承这类关系,因此上述类定义可以改写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@extend(None)
def Animal(self, *args, **kwargs):
self.name = kwargs.get("name", "Unknown")

def to_string():
return f"Animal(name = {self.name})"

self.to_string = to_string

return self

@extend(Animal)
def Dog(self, *args, **kwargs):
self.age = kwargs.get("age", 10)

def to_string():
return f"Dog(name = {self.name}, age = {self.age})"

self.to_string = to_string

return self

其中,@extend(None)表示其为基类,不要继承任何类。因此剩下来的工作就是完成@extend的逻辑。

完成装饰器生成器extend

可以看到,@extend(None)@extend(Animal)才是装饰器,因此extend只是一个装饰器生成器而已,其应该返回一个装饰器。我们先搭建出大致框架:

1
2
3
4
5
def extend(super_cls):
def decorator(cls):
pass

return decorator

装饰器decorator的参数是子类cls,返回值应该是被装饰过后的子类,其也是一个类,因此我们继续完善上述代码:

1
2
3
4
5
6
7
8
9
def extend(super_cls):
def decorator(cls):

def decorated_cls():
pass

return decorated_cls

return decorator

首先,我们需要确定被装饰子类decorated_cls的形参列表。在面向对象编程中,我们初始化子类并不需要传入父类实例,因此,self肯定是多余的形参。因此我们能够确定,返回的被装饰子类形参列表必然不存在self,即decorated_cls(*args, **kwargs)

那么接下来的逻辑是,我们如何处理原本定义中的self形参?显然,这依赖于该类所继承的父类。如果该类本身没有父类(即基类),那么self显然是空对象;反之,self对应的就是使用参数列表所初始化的父类对象。完成上述逻辑即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def extend(super_cls):
def decorator(cls):
def decorated_cls(*args, **kwargs):
# 对应基类情况
if super_cls is None:
return cls(lambda: ..., *args, **kwargs)
# 对应子类情况
else:
super_obj = super_cls(*args, **kwargs)
return cls(super_obj, *args, **kwargs)


return decorated_cls
return decorator

因此,完整的定义即:

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
def extend(super_cls):
def decorator(cls):
def decorated_cls(*args, **kwargs):
if super_cls is None:
return cls(lambda: ..., *args, **kwargs)
else:
super_obj = super_cls(*args, **kwargs)
return cls(super_obj, *args, **kwargs)


return decorated_cls
return decorator



@extend(None)
def Animal(self, *args, **kwargs):
self.name = kwargs.get("name", "Unknown")

def to_string():
return f"Animal(name = {self.name})"

self.to_string = to_string

return self

@extend(Animal)
def Dog(self, *args, **kwargs):
self.age = kwargs.get("age", 10)

def to_string():
return f"Dog(name = {self.name}, age = {self.age})"

self.to_string = to_string

return self

我们测试一下:

1
2
dog = Dog(name="Buddy", age=5)
print(dog.to_string())

运行结果:

1
Dog(name = Buddy, age = 5)

“嗨,不过如此嘛。”又喝了一口剩下的盐汽水,我长舒一口气。

“哼哼,你以为就结束了?”

“不然呢?这个闭包模拟类行为简直是天才想法!”

“真的吗?”突然,我的眼前出现了这么一行代码:

1
2
3
@extend(Animal, Runner)
def Dog(self, *args, **kwargs):
pass

“我要你做出来多继承!!!”

“啊??”

第四步!多继承,启动!

理解上述单继承后,多继承的逻辑只需要在extend中修改即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def extend(*super_cls_list):    # 此时传入的是一个父类列表
def decorator(cls):
def decorated_cls(*args, **kwargs):
# 对应基类情况(基类情况变更为:父类列表长度为零)
if len(super_cls_list) == 0:
return cls(lambda: ..., *args, **kwargs)
# 对应子类情况
else:
# 依次利用参数列表初始化父类,然后将父类的属性绑定到子类super_obj上
super_obj = lambda: ...
for super_cls in super_cls_list:
cur_obj = super_cls(*args, **kwargs)
for k,v in cur_obj.__dict__.items():
if not k.startswith("__"):
setattr(super_obj, k, v)
# 最后利用super_obj初始化self
return cls(super_obj, *args, **kwargs)


return decorated_cls
return decorator
注意

此时需要对extend用法稍作修改:对于基类,不再需要传入None;对于子类,需要传入父类列表而不是以逗号隔开的多个参数

完整代码如下:

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
def extend(*super_cls_list):
def decorator(cls):
def decorated_cls(*args, **kwargs):
if len(super_cls_list) == 0:
return cls(lambda: ..., *args, **kwargs)
else:
super_obj = lambda: ...
for super_cls in super_cls_list:
temp_obj = super_cls(*args, **kwargs)
for k,v in temp_obj.__dict__.items():
if not k.startswith("__"):
setattr(super_obj, k, v)

return cls(super_obj, *args, **kwargs)


return decorated_cls
return decorator



@extend()
def Animal(self, *args, **kwargs):
self.name = kwargs.get("name", "Unknown")

def to_string():
return f"Animal(name = {self.name})"

self.to_string = to_string

return self

@extend()
def Runner(self, *args, **kwargs):
self.speed = kwargs.get("speed", 10)

def to_string():
print(f"{self.name} is running with speed {self.speed}")

self.to_string = to_string

return self

@extend(*[Animal, Runner])
def Dog(self, *args, **kwargs):
self.age = kwargs.get("age", 10)

def to_string():
return f"Dog(name = {self.name}, age = {self.age}, speed = {self.speed})"

self.to_string = to_string

return self

试验一下:

1
2
dog = Dog(name="Buddy", age=5, speed=20)
print(dog.to_string())

结果:

1
Dog(name = Buddy, age = 5, speed = 20)

后记

“小小的class关键字,居然要考虑这么多。”忙活了这么久,终于可以喝完这瓶盐汽水了。

“不对啊,这盐汽水喝不完啊!”

突然,梦醒了。一如既往地打开电脑和VSCode,输入class关键字。

“好像?都是一场梦?”

“哈哈哈哈,”古怪的声音再次响起,“但你学会了不少新东西,不是吗?”这声音又一次离去了,只不过这次再未出现过。

- -