python核心笔记-9.符合python风格的对象

python核心笔记-9.符合python风格的对象

1、对象表现形式

python提供了两种获取对象的字符串表示形式的标准方式,两者的功能是一致的,只是使用的情境不同:

  • repr(): 以便于开发者理解的方式返回对象的字符串表示形式。这是交互环境中调用,例如在cmd的python,直接输入实例名就可以打印字符串信息。
  • str(): 以便于用户理解的方式返回对象的字符串表示形式。简单来讲就是用.py脚本时,用print语句输出字符串信息。

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

2、向量类

为了说明生成对象表现形式的众多方法,通过实现一个Vector2d类来展示。

Vector2d具有的基本行为如下:

>>> v1 = Vector2d(3, 4) 
>>> print(v1.x, v1.y)#Vector2d实例的分量可以通过属性直接访问
3.0 4.0 
>>> x, y = v1 ➋ #Vector2d实例可以拆包成元组
>>> x, y 
(3.0, 4.0) 
>>> v1 ➌ #repr函数调用Vector2d实例,得到的结果类似于构建实例的源码
Vector2d(3.0, 4.0) 
>>> v1_clone = eval(repr(v1))#使用eval函数表明repr函数调用Vector2d实例得到的对构造方法的准确描述。
>>> v1 == v1_clone ➎ #Vector2d实例支持使用==比较;
True 
>>> print(v1)#print 函数会调用 str 函数,对 Vector2d 来说,输出的是一个有序对
(3.0, 4.0)
>>> octets = bytes(v1)#bytes 函数会调用 __bytes__ 方法,生成实例的二进制表示形式。
>>> octets b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' 
>>> abs(v1)#abs 函数会调用 __abs__ 方法,返回 Vector2d 实例的模。
5.0 
>>> bool(v1), bool(Vector2d(0, 0))#bool 函数会调用 __bool__ 方法,如果 Vector2d 实例的模为零,返回 False,否则返回 True。

相应的实现源码:目前定义的都是特殊方法。

from array import array
import math

class Vector2d:
    typecode = 'd' #typecode 是类属性,在 Vector2d 实例和字节序列之间转换时使用。

    def __init__(self, x, y):
        """
        初始化方法将x,y转化成浮点数,尽早捕获错误,以防调用Vector2d函数时传入不当参数
        """
        self.x = float(x)
        self.y = float(y)

    def __iter__(self):
        """
        定义__iter__方法,把Vector2d实例变成可迭代的对象,这样才能拆包。
        """
        return (i for i in (self.x, self.y))

    def __repr__(self):
        """
         __repr__ 方法使用 {!r} 获取各个分量的表示形式,然后插值,构
成一个字符串;因为 Vector2d 实例是可迭代的对象,所以 *self 会把
x 和 y 分量提供给 format 函数。
        """
        class_name = type(self).__name__
        return '{}({!r},{!r})'.format(class_name, *self)

    def __str__(self):
        """
        从可迭代的Vector2d实例中得到一个元组,显示为一个有序对。
        """
        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))
    

3、备选构造方法

上一节将Vector2d实例转换成字节序列,那么如何将字节序列转换成Vector2d实例呢?

array.array中类方法.frombtyes可以实现这个功能。

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

memoryview() 函数返回给定参数的内存查看对象(memory view)。

所谓内存查看对象,是指对支持缓冲区协议的数据进行包装,在不需要复制对象基础上允许Python代码访问。

4、classmethod 与 staticmethod

classmethod:用来指定一个类的方法为类方法,没有此参数指定的类的方法为实例方法。

类方法与实例方法的区别在于,类方法操作的对象是类本身,实例方法操作的对象是具体的实例。

静态方法主要用来存放逻辑性代码,逻辑上属于类,但本身与类没有关系,不需要实例就可以调用,相当于我们只是因为类之外的函数与类存在逻辑上的关系,为了方便使用和维护,所以将其也放在类内,然后我们调用的时候在函数名之前加了个 类名.函数名()。所以静态方法其实可以直接在.py模快中定义,不需要写在类内。

首选我们看为什么要使用类方法。

比如说我们要定一个时间类用来打印时间。

class Data_test(object):
    def __init__(self, year=0, month=0, day = 0):
        self.day = day
        self.month = month
        self.year = year

    def out_date(self):
        print("year is:", self.year)
        print("month is:", self.month)
        print("day is:", self.day)

if __name__ == "__main__":
    t = Data_test(2020,12,5)
    t.out_date()
    
 #输出
year is: 2020
month is: 12
day is: 5

符合我们的预期,但是如果用户输入的是“2020-12-5”,这种字符格式,显然我们就不能直接使用这个类,需要先处理一下。

string_date='2016-8-1'
year,month,day=map(int,string_date.split('-'))
s=Data_test(year,month,day)

我们可以这样处理,但是每当处理这种情况,都需要重新写一下这个过程,那么能不能将这个处理函数写入类中,classmethod就是这个用处。

class Data_test(object):
    def __init__(self, year=0, month=0, day = 0):
        self.day = day
        self.month = month
        self.year = year

    def out_date(self):
        print("year is:", self.year)
        print("month is:", self.month)
        print("day is:", self.day)

    @classmethod
    def get_date(cls, string_date):
        year, month, day = map(int, string_date.split('-'))
        date1 = cls(year, month, day) #这里cls将处理过的字符重新建立了一个新实例
        return date1


if __name__ == "__main__":
    t = Data_test.get_date("2020-12-5")
    t.out_date()
 #输出
year is: 2020
month is: 12
day is: 5

此时,@classmethod的作用就相当于是这个类的备选构造函数。这样的好处就是在写好初始类得情况下,想给初始类添加新功能,只要额外添加你要处理的函数,然后使用@classmethod即可,不需要改初始类。

5、格式化显示

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式 委托给相应的 .format(format_spec) 方法。format_spec 是格 式说明符,它是:

  • format(my_obj, format_spec) 的第二个参数,或者
  • str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分

6、可散列的Vector2d

为了将Vector2d变成可散列的,必须使用___hash___方法和__eq__方法,此外还要让向量不可变。

可散列的数据类型:“Python 里所有的不可变类型都是可散列的”。这个说法其实是不 准确的,比如虽然元组本身是不可变序列,它里面的元素可能是其 他可变类型的引用。一般来讲用户自定义的类型的对象都是可散列的,散列值就是它们 的 id() 函数的返回值,所以所有这些对象在比较的时候都是不相 等的。如果一个对象实现了 eq 方法,并且在方法中用到了这 个对象的内部状态的话,那么只有当所有这些内部状态都是不可变 的情况下,这个对象才是可散列的。

让Vector2d不可变的代码清单。

class Vector2d:
    typecode = "d"

    def __init__(self, x, y):
        self.__x = float(x)#使用两个前导下划线,把属性标记为私有
        self.__y = float(y)

    @property #@property装饰器将读值方法标记为特性
    def x(self):#读值方法与公开属性同名
        return self.__x#直接返回self.__x

    @property
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))#需要读取 x 和 y 分量的方法可以保持不变,通过 self.x 和 self.y读取公开特性,而不必读取私有属性

7、私有属性和“受保护的”属性

Python 不能像 Java 那样使用 private 修饰符创建私有属性,但是 Python 有个简单的机制,能避免子类意外覆盖“私有”属性。

例如一个类Dog,这个类内部用到了mood实例属性,但是没将其开放。现在我们又创建了Dog子类:Beagle,同时创建了名为mood的实例属性,那么继承的方法中就会把Dog类的mood属性覆盖掉。

为了避免这种情况,我们可以在Dog类中以__mood的形式命名实例属性,这样子类同名属性就不会覆盖掉父类的mood,这也称为名称改写。没有被覆盖掉的原因是,python会将__mood存入实例的__dict__属性中,而且会在前面加上一个下划线和类名。因此对Dog类来说,__mood会变成_Dog__mood;对于对 Beagle 类来说,会变成 _Beagle__mood。

不过由于不是所有的python程序员都喜欢名称改写功能,所以约定单个下划线将实例属性变成“受保护”的属性,让看到单个下划线的属性的人知道这个属性是受保护的,不要轻易改动。

8、使用__slots__类属性节省空间

默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。

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

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

使用方法:

class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
# 下面是各个方法

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

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

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

弱引用是因为有是有我们需要引用对象,但是又不想对象存在的时间超过所需时间,例如缓存中,弱引用不会增加对象的引用数量。引用的目标称为所指对象,弱引用不会妨碍所指对象当作垃圾回收。

__slots__需要注意的问题:

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

9、覆盖类属性

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

Vector2d 中有个 typecode 类属性,__bytes__ 方法两次用到了 它,而且都故意使用 self.typecode 读取它的值。因为 Vector2d 实 例本身没有 typecode 属性,所以 self.typecode 默认获取的是 Vector2d.typecode 类属性的值。

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

>>> from vector2d_v3 import Vector2d
>>> v1 = Vector2d(1.1, 2.2)
>>> dumpd = bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'
>>> len(dumpd) # ➊默认字节序列长度为17个字节
17
>>> v1.typecode = 'f' # ➋把v1的实例的typecode属性设为‘f’
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf) # ➌现在的得到字节序列是9个字节长度
9
>>> Vector2d.typecode # ➍类属性的值没有改变,只是v1的typecode属性值改变了
'd'

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值