Python拾遗 符合python风格的对象 --流畅的python
本章的notebook文件在这里
符合Python风格的对象
这次来讨论一下如何写出符合Python风格(Pythonic)的对象.
对象的表示形式
如何用字符串来表示Python对象呢, python提供了两种方式: repr
和str
. 前者是以便于开发者理解的方式表示, 后者则是以便于用于理解的方式表示. 我们需要分别实现__repr__
和__str__
两个特殊方法.
我们以Counter
对象为例介绍一下:
1 | from collections import Counter |
'Counter({1: 2, 2: 1, 34: 1})'
我们自己在面向对象编程时, 可以通过实现__repr__
方法来自定义对象的表示形式, 但是一般遵循的原则是最好表示出来的字符串能够被eval
执行, 返回一个满足要求的对象, 而__str__
则可以简单些.
下面以一个学生类为例:
1 | class student: |
1 | ming = student("小明", "男") |
'student("小明", "男")'
1 | # 通过eval 和 repr可以构建一个student对象 |
'我叫小明, 我是男滴'
为了写出符合Python风格的对象, 当有需要时, 我们应该尽可能实现这类用户需要的特殊方法, 在对象表示上, 除了__str__
和__repr__
, 还有__format__
可以让用户根据情况自定义对象的表示格式.
classmethod与staticmethod
在python中, classmethod
和staticmethod
都可以用于定义操作类的方法, 而不是定义实例的方法. 即当我们用该二者在某类里定义了一个方法, 直接可以通过该类调用该方法, 而不用先创建该类的一个实例.
二者的区别在于, classmethod
会默认将类本身做第一个参数, 而staticmethod
则不会. 举例说明:
1 | class Demo: |
1 | Demo.clsm() |
classmethod args: (<class '__main__.Demo'>,)
staticmethod args: ()
classmethod args: (<class '__main__.Demo'>, 1, 2, 3)
staticmethod args: (1, 2, 3)
可散列对象
之前我们定义的student
的实例ming
和ming2
其属性相同, 而且由于其属性都是字符串这种不可变对象, 所以python会认为其可以散列, 但是实际上我们来看:
1 | # 按照我们的设计预期,ming和ming2属性相同, 应该是一致的 |
student("小明", "男")
student("小明", "男")
1 | # 然而其Hash值并不一样 |
ming -9223363254957939347
ming2 8781896836479
1 | # 比较起来它们也不相等 |
False
这里我们需要自己来实现__hash__
方法, 除此之外, 对name
和gender
我们最好将其设置为只读特性(利用@property
装饰器).
当然了, 为了解决比较的问题, 我们也需要实现__eq__
(当然现实中并不是姓名和性别相同就是同一个学生,此处只是为了演示).
修改后的student
如下:
1 | class student: |
1 | ming = student("小明", "男") |
二者是否相等: True
1 | print("ming 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 | class student: |
1 | ming.gender |
'男'
当我们改写该实例的该属性, 只会影响到该实例, 而类属性不变:
1 | ming.gender = "女" |
实例属性 女
类属性 男
如果希望改写类属性, 可以student.gender = "女"
这样来写, 但是更符合Python风格的方法是创建一个该类属性不同默认值的子类.
最后, 记得Python之禅里说的:
简洁胜于复杂
符合Python风格的对象应该是正好符合所需,而不是堆砌语言特性.