本章的notebook文件在这里

符合Python风格的对象

这次来讨论一下如何写出符合Python风格(Pythonic)的对象.

对象的表示形式

如何用字符串来表示Python对象呢, python提供了两种方式: reprstr. 前者是以便于开发者理解的方式表示, 后者则是以便于用于理解的方式表示. 我们需要分别实现__repr____str__两个特殊方法.

我们以Counter对象为例介绍一下:

1
2
3
from collections import Counter
ct = Counter([1,2,34,1,])
repr(ct)
'Counter({1: 2, 2: 1, 34: 1})'

我们自己在面向对象编程时, 可以通过实现__repr__方法来自定义对象的表示形式, 但是一般遵循的原则是最好表示出来的字符串能够被eval执行, 返回一个满足要求的对象, 而__str__则可以简单些.

下面以一个学生类为例:

1
2
3
4
5
6
7
8
9
10
11
12
class student:

def __init__(self, name, gender):
self.name = name
self.gender = gender

def __repr__(self):
class_name = type(self).__name__
return '{}("{!s}", "{!s}")'.format(class_name, self.name, self.gender)

def __str__(self):
return "我叫%s, 我是%s滴"%(self.name, self.gender)
1
2
ming = student("小明", "男")
repr(ming)
'student("小明", "男")'
1
2
3
# 通过eval 和 repr可以构建一个student对象
ming2 = eval(repr(ming))
str(ming2)
'我叫小明, 我是男滴'

为了写出符合Python风格的对象, 当有需要时, 我们应该尽可能实现这类用户需要的特殊方法, 在对象表示上, 除了__str____repr__, 还有__format__可以让用户根据情况自定义对象的表示格式.

classmethod与staticmethod

在python中, classmethodstaticmethod都可以用于定义操作类的方法, 而不是定义实例的方法. 即当我们用该二者在某类里定义了一个方法, 直接可以通过该类调用该方法, 而不用先创建该类的一个实例.

二者的区别在于, classmethod会默认将类本身做第一个参数, 而staticmethod则不会. 举例说明: 

1
2
3
4
5
6
7
8
9
10
11
class Demo:

@classmethod
def clsm(*args):
print('classmethod args:', args)

@staticmethod
def stcm(*args):
print('staticmethod args:', args)


1
2
3
4
Demo.clsm()
Demo.stcm()
Demo.clsm(1,2,3)
Demo.stcm(1,2,3)
classmethod args: (<class '__main__.Demo'>,)
staticmethod args: ()
classmethod args: (<class '__main__.Demo'>, 1, 2, 3)
staticmethod args: (1, 2, 3)

可散列对象

之前我们定义的student的实例mingming2其属性相同, 而且由于其属性都是字符串这种不可变对象, 所以python会认为其可以散列, 但是实际上我们来看: 

1
2
3
4
# 按照我们的设计预期,ming和ming2属性相同, 应该是一致的
print(repr(ming))
print(repr(ming2))

student("小明", "男")
student("小明", "男")
1
2
3
# 然而其Hash值并不一样  
print('ming ',hash(ming))
print('ming2', hash(ming2))
ming   -9223363254957939347
ming2 8781896836479
1
2
# 比较起来它们也不相等
ming == ming2
False

这里我们需要自己来实现__hash__方法, 除此之外, 对namegender我们最好将其设置为只读特性(利用@property装饰器).
当然了, 为了解决比较的问题, 我们也需要实现__eq__(当然现实中并不是姓名和性别相同就是同一个学生,此处只是为了演示).

修改后的student如下: 

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
class student:

def __init__(self, name, gender):
self._name = name
self._gender = gender

@property
def name(self):
return self._name

@property
def gender(self):
return self._gender

def __eq__(self, other):
return (self.name == other.name) and (self.gender == other.gender)

# 用位异或来混合各个分量的散列值
def __hash__(self):
return hash(self.name) ^ hash(self.gender)

def __repr__(self):
class_name = type(self).__name__
return '{}("{!s}", "{!s}")'.format(class_name, self.name, self.gender)

def __str__(self):
return "我叫%s, 我是%s滴"%(self.name, self.gender)
1
2
3
ming = student("小明", "男")
ming2 = student("小明", "男")
print("二者是否相等: ", ming == ming2)
二者是否相等:  True
1
2
print("ming  hash value ", hash(ming))
print("ming2 hash value ", hash(ming))
ming  hash value  3712305523585170166
ming2 hash value  3712305523585170166

这时我们就将该对象变成可散列的了.

Python的私有属性和受保护属性

我们知道C++中有private, public, protect可以避免子类覆盖属性.
例如, 对于student类里面有一个score属性, 但是其子类pri_student的设计者不知道, 覆盖了该属性, 就可能会有一些难以预料的问题, 为了避免这些问题, 可以以__mood的形式(前面两个下划线, 后面没有或仅有一个下划线)来命名属性, 这样python在将该属性存入实例时会加入下划线和类名, 例如对于前例就是_student__socre_pri_student__score, 这样两个属性就能被区分开来了.

这种特性叫做名称改写.

当然有些人不喜欢这种名称改写, 并认为这种行为很烦人自私(当在开源项目中使用时), 并倡议对于需要保护的属性使用一个下划线前缀来标识,例如self._x, 这是一种约定俗成, 大家一般不会在类外访问这种属性,但是需要注意python本身不会对这种属性名做特殊处理.

使用slots类属性节省空间

python中默认会在实例中用__dict__的字典来存储实例属性, 使用__slots__可以转为使用元组来存储这些实例, 当每个实例具有很多属性时(几百万个), 使用__slots__类属性能够显著减少内存空间.

覆盖类属性

python中有一个特性, 即类属性会给实例属性提供默认值.
例如:

1
2
3
4
5
6
7
class student:
# 类属性
gender = "男"

def __init__(self, name):
self.name = name
ming = student("小明")
1
ming.gender
'男'

当我们改写该实例的该属性, 只会影响到该实例, 而类属性不变:

1
2
3
ming.gender = "女"
print("实例属性 ", ming.gender)
print("类属性 ", student.gender)
实例属性  女
类属性   男

如果希望改写类属性, 可以student.gender = "女"这样来写, 但是更符合Python风格的方法是创建一个该类属性不同默认值的子类.

最后, 记得Python之禅里说的:  

简洁胜于复杂  

符合Python风格的对象应该是正好符合所需,而不是堆砌语言特性.