Skip to content

Latest commit

 

History

History
152 lines (101 loc) · 8.47 KB

第9章 符合python风格的对象.md

File metadata and controls

152 lines (101 loc) · 8.47 KB

符合python风格的对象

对象表示形式

Python提供了两种获取对象的字符串表示形式的标准方式:

  1. repr():以便于开发者理解的方式返回对象的字符串表示形式,需要实现__repr__特殊方法
  2. str():以便于开发者理解的方式返回对象的字符串表示形式,需要实现__str__特殊方法

为了给对象提供其他的表示形式,还会用到另外两个特殊方法:__bytes____format____bytes__方法与__str__方法类似:bytes( )函数调用它获取对象的字节序列表示形式。而__format__方法会被内置的format( )函数和str.format( )方法调用,使用特殊的格式代码显示对象的字符串表示形式。

在Python 3中,__repr____str____format__都必须返回Unicode字符串(str类型)。只有__bytes__方法应该返回字节序列(bytes类型)。

classmethod与staticmethod

classmethod定义操作类,而不是操作实例的方法。classmethod改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。classmethod最常见的用途是定义备选构造方法,

例如:

@classmethod
def frombytes(cls, octets):
  typecode = chr(octets[0])
  memv = memoryview(octets[1:].cast(typecode))
  return cls(*memv)

注意,frombytes的最后一行使用cls参数构建了一个新实例,即cls(*memv)。按照约定,类方法的第一个参数名为cls(但是Python不介意具体怎么命名)。

staticmethod装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。

格式化显示

内置的format( )函数和str.format( )方法把各个类型的格式化方式委托给相应的.__format__(format_spec)方法。format_spec是格式说明符,它是:format(my_obj, format_spec)的第二个参数,或者str.format( )方法的格式字符串,{}里代换字段中冒号后面的部分

'{0.mass:5.3e}'这样的格式字符串其实包含两部分,冒号左边的'0.mass'在代换字段句法中是字段名,冒号后面的'5.3e'是格式说明符。格式说明符使用的表示法叫格式规范微语言(“Format Specification Mini-Language”)。

格式规范微语言为一些内置类型提供了专用的表示代码。比如,b和x分别表示二进制和十六进制的int类型,f表示小数形式的float类型,而%表示百分数形式:

>>> format(42, 'b')
'101010'
>>> format(2.0/3, '.1%')
'66.7%'

格式规范微语言是可扩展的,因为各个类可以自行决定如何解释format_spec参数。

如果类没有定义__format__方法,从object继承的方法会返回str(my_object)。

可散列的Vector2d

为了把Vector2d实例变成可散列的,必须使用__hash__方法(还需要__eq__方法,前面已经实现了)。此外,还要让向量不可变。

class Vector2d:
  typecode = 'd'
  def __init__(self, x, y):
    self.__x = float(x)
    self.__y = float(y)
  @property
  def x(self):
    return self.__x
  @property
  def y(self):
    return self.__y
  def __iter__(self):
    return (i for i in (self.x, self.y))
  • @property装饰器把读值方法标记为特性。
  • 读值方法与公开属性同名,都是x。

根据特殊方法__hash__的文档,最好使用位运算符异或(^)混合各分量的散列值——我们会这么做。Vector2d.__hash__方法的代码十分简单,如示例:

def __hash__(self):
  return hash(self.x)  ^ hash(self.y)

要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现__hash____eq__方法即可。但是,实例的散列值绝不应该变化,

如果定义的类型有标量数值,可能还要实现__int____float__方法(分别被int( )和float( )构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的complex( )构造函数的__complex__方法。

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

Python有个简单的机制,能避免子类意外覆盖“私有”属性。

如果以__mood的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python会把属性名存入实例的__dict__属性中,而且会在前面加上一个下划线和类名。因此,对Dog类来说,__mood会变成_Dog__mood;对Beagle类来说,会变成_Beagle__mood。这个语言特性叫名称改写(name mangling)。

class A:
    def __init__(self):
        self.__x = 1
        self.__y = 2
    
    def getX(self):
        return self.__x

if __name__ == "__main__":
    a = A()
    print(a.__dict__)
    print(a.getX())
    print(a._A__x)
    
{'_A__y': 2, '_A__x': 1}
1
1

Python解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多Python程序员严格遵守的约定,他们不会在类外部访问这种属性。

Python文档的某些角落把使用一个下划线前缀标记的属性称为“受保护的”属性。使用self._x这种形式保护属性的做法很常见,但是很少有人把这种属性叫作“受保护的”属性。有些人甚至将其称为“私有”属性。

使用__slots__类属性节省空间

默认情况下,Python在各个实例中名为__dict__的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过__slots__类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。

继承自超类的__slots__属性没有效果。Python只会使用各个类中定义的__slots__属性。

class Vector2d:
  __slots__ = ('__x', '__y')
  typecode = 'd'
  ...

在类中定义__slots__属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的__dict__属性。如果有数百万个实例同时活动,这样做能节省大量内存。

如果要处理数百万个数值对象,应该使用NumPy数组。NumPy数组能高效使用内存,而且提供了高度优化的数值处理函数,其中很多都一次操作整个数组。

在类中定义__slots__属性之后,实例不能再有__slots__中所列名称之外的其他属性。

然而,“节省的内存也可能被再次吃掉”:如果把'__dict__'这个名称添加到__slots__中,实例会在元组中保存各个实例的属性,此外还支持动态创建属性,这些属性存储在常规的__dict__中。当然,把'__dict__'添加到__slots__中可能完全违背了初衷,这取决于各个实例的静态属性和动态属性的数量及其用法。粗心的优化甚至比提早优化还糟糕。

此外,还有一个实例属性可能需要注意,即__weakref__属性,为了让对象支持弱引用,必须有这个属性。用户定义的类中默认就有__weakref__属性。可是,如果类中定义了__slots__属性,而且想把实例作为弱引用的目标,那么要把'__weakref__'添加到__slots__中。

__slots__的问题

如果使用得当,__slots__能显著节省内存,不过有几点要注意:

  • 每个子类都要定义__slots__属性,因为解释器会忽略继承的__slots__属性。
  • 实例只能拥有__slots__中列出的属性,除非把'__dict__'加入__slots__中(这样做就失去了节省内存的功效)。
  • 如果不把'__weakref__'加入__slots__,实例就不能作为弱引用的目标。

覆盖类属性

Python有个很独特的特性:类属性可用于为实例属性提供默认值。

如果为不存在的实例属性赋值,会新建实例属性。假如我们为typecode实例属性赋值,那么同名类属性不受影响。然而,自此之后,实例读取的self.typecode是实例属性typecode,也就是把同名类属性遮盖了。借助这一特性,可以为各个实例的typecode属性定制不同的值。

然而,有种修改方法更符合Python风格,而且效果持久,也更有针对性。类属性是公开的,因此会被子类继承,于是经常会创建一个子类,只用于定制类的数据属性。Django基于类的视图就大量使用了这个技术。