zoukankan      html  css  js  c++  java
  • 流畅的python——9 符合 python 风格的对象

    九、符合 python 风格的对象

    绝对不要使用两个前导下划线,这是很烦人的自私行为。

    ​ ——Ian Bicking

    ​ pip、virtualenv 和 Paste 等项目的创建者

    得益于 python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型:我们只需要按照预定行为实现对象所需的方法即可。

    对象表示形式

    repr()

    以便于开发者理解的方式返回对象的字符串表示形式

    str()

    以便于用户理解的方式返回对象的字符串表示形式

    bytes()

    获取对象的字节序列表示形式

    format , str.format __format__

    使用特殊的格式代码显示对象的字符串表示形式

    例子:向量类

    In [1]: from array import array
    
    In [2]: import math
    
    In [3]: class Vector2d:
       ...:     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):
       ...:         class_name = type(self).__name__
        			# 感叹号后面跟的是conversion,而conversion有两个值.
    				# 分别是s对应str()函数, r对应repr()函数。
       ...:         return '{}({!r},{!r})'.format(class_name, *self)  # 可迭代,*self 拆包
       ...:     def __str__(self):
       ...:         return str(tuple(self))
       ...:     def __bytes__(self):
       ...:         return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))
       ...:     def __eq__(self,other):
       ...:         return tuple(self) == tuple(self)
       ...:     def __abs__(self):
       ...:         return math.hypot(self.x,self.y)
       ...:     def __bool__(self):
       ...:         return bool(abs(self))
    
     	...:     @classmethod
        ...:     def frombytes(cls, b):
        ...:         typecode = chr(b[0])
        ...:         memv = memoryview(b[1:]).cast(typecode)  # 将共享内存数据转为 'd' 类型
        ...:         return cls(*memv)
        ...:     def __format__(self,fmt_spec = ''):  # 以 p 结尾的 格式说明符,返回向量形式
        ...:         # compnents = (format(c,fmt_spec) for c in self)
        ...:         #return '({}, {})'.format(*compnents)
        ...:         if fmt_spec.endswith('p'):
        ...:             fmt_spec = fmt_spec[:-1]
        ...:             coords = (abs(self), self.angle())
        ...:             outer_fmt = '<{}, {}>'
        ...:         else:
        ...:             coords = self
        ...:             outer_fmt = '({}, {})'
        ...:         components = (format(c, fmt_spec) for c in coords)
        ...:         return outer_fmt.format(*components)
        ...:
        ...:     def angle(self):
        ...:         return math.atan2(self.y,self.x)
    

    classmethod

    classmethod 最常见的用途是定义备选构造方法。

    staticmethod

    静态方法就是定义在类中的普通函数。

    格式化显示

    __format__(format_spec)

    format_spec 是格式说明符

    1 format(my_obj, format_spec)

    2 str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分

    In [22]: format(brl, '0.4f')  # 格式说明符是 0.4f
    Out[22]: '0.4274'
    
    IIn [23]: '1 BRL = {rate:0.2f} US'.format(rate=brl)  # 格式说明符是 0.2f
    # 代换字段中的 'rate' 子串是字段名称,与格式说明符无关,但是它决定把 .format() 的哪个参数传给代换字段。
    Out[23]: '1 BRL = 0.43 US'
    
    

    第 2 条标注指出了一个重要知识点:'{0.mass:5.3e}' 这样的格式字符串其实包含两部分,冒号左边的 '0.mass' 在代换字段句法中是字段名冒号后面的 '5.3e' 是格式说明符。格式说明符使用的表示法叫格式规范微语言(“Format Specification Mini Language”,https://docs.python.org/3/library/string.html#formatspec)。

    datetime 模块中的类,它们的 __format__·方法使用的格式代码与 strftime() 函数一样。

    >>> from datetime import datetime
    >>> now = datetime.now()
    >>> format(now, '%H:%M:%S')
    '18:49:05'
    >>> "It's now {:%I:%M %p}".format(now)
    "It's now 06:49 PM"
    

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

    然而,如果传入格式说明符, obj.__format__ 会抛出异常。

    In [24]: format(a)
    Out[24]: '(1.0, 2.0)'
    
    In [25]: format(a,'.3f')
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-25-75412ed59581> in <module>
    ----> 1 format(a,'.3f')
    
    TypeError: unsupported format string passed to Vector2d.__format__
    

    自定义 p 结尾,为向量形式

    >>> format(Vector2d(1, 1), 'p')
    '<1.4142135623730951, 0.7853981633974483>'
    >>> format(Vector2d(1, 1), '.3ep')
    '<1.414e+00, 7.854e-01>'
    >>> format(Vector2d(1, 1), '0.5fp')
    '<1.41421, 0.78540>'
    

    可散列的 Vector2d

    当前对象不可散列

    In [40]: a
    Out[40]: Vector2d(7.0,8.0)
    
    In [41]: b = {a : 1}
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-41-12275979062c> in <module>
    ----> 1 b = {a : 1}
    
    TypeError: unhashable type: 'Vector2d'
            
    In [42]: hash(a)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-42-57b555d30865> in <module>
    ----> 1 hash(a)
    
    TypeError: unhashable type: 'Vector2d'
    

    什么是可散列的数据类型

    如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现 __hash__() 方法。另外可散列对象还要有 __qe__() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……

    根据特殊方法 __hash__的文档(https://docs.python.org/3/reference/datamodel.html),最好使用位运算符异或(^)混合各分量的散列值——我们会这么做。

    In [45]: 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))
        ...:     def __hash__(self):
        ...:         return hash(self.x) ^ hash(self.y)
    
    In [57]: a = Vector2d(7,8)
    
    In [58]: a
    Out[58]: <__main__.Vector2d at 0x274a4874cc0>
    
    In [59]: hash(a)
    Out[59]: 15
    
    In [60]: b = Vector2d(1,1)
    
    In [61]: hash(b)
    Out[61]: 0
    

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

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

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

    私有属性:__mood 会存入实例的 __dict__ 属性中,变成 _Dog__mood ,且会在前面加上一个下划线和类名。这个语言特性叫 名称改写。

    名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事。

    只要编写 v1._Vector__x = 7 这样的代码,就能轻松地为 Vector2d 实例的私有分量直接赋值。如果真在生产环境中这么做了,出问题时可别抱怨。

    不过在模块中,顶层名称使用一个前导下划线的话,的确会有影响:对 from mymod import * 来说,mymod 中前缀为下划线的名称不会被导入。然而,依旧可以使用 from mymod import _privatefunc 将其导入。Python 教程的 6.1节“More on Modules”(https://docs.python.org/3/tutorial/modules.html#more-on-modules)说明了这一点。

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

    总之,Vector2d 的分量都是“私有的”,而且 Vector2d 实例都是“不可变的”。我用了两对引号,这是因为并不能真正实现私有和不可变。

    使用 __slots__ 类属性节省空间

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

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

    定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__ 中所含的信息不会变化。

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

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

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

    在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其他属性。这只是一个副作用,不是 __slots__ 存在的真正原因。不要使用 __slots__ 属性禁止类的用户新增实例属性。__slots__ 是用于优化的,不是为了约束程序员。

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

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

    综上,__slots__ 属性有些需要注意的地方,而且不能滥用,不能使用它限制用户能赋值的属性。处理列表数据时 __slots__ 属性最有用,例如模式固定的数据库记录,以及特大型数据集。

    __slots__ 的问题

    1 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。

    2 实例只能拥有 __slots__ 中列出的属性,除非把 '__dict__' 加入 __slots__ 中(这样做就失去了节省内存的功效)。

    3 如果不把 '__weakref__' 加入 __slots__,实例就不能作为弱引用的目标。

    如果你的程序不用处理数百万个实例,或许不值得费劲去创建不寻常的类,那就禁止它创建动态属性或者不支持弱引用。与其他优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用 __slots__属性。

    覆盖类属性

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

    如果想修改类属性的值,应该直接 类.属性 而不是 self.属性,修改类属性,可以修改所有实例的类属性。

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

    这也说明了我在 Vecto2d.__repr__ 方法中为什么没有硬编码 class_name 的值,而是使用 type(self).__name__ 获取。如果硬编码 class_name 的值,那么 Vector2d 的子类(如 ShortVector2d)要覆盖__repr__ 方法,只是为了修改 class_name 的值。从实例的类型中读取类名,__repr__ 方法就可以放心继承。

    __index__ 这个方法的作用是强制把对象转换成整数索引,在特定的序列切片场景中使用,以及满足 NumPy 的一个需求。

    要构建符合 Python 风格的对象,就要观察真正的 Python 对象的行为。 ——古老的中国谚语

    特性有助于减少前期投入

    python 类和实例的所有属性都是公开的,当要避免意外修改了属性,可以实现特性,调用方式不受影响。

    java 没有特性,只能实现读值方法和设值方法。然而,这些方法必须写,但不能保证有用。

    维基的发明人和极限编程先驱 Ward Cunningham 建议问这个问题:“做这件事最简单的方法是什么?”意即,我们应该把焦点放在目标上。提前实现设值方法和读值方法偏离了目标。在 Python 中,我们可以先使用公开属性,然后等需要时再变成特性。

    私有属性的安全性和保障性

    Perl 不会强制你保护隐私。你应该待在客厅外,因为你没收到邀请,而不是因为里面有把枪。

    ​ ——Larry Wall | Perl 之父

    java 提供好的隐私保障,但是只有用 SecurityManager 部署时,才是绝对隐私的。但是,实际并不常用。

    所以,java 的隐私属性也是一种防止意外的措施。通常也不是绝对安全的,也是可以修改的。

    所以,隐私属性并非绝对隐私,而是一种防止意外的措施,是一种约定俗成的规范。

    作者:我的观点是,Java 中的访问控制修饰符基本上也是安全措施,不能保证万无一失——至少实践中是如此。因此,安心享用 Python 提供的强大功能吧,放心去用吧!

  • 相关阅读:
    CSS
    Html5
    [LeetCode] 78. Subsets(子集)
    [LeetCode] 22. Generate Parentheses(括号生成器)
    [LeetCode] 406. Queue Reconstruction by Height(按身高重排队列)
    [LeetCode] 46. Permutations(全排列)
    [LeetCode] 94. Binary Tree Inorder Traversal(二叉树的中序遍历)
    [LeetCode] 338. Counting Bits(数比特位)
    [LeetCode] 763. Partition Labels(标签划分)
    [LeetCode] 20. Valid Parentheses(有效的括号)
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15344599.html
Copyright © 2011-2022 走看看