通过我们了解了如何用Python的方式理解和使用变量。
通过我们了解了如何用Python的方式使用函数。
现在我们来看如何用Pyton的方式构建和使用对象。
对象的表现形式
首先我们需要说明如何打印一个对象,在Java中我们会通过实现toString
接口去实现,Python中有两个魔术方法:__str__
和__repr__
,分别对应内建函数str()
和repr()
,以及格式化字符串时候会用到的%s
和%r
。
这两者的区别在于__str__
返回的信息更多地是展示给用户,而__repr__
返回的信息则用于展示给开发者,用于调试。
此外,对于str()
,如果__str__
没有被实现,则会自动调用__repr__
。对于print()
,会先尝试调用__str__
,如果没有实现,会调用__repr__
。
class Test():
def __repr__(self):
return "this is a Test class"
class Test2():
def __str__(self):
return "this is a Test2 class"
test = Test()
test2 = Test2()
print(repr(test))
print(str(test))
print(test)
print(repr(test2))
print(str(test2))
print(test2)
# this is a Test class
# this is a Test class
# this is a Test class
# <__main__.Test2 object at 0x00000210B36AF130>
# this is a Test2 class
# this is a Test2 class
接下来我会像《Fluent Python》中展示的那样,使用一个向量类来说明如何构建一个Python式的对象。
向量类
对于一个基本的二维向量,可以简单地表示为:
class Vector():
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
这里使用float
函数对传入参数进行格式转换和验证,避免非法类型的参数传递,这是个好习惯。
我们知道,Python风格和其它传统编程语言的最大区别是可以通过一系列魔术方法将用户自定义类很好地融入Python中,只要你实现相应的魔术方法,那就可以用依赖此魔术方法的内建函数进行调用,表现出的效果就像内生的类一样。
这就是所谓的Python风格对象。
我们现在给向量类添加一些常用的魔术方法:
class Vector():
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __iter__(self):
return (i for i in (self.x,self.y))
vector = Vector(1, 1)
for i in vector:
print(i)
x,y = vector
print(x,y)
# 1
# 1
# 1 1
__iter__
用于返回一个迭代器,在这里用生成器实现。
关于迭代器的详细介绍,会在后续文章中进行。
这里并不能通过返回
(self.x,self.y)
或者[self.x,self.y]
的方式实现,会提示返回的类型不是迭代器,这是因为列表或元组本身实现了__iter__
接口,是可以迭代的,但是他们本身并不是一个迭代器。这里也可以用
yied
实现,作为生成器的一种表现形式,也是合格的迭代器。
实现了__iter__
接口后,我们可以使用for/in
进行迭代访问,或者进行拆包,就像示例中展示的那样。
现在实现==
运算符重载:
def __eq__(self, other):
return tuple(self) == tuple(other)
vector = Vector(1, 2)
vector2 = Vector(1, 2)
print(vector == vector2)
print(vector == (1,2))
# True
# True
需要注意的是,这里转化为元组后进行比较的方式实现。这样会引发一个问题,就像示例中展示的那样,除了可以正常对Vector
类型的实例进行比较以外,还可以将Vector
与一个元组或者列表或者其它可以转化为元组的实例进行比较。这可以看做是一个缺陷,但也可以看做是一个特性,具体取决于开发者实现__eq__
的目的和用途,但是无论如何,开发者都要对此有清醒的认识。
我们再实现__repr__
:
def __repr__(self):
className = type(self).__name__
return "{}({!r}, {!r})".format(className, *self)
vector = Vector(3, 4)
source = repr(vector)
print(source)
vector2 = eval(source)
print(vector2)
print(vector == vector2)
# Vector(3.0, 4.0)
# (3.0, 4.0)
# True
这里需要注意的有三点:
-
我们并没有使用硬编码,而是通过
className = type(self).__name__
的方式获取当前类名,这样有一个好处:更为灵活。如果我们使用的是硬编码,则其子类继承此__repr__
后必然出现问题,除非重写此方法。 -
我们使用
*self
而非self.x,self.y
的方式传递参数给格式化方法,这是因为我们已经实现了__iter__
方法,所以我们的类实例是可以作为一个迭代器进行使用的,当然也可以进行拆包,而*self
则是用拆包的方式传递位置参数,所以当然是可以的。 -
eval
可以通过规范化的repr
字符串来生成实例。
对于第二个要点,还要感慨一下,这就是编译型语言和解释型语言的差异,如果是编译型语言,多半是要出问题的。
简单地实现__str__
:
def __str__(self):
return str(tuple(self))
vector = Vector(1, 2)
print(vector)
# (1, 2)
self
能作为参数传递给tuple
的原因和之前解释的一样。
向量可以进行求摸运算,所以这里实现一个只有数学运算会用到的魔术方法__abs__
:
def __abs__(self):
return math.hypot(self.x, self.y)
vector = Vector(3, 4)
print(abs(vector))
# 5.0
具体运算由Python自带的math
模块实现。
我们还可以通过实现__bool__
提供对布尔运算的支持:
def __bool__(self):
return bool(abs(self))
vector = Vector(3, 4)
vector2 = Vector(0, 0)
print(bool(vector))
print(bool(vector2))
# True
# False
这里利用模运算的结果来进行判断。
最后实现一个最困难的魔术方法__bytes__
,这个方法用于将向量转化为字节序列。
#double
typeCode = 'd'
def __bytes__(self):
toArray = array.array(self.typeCode, self)
return self.typeCode.encode('UTF-8')+bytes(toArray)
vector = Vector(3, 4)
bytesVector = bytes(vector)
print(chr(bytesVector[0]))
print(bytesVector)
# d
# b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
这里利用array
转换,array
的类型码为d
,意为8字节的双精度浮点值(double)。
除此以外,类型码由类变量typeCode
保存,但是我们可以通过实例属性self.typeCode
的方式访问,这种特性稍后会解释。
附上目前为止的Vector
定义:
import math
import array
class Vector():
#double
typeCode = 'd'
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
className = type(self).__name__
return "{}<{!r},{!r}>".format(className, *self)
def __str__(self):
return str(tuple(self))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __bytes__(self):
toArray = array.array(self.typeCode, self)
return self.typeCode.encode('UTF-8')+bytes(toArray)
备选构造方法
既然我们可以已经视线了Vector
的字节序列化,那当然也可以将字节序列的Vector
转化回来。这种操作类似于array
的fromBytes
方法,所以我们也同样命名。
fromBytes
@classmethod
def fromBytes(cls, bytesVector):
codeType = chr(bytesVector[0])
arrayVector = array.array(codeType)
arrayVector.frombytes(bytesVector[1:])
return cls(*arrayVector)
vector = Vector(3, 4)
bytesVector = bytes(vector)
vecotr2 = Vector.fromBytes(bytesVector)
print(vecotr2)
print(vector == vecotr2)
# (3.0, 4.0)
# True
有这么几点需要注意:
-
我们通过
@classmethod
装饰器指明fromBytes
为类方法,同时传入的首个参数不是类实例而是类。 -
我们通过
array
实现转换过度,传入的参数是用切片获取的从第二个字节开始的字节序列。 -
我们使用
cls(*arrayVector)
而非Vecotor(*arrayVecor)
,这同样是为了保持代码的灵活性,保证子类调用的时候不会出现问题。
classmethod与staticmethod
通过之前的学习,我们已经知道classmethod
装饰器可以用来构建一个类方法。
除此以外,还有一个staticmethod
装饰器,这个装饰器就显得很古怪。因为在主流变成语言中,一般静态方法就是类方法,但这里的staticmethod
的定义更像是包含在类定义中的普通方法。
这里举一个简单的例子:
class Test():
@classmethod
def getInstance(cls):
return cls()
@staticmethod
def show():
print('this is a staticmethod')
test = Test.getInstance()
print(test)
Test.show()
# <__main__.Test object at 0x000001527595B8E0>
# this is a staticmethod
在这个例子中,getInstance
作为一个类方法,返回该类的一个实例,这往往也是类方法最常用的功能之一。而show
是个静态方法,它可以不接受任何self\cls
或者别的什么参数,除了在调用时候必须通过类名以外,和别的在全局定义的普通函数没有任何区别。
所以《Fluent Python》对静态方法的评价不高,称其可能并没有多大用途。
但我猜测静态方法的最大用途是避免重名,比如你要创建一组工具函数,里边包含大量诸如len/bytes
等会和已有内建或引入的第三方函数重名,那么我觉得通过类方法定义是个不错的选择。当然,你也可以使用模块名称进行调用,但是我想任何人不会拒绝多一种灵活的选择的。
格式化显示
老实说这是个我极力想避免的论题,毕竟我写PHP代码的时候就不喜欢各种格式化函数,凡是能用"{$test}"
方式实现字符串拼接的,我绝对不会用格式化函数,因为我记不住那些个格式化专用符号。
但是当我们遇到需要输出诸如“小数点后两位”等格式化的数据的时候,我们又不得不把格式化函数捡起来,因为那是最快最简单最安全的实现方式。
Python中,格式化函数主要有str.format
和format
两种。
格式化函数
format
format
函数相对简单,接受两个参数:待格式化的数据和格式化字符串。
print(format(3.1415926, '.2f'))
# 3.14
str.format
str.format
相对复杂一点:
import datetime
now = datetime.datetime.now()
year = now.strftime('%Y')
month = now.strftime('%m')
day = now.strftime('%d')
fmt = "Today is {:4d}-{:0>2d}-{:0>2d} , pi is {pi:.2f}"
formatStr = fmt.format(int(year),int(month),int(day),pi=3.1415926)
print(formatStr)
# Today is 2021-04-26 , pi is 3.14
str.format
支持多个参数,也支持指名传参,例如上边的pi=3.14
。可以发现,{}
起到一个占位符的作用,其中{key:type}
的key
可以接收指名传参,type
就是format
函数中的第二个参数,其正式名称是“格式规范微语言”。
更多
str.format
和“格式规范微语言”的用法可以参考和。
其实上面的示例中关于时间的显示完全不必如此复杂,事实上str.format
具有“扩展性”,它会根据不同的数据类型调用其__format__
函数进行相应的格式化。
我们可以这样:
fmt = "Today is {date:%Y}-{date:%m}-{date:%d} , pi is {pi:.2f}"
还可以更进一步:
fmt = "Today is {date:%Y-%m-%d} , pi is {pi:.2f}"
正如我们所看到的,可以通过datetime
实现的__format__
对其实现格式化输出,类似的,我们也可以给Vector
添加格式化方法。
__format__
首先要说明的是,即使Vector
没有 实现__format__
,也是可以通过格式化函数调用的:
from vector import Vector
vector = Vector(3, 4)
formatVector = format(vector)
print(formatVector)
# (3.0, 4.0)
可以看到,这种情况下是使用了__str__
返回的字符串。而且是不能使用参数的:
from vector import Vector
vector = Vector(3, 4)
formatVector = format(vector,'%s')
print(formatVector)
# Traceback (most recent call last):
# File "D:\workspace\python\test\test.py", line 3, in <module>
# formatVector = format(vector,'%s')
# TypeError: unsupported format string passed to Vector.__format__
我们现在给Vector
添加魔术方法以实现格式化支持:
def __format__(self, format_spec):
fmtX = format(self.x, format_spec)
fmtY = format(self.y, format_spec)
return "({},{})".format(fmtX, fmtY)
我们简单测试一下:
from vector import Vector
vector = Vector(3, 4)
formatVector = format(vector,'.2f')
print(formatVector)
# (3.00,4.00)
除了利用已有的格式化微语言来实现Vector
的格式化,我们还可以定制自己的格式化微语言。
比如我们可以制定一个标识来改变Vector
的显示方式,从直角坐标系转换为极坐标系。
如果你和我一样已经把这部分知识还给高中数学老师的话,可以阅读进行回想。
这里仅摘抄最重要的转换函数:
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, format_spec):
if format_spec.endswith('p'):
r = abs(self)
theta = self.angle()
return "<{},{}>".format(r,theta)
else:
fmtX = format(self.x, format_spec)
fmtY = format(self.y, format_spec)
return "({},{})".format(fmtX, fmtY)
atan2
函数比较复杂,需要考虑x/y所在象限,但好在此函数在很多编程语言中都是内建的,包括Python,可以通过math
模块直接调用。
我们这里使用p
来定义我们显示极坐标的格式化微语言,主要是为了避开格式化微语言定义中的已使用字符,比如f/d/s
等。虽然就算使用了重复字符这里也不一定会影响功能,但这是一个好习惯。
我们现在看一下效果:
from vector import Vector
vector = Vector(3, 4)
print(format(vector,'p'))
print("this is a Vector:{:p}".format(vector))
# <5.0,0.9272952180016122>
# this is a Vector:<5.0,0.9272952180016122>
可散列的Vector
之前我们在讨论过一个Python可散列的Python对象需要满足哪些条件。
其中最重要的一点是在对象创建后,其内容不能发生变化。
在其他常用语言中很容易,比如Java中我们可以通过const
关键字将属性设为只读,而Python没有类似的关键字。但是我们可以使用一个特殊的装饰器property
。
我们先需要将原有属性定义为“私有”:
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
然后通过property
装饰器定义两个与原属性同名的“只读属性”:
def x(self):
return self.__x
def y(self):
return self.__y
可以看到,所谓的“只读属性”是类似于Java中的getter
方法,只不过是和原属性同名,最奇妙的是通过这种方式,原有代码完全无需修改,所有使用self.x
的地方都会自动调用self.x()
。
通过这种方式我们可以将Vector
的属性设置为只读:
from vector import Vector
vector = Vector(3, 4)
vector.x = 1
# Traceback (most recent call last):
# File "D:\workspace\python\test\test.py", line 3, in <module>
# vector.x = 1
# AttributeError: can't set attribute
现在Vector
理论上是可散列的对象了,但还缺少一个对应的哈希算法。
事实上我们可以简单地使用对象的唯一标识符id()
作为哈希值,但是那样并没有实际意义,而且违反散列定义中的“相等的对象必须有相同的哈希值”。
我们可以利用self.x
和self.y
的哈希值来构建:
def __hash__(self):
return hash(self.x)+hash(self.y)
这样符合要求,但这样做的话并不是一个很好的散列算法,太浪费空间。我们知道良好的散列算法必须让哈希值尽可能“分散”地分布的同时还不能太浪费空间,散列算法本质上是一种折中的做法。
所以这里可以使用位运算异或^
:
return hash(self.x)^hash(self.y)
测试一下:
from vector import Vector
vector = Vector(3.02, 4.02)
vector2 = Vector(3.01, 4.01)
print(hash(vector))
print(hash(vector2))
# 1031
# 7
即使是很相似的值,也有一个分布不错的哈希值。
现在,我们可以把Vector
用于仅可以使用散列化数据的地方了,比如集合:
from vector import Vector
vector = Vector(3.02, 4.02)
vector2 = Vector(3, 4)
vector3 = Vector(3, 4)
vectors = {vector,vector2,vector3}
print(vectors)
# {Vector(3.0, 4.0), Vector(3.02, 4.02)}
私有属性和“受保护”的属性
前面我展示了通过类似__x
的做法可以将x
属性设置为私有属性。其实质上是一种名称改写机制。
名称改写
from vector import Vector
import pprint
vector = Vector(3.02, 4.02)
print(dir(vector))
# ['_Vector__x', '_Vector__y', '__abs__', '__bool__', '__bytes__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
# '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'fromBytes', 'typeCode', 'x', 'y']
通过dir
查看对象属性我们可以发现,_Vector__x
和_Vector__y
这两个奇怪的属性。
事实上它们正是类声明中的self._x
和self._y
。
每当在类定义中有属性以双下划线前缀命名后,Python解释器在处理的时候就会以_cls__attr
的方式构建对象属性。理所当然地,在外部程序调用的时候你是调用不到obj.__attr
的,但是这种名称改写机制并不能真正地提供完善的数据保护,事实上依然可以使用obj._cls__attr
的方式访问私有属性:
from vector import Vector
vector = Vector(3.02, 4.02)
print(vector._Vector__x)
vector._Vector__x = 2
print(vector)
# 3.02
# (2, 4.02)
我们可以看到,通过这种方式甚至可以改变原本已经被修改为可散列的Vector
中的属性,这显然是不符合散列定义的。
《Fluent Python》一书把Python的这种机制形容为列车上的紧急制动把手的安全外壳,它是用来防止你在正常状况下“误触”用的,但并不能防止有心人恶意使用紧急制动把手。
事实上Python的这种机制更重要的意义在于完善了OOP中的父类私有属性不能被继承的定义,如果没有这种机制,显然Python的OOP将不完整。
《Fluent Python》还提到有一些大佬提出不应该倚赖此项技术,与其让解释器去“改名”,不如程序员直接将此类属性用"_myCls__attr"之类的名称命名,还可以自己指定有意义的前缀。但在我看来大可不必,那样会让命名复杂化,反而让第三方程序员更难阅读程序。
受保护的属性
OOP中与private
对应的是protected
,在Python中对应的是单下划线前缀类属性:
class Test():
def __init__(self,x):
self._protected_x = x
def __str__(self):
return "Test({:d})".format(self._protected_x)
test = Test(1)
print(test)
test._protected_x = 2
print(test)
# Test(1)
# Test(2)
但就像上面示例展示的那样,这样的方式连“名称改写”机制都不如,完全起不到任何保护目的。用那个火车制动手柄的例子来比喻就是,Python的私有属性是透明塑料外壳,而受保护的属性更像是一张封条,或者干脆是透明胶带之类的东西。
事实上Python原生并不支持此类“受保护”的属性定义,这种方式完全是Python开发者自发地、约定俗成的命名方式,并没有任何Python机制的保障。
__slots__
Python使用字典结构存储对象属性,这点我们通过dir
函数可以很容易查看和验证。这样做,让对象属性的访问可以具有良好的性能,但同时也会带来字典结构的缺点:占空间。
所以如果我们需要创建某个类的海量实例,我们可能需要考虑对这种方式进行优化。
Python提供一个特殊属性__slots__
,正是在这种情况下能排上用场的。
这里依然用Vector
举例:
class Vector():
# double
typeCode = 'd'
__slots__ = ("__x", "__y")
通过定义一个类属性__slots__
,就可以很容易地改变对象属性的存储方式,这种方式定义的类实例的属性,将以元组而非字典结构进行存储,这样会大大减少内存占用。
需要说明的是,__slots__
后需要定义一个包含类实例的所有属性的可迭代容器,当然元组和列表都是可以的,但是推荐使用元组,因为这里的__slots__
属性定义后就不可变更了,所以元组更合适。
__slots__
可以方便快捷地进行内存优化,但使用的时候有一些需要注意的地方:
-
不会被继承。
__slots__
属性并不会被子类继承,所以如果子类也需要内存优化,则需要覆盖__slots__
属性。 -
实例只能拥有
__slots__
列出的属性。__slots__
属性中罗列的属性就是实例可以使用的全部属性,无法在这个基础上新增属性。需要注意的是这种机制是内存优化的副产品,不能以限制属性创建为目的使用__slots__
属性,那不符合Python的风格。 -
弱引用。
我们在中提到了弱引用,事实上要能被弱引用,对象必须有一个属性
__weakref__
,用户创建的类默认就包含此属性,如果要被优化后的对象实例也能被弱引用,则也必须把__weakref__
属性添加到__slots__
中。
事实上,Python的海量数据运算往往是通过NumPy模块实现的,__slots__
只是内建的一种优化选择。
覆盖类属性
我们之前提到过,Vector
中的类属性typeCode
可以以self.typeCode
的方式进行访问,这是Python中的一种机制:如果访问的实例属性不存在,但是有类的同名属性,则会返回类的同名属性。
class Test():
clsAttr = 1
test = Test()
print(test.clsAttr)
# 1
但如果我们试图改写不存在的实例属性,则会创建一个新的实例属性,而非改写相应的类属性:
class Test():
clsAttr = 1
test = Test()
test.clsAttr = 2
print(test.clsAttr)
print(Test.clsAttr)
# 2
# 1
通过这种特殊机制,我们可以给一些实例属性设置“默认值”。
实例属性默认值
__slots__
属性会对我们的演示造成一些麻烦,这里先注释掉:
class Vector():
# double
typeCode = 'd'
# __slots__ = ("__x", "__y")
我们看这个示例:
from vector import Vector
vector = Vector(3, 4)
print(bytes(vector))
vector.typeCode = 'f'
print(bytes(vector))
# b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
# b'f\x00\x00@@\x00\x00\x80@'
默认情况下Vector
会使用类属性typeCode
创建一个double型的array,通过这个array生成字节序列。在这里,我们通过给实例添加一个值为f
的typeCode
属性的方式,通过float型的array生成字节序列,后者为单精度,显然比前者的双精度字节序列长度更短。
更进一步,我们还是可以用这个更短的字节序列来转换回Vector
实例:
bytesV = bytes(vector)
vector2 = Vector.fromBytes(bytesV)
print(vector2)
print(vector == vector2)
# (3.0, 4.0)
# True
在这个例子中,我们只是设置了一个类属性typeCode
,就实现了实例属性拥有“默认值”,以及可以灵活地给实例属性添加新值的特性,这的确非常不错。
老实说,刚学到这个特性的时候我也觉得振奋且有趣。但这两天回想起来,其实用性并不大。我们完全可以通过在初始化方法中直接创建实例属性,并指定默认值,而非是用这种隐式的方式使用。反而是此类做法给代码的可读性设置了障碍,会让一些不了解这个特性的程序员陷入不必要的困惑。此外,使用这种特性还不能和
__slots__
一同使用,如果你尝试过的话,就明白我说的是怎么一回事。所以从收益和付出的角度来说,这个特性并不值得过于推崇和使用,当然,这仅仅是我的个人看法。
最后附上目前为止所有的Vector
代码以供参考:
import math
import array
class Vector():
# double
typeCode = 'd'
# __slots__ = ("__x", "__y")
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
className = type(self).__name__
return "{}({!r}, {!r})".format(className, *self)
def __str__(self):
return str(tuple(self))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __bytes__(self):
toArray = array.array(self.typeCode, self)
return self.typeCode.encode('UTF-8')+bytes(toArray)
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, format_spec):
if format_spec.endswith('p'):
r = abs(self)
theta = self.angle()
return "<{},{}>".format(r,theta)
else:
fmtX = format(self.x, format_spec)
fmtY = format(self.y, format_spec)
return "({},{})".format(fmtX, fmtY)
def fromBytes(cls, bytesVector):
codeType = chr(bytesVector[0])
arrayVector = array.array(codeType)
arrayVector.frombytes(bytesVector[1:])
return cls(*arrayVector)
def x(self):
return self.__x
def y(self):
return self.__y
def __hash__(self):
return hash(self.x) ^ hash(self.y)
if __name__ == '__main__':
vector = Vector(3, 4)
bytesVector = bytes(vector)
vecotr2 = Vector.fromBytes(bytesVector)
print(vecotr2)
print(vector == vecotr2)
# (3.0, 4.0)
# True
好了,以上就是全部内容。
就像《Fluent Python》中强调的那样,上边的所有演示都是为了说明符合Python风格的对象应该是什么样子的,但这并不意味着你的每个Python对象都要如此创建,需要实现所有的魔术方法,那样完全是本末倒置。Python的真正精髓是用最顺手的工具最有效率地完成工作,仅此而已。
最后感慨一下,最近停暖气了,挺冷的,如果不是你们的支持和CSDN上突然暴涨的关注数,我可能没有动力从被窝里爬出来更新博客,这么看的话,在北方,暖气也是生产力啊。
最后,谢谢大家的阅读和支持。
文章评论