资讯专栏INFORMATION COLUMN

流畅的python读书笔记-第九章-符合Python风格的对象

fai1017 / 1073人阅读

摘要:以便于用户理解的方式返回对象的字符串表示形式。函数会调用函数,对来说,输出的是一个有序对。此外,还有用于支持内置的构造函数的方法。可散列实现了方法,使用推荐的异或运算符计算实例属性的散列值私有属性最好用命名规则来实现这种方式有好有坏

绝对不要使用两个前导下划线,这是很烦人的自私行为。——Ian Bicking
对象表示形式

repr()
  以便于开发者理解的方式返回对象的字符串表示形式。
str()
  以便于用户理解的方式返回对象的字符串表示形式。

制作一个向量类,和第一章的有点类似
❸ 定义 iter 方法,把 Vector2d 实例变成可迭代的对象,这样才能拆包(例
如,x, y = my_vector)。这个方法的实现方式很简单,直接调用生成器表达式一个接
一个产出分量。

这一行也可以写成 yield self.x; yield.self.y。第 14 章会进一步讨论 iter 特殊方法、生成器表达式和
yield 关键字。

❻ print 函数会调用 str 函数,对 Vector2d 来说,输出的是一个有序对。
❼ bytes 函数会调用 bytes 方法,生成实例的二进制表示形式。
bool 函数会调用 bool 方法,如果 Vector2d 实例的模为零,返回 False,否则
返回 True。

另一种实现
from array import array
import math


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__
        return "{}({!r}, {!r})".format(class_name, *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(other)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

❷ 在 init 方法中把 x 和 y 转换成浮点数,尽早捕获错误,以防调用 Vector2d 函
数时传入不当参数。
❸ 定义 iter 方法,把 Vector2d 实例变成可迭代的对象,这样才能拆包(例
如,x, y = my_vector)。这个方法的实现方式很简单,直接调用生成器表达式一个接
一个产出分量。
这一行也可以写成 yield self.x; yield.self.y。第 14 章会进一步讨论 iter 特殊方法、生成器表达式和
yield 关键字。

"__eq__ 方法",在两个操作数都是 Vector2d 实例时可用,不
过拿 Vector2d 实例与其他具有相同数值的可迭代对象相比,结果也是 True(如
Vector(3, 4) == [3, 4])。这个行为可以视作特性,也可以视作缺陷。
备选构造方法

1.把 Vector2d 实例转换成字节序列了;
2.同理,也应该能从字节序列转换成Vector2d 实例。
3.在标准库中探索一番之后,我们发现 array.array 有个类方法.frombytes(2.9.1 节介绍过)正好符合需求。

代码如下:

from array import array
import math


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__
        return "{}({!r}, {!r})".format(class_name, *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(other)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

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

❸ 从第一个字节中读取 typecode。
❹ 使用传入的 octets 字节序列创建一个 memoryview,然后使用 typecode 转换。
2.9.2 节简单介绍过 memoryview,说明了它的 .cast 方法。
❺ 拆包转换后的 memoryview,得到构造方法所需的一对参数。
内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的
.__format__(format_spec) 方法。format_spec 是格式说明符,它是:
format(my_obj, format_spec) 的第二个参数,或者
str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分

classmethod与staticmethod

定义操作类,而不是操作实例的方法。

classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。

classmethod 最常见的用途是定义备选构造方法,例如示例 9-3 中的frombytes。

注意,frombytes 的最后一行使用 cls 参数构建了一个新实例,即cls(*memv)。

我们对 classmethod 的作用已经有所了解(而且知道 staticmethod 不是特别有 用)

9.5 格式化显示

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的.__format__(format_spec) 方法。

format_spec 是格式说明符,它是:format(my_obj, format_spec) 的第二个参数,或者str.format() 方法的格式字符串,

{} 里代换字段中冒号后面的部分

>>> brl = 1/2.43 # BRL到USD的货币兑换比价
>>> brl
0.4115226337448559
>>> format(brl, "0.4f") # ➊
"0.4115"
>>> "1 BRL = {rate:0.2f} USD".format(rate=brl) # ➋
"1 BRL = 0.41 USD"
格式规范微语言为一些内置类型提供了专用的表示代码。
比如,b 和 x 分别表示二进制和十六进制的 int 类型,f 表示小数形式的 float 类型,而 % 表示百分数形式:
>>> format(42, "b")
"101010"
>>> format(2/3, ".1%")
"66.7%"
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_object)。我们为 Vector2d 类定义了 str 方法
>>> v1 = Vector2d(3, 4)
>>> format(v1)
"(3.0, 4.0)"
向量类做format
from array import array
import math


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__
        return "{}({!r}, {!r})".format(class_name, *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(other)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

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

    def __format__(self, fmt_spec=""):
        components = (format(c, fmt_spec) for c in self) # ➊
        #这里还是有些看不懂,拆包拆出来个啥?
        return "({}, {})".format(*components)
    
        


v1 = Vector2d(3, 4)
print(format(v1))

print(format(v1, ".2f"))

print(format(v1, ".3e"))
增强 format 方法,计算极坐标

我们已经定义了计算模的 abs 方法,因此还要定义一个简单的
angle 方法,使用 math.atan2() 函数计算角度

from array import array
import math


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__
        return "{}({!r}, {!r})".format(class_name, *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(other)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

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

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=""):
        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)


print(format(Vector2d(1, 1), "p"))
print(format(Vector2d(1, 1), ".3ep"))
print(format(Vector2d(1, 1), "0.5fp"))
可散列的Vector2d

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


目前,我们可以为分量赋新值,如 v1.x = 7,Vector2d 类的代码并不阻止这么做。我
们想要的行为是这样的:

>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 7
Traceback (most recent call last):
 ...
AttributeError: can"t set attribute
#为此,我们要把 x 和 y 分量设为只读特性,

不完整代码

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)

使用两个前导下划线(尾部没有下划线,或者有一个下划线),把属性标记为私有

@property 装饰器把读值方法标记为特性。

读值方法与公开属性同名,都是 x。

使用位运算符异或(^)混合各分量的散列值——我们会这么做

要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需
正确地实现 hasheq 方法即可。但是,实例的散列值绝不应该变化,
hash成功了
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> hash(v1), hash(v2)
(7, 384307168202284039)
>>> set([v1, v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

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

此外,还有用于支持内置的 complex() 构造函数的 complex 方法。

Vector2d 或许应该提供__complex__ 方法,不过我把它留作练习给读者。

完整的向量类代码

from array import array
import math


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 __repr__(self):
        class_name = type(self).__name__
        return "{}({!r}, {!r})".format(class_name, *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(other)

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

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=""):
        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)

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)
Python的私有属性和“受保护的”属性
举个例子。有人编写了一个名为 Dog 的类,这个类的内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类:Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉。这是个难以调试的问题。
为了避免这种情况,如果以 mood 的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python 会把属性名存入实例的 __dict 属性中,而且会在前面加上一个下划线和类名。因此,对 Dog 类来说,__mood 会变成 _Dog__mood;对 Beagle类来说,会变成 _Beagle__mood。这个语言特性叫名称改写(name mangling)。
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{"_Vector2d__y": 4.0, "_Vector2d__x": 3.0}
>>> v1._Vector2d__x
3.0
对于私有属性推荐这么做
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员
严格遵守的约定,他们不会在类外部访问这种属性。 遵守使用一个下划线标记对象的私
有属性很容易,就像遵守使用全大写字母编写常量那样容易。
使用 slots 类属性节省空间

但是,如果我们想要限制class的属性怎么办?比如,只允许对Student实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class能添加的属性

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

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

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

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

覆盖类属性

因为 Vector2d 实例本身没有 typecode 属性,所以 self.typecode 默认获
取的是 Vector2d.typecode 类属性的值

Vecto2d.__repr__ 方法中为什么没有硬编码 class_name 的值,而是

使用 type(self).__name__ 获取,如下所示:

 # 在Vector2d类中定义
 def __repr__(self):
 class_name = type(self).__name__
 return "{}({!r}, {!r})".format(class_name, *self)

如果硬编码 class_name 的值,那么 Vector2d 的子类(如 ShortVector2d)要覆盖__repr__ 方法,只是为了修改 class_name 的值。

从实例的类型中读取类名,__repr__ 方法就可以放心继承。

如何利用数据模型处理 Python 的其他功能:
提供不同的对象表示形式、实现自定义的格式代码、公开只读属性,以及通过 hash() 函数支持集合和映射
小总结

所有用于获取字符串和字节序列表示形式的方法:__repr__、__str__、__format__ 和 __bytes__。

把对象转换成数字的几个方法:__abs__、__bool__和 __hash__。

用于测试字节序列转换和支持散列(连同 hash 方法)的 eq 运算符。

format

format 方法,对提供给内置函数 format(obj,format_spec) 的 format_spec

提供给 str.format 方法的"{:«format_spec»}" 位于代换字段中的 «format_spec» 做简单的解析

slots 属性节省内存,以及这么做要注意的问题。

slots 属性有点棘手,因此仅当处理特别多的实例(数百万个,而不是几千
个)时才建议使用。

可散列

实现了 hash 方法,使用推荐的异或运算符计算实例属性的散列值

私有属性最好用 命名规则来实现

self.__x 这种方式有好有坏

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41708.html

相关文章

  • 《java 8 实战》读书笔记 -九章 默认方法

    摘要:类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。只有声明了一个默认方法。由于比更加具体,所以编译器会选择中声明的默认方法。 如果在现存的接口上引入了非常多的新方法,所有的实现类都必须进行改造,实现新方法,为了解决这个问题,Java 8为了解决这一问题引入了一种新的机制。Java 8中的接口现在支持在声明方法的同时提供实现,这听起来让人惊讶!通过两种方式可以完成这种操作。其一...

    phoenixsky 评论0 收藏0
  • 流畅python读书笔记-第一章Python 数据模型

    摘要:第一章数据类型隐式方法利用快速生成类方法方法通过下标找元素自动支持切片操作可迭代方法与如果是一个自定义类的对象,那么会自己去调用其中由你实现的方法。若返回,则会返回否则返回。一个对象没有函数,解释器会用作为替代。 第一章 python数据类型 1 隐式方法 利用collections.namedtuple 快速生成类 import collections Card = collec...

    tomener 评论0 收藏0
  • 流畅python读书笔记-第一章Python 数据模型

    摘要:第一章数据类型隐式方法利用快速生成字典方法方法通过下标找元素自动支持切片操作可迭代方法与如果是一个自定义类的对象,那么会自己去调用其中由你实现的方法。若返回,则会返回否则返回。一个对象没有函数,解释器会用作为替代。 第一章 python数据类型 1 隐式方法 利用collections.namedtuple 快速生成字典 import collections Card = coll...

    FullStackDeveloper 评论0 收藏0
  • 流畅python读书笔记-第四章 编码问题

    摘要:处理文本的最佳实践是三明治要尽早把输入例如读取文件时的字节序列解码成字符串。这种三明治中的肉片是程序的业务逻辑,在这里只能处理字符串对象。 处理文本的最佳实践是Unicode 三明治 要尽早把输入(例如读取文件时)的字节序列解码成字符串。 这种三明治中的肉片是程序的业务逻辑,在这里只能处理字符串对象。 在其他处理过程中,一定不能编码或解码。 对输出来说,则要尽量晚地把字符串编码成字...

    leone 评论0 收藏0
  • 流畅python读书笔记-第11章-接口:从协议到抽象基类

    摘要:自己定义的抽象基类要继承。抽象基类可以包含具体方法。这里想表达的观点是我们可以偷懒,直接从抽象基类中继承不是那么理想的具体方法。 抽象基类 抽象基类的常见用途: 实现接口时作为超类使用。 然后,说明抽象基类如何检查具体子类是否符合接口定义,以及如何使用注册机制声明一个类实现了某个接口,而不进行子类化操作。 如何让抽象基类自动识别任何符合接口的类——不进行子类化或注册。 接口在动态类...

    Kyxy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<