Python编程:符合Python风格的对象

1.对象表示形式

        每门面向对象的语言至少都有一种获取对象字符串的表示方式的标准方式。Python中提供了以下两种:

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

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

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

2.再谈向量类

        为了说明用于生成对象表示形式的众多方法,我们创建一个Vector2d类进行说明。

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))

        在上面的代码中,我们给typecode赋值‘d’,即定义了类属性,在Vector2d实例和字节序列之间转换时使用。在_init_方法中,我们将x、y的值变为浮点数,尽早捕获错误,以防调用函数时传入不当参数。_iter_方法是将x,y变成可迭代的对象,用元组保存,这样才能拆包。_repr_方法使用{!r}获取各个分量的表示形式,然后插值,构成一个字符串。因为*self是一个可迭代的对象,因此会分别把x,y提供给format函数。_bool_方法使用abs(self)计算模,然后把结果转换成布尔值,如果值为0.0,就输出False,值为其它,输出为Ture。

3.备选构造方法

       我们可以把Vector2d实例转换成字节序列,应该也可以把字节序列转换成Vector2d实例。而.frombytes方法正好符合这个需求。

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

       该段代码,使用了classmethod装饰器修饰,该函数传入的不是self而是类本身。并且通过cast函数将数据由当前类型变化为其他类型的操作。

4.classmethod和staticmethod

       classmethod是用来定义操作类,而不是实例的方法。classmethod改变了调用方法,第一个传入的参数是类本身,而不是实例,最常见的用途是用来构建备选构造方法。staticmethod也会改变调用方法,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数。只是碰巧在类的定义体中,而不是在模块层中定义。 

class Demo:
    @classmethod
    def klassmeth(*args):
        return args
    @staticmethod
    def statmeth(*args):
        return args

print(Demo.klassmeth())
print(Demo.statmeth())
print(Demo.statmeth('spam'))

        运行上段代码,会发现klassmeth和statmeth都会返回位置参数,只不过klassmeth的第一个参数始终都是Demo类,而statmeth和普通的函数差不多。但是,值得一提的是,classmethod装饰器用途很广,而staticmethod只有在极少数情况下才使用。

5.格式化显示

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

(1)format(my_obj, formaat_spec)的第二个参数。

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

b = 1/2.43
a = format(b, '0.4f')
print(a)
print('{rate:0.2f}'.format(rate=b))

        像'{0.mass:5.3e}'这样的字符串其实包含两个部分:冒号左边的‘0.mass’在替换字段句法中是字段名,冒号后面的‘5.3e’是格式说明符。格式化说明符实用的表示方法叫格式规范微语言。其中,为一些内置类型提供了专用的代码。如:b表示2进制int类,x表示16进制int类,f表示小数类型的浮点数,%表示百分数形式。

    def __format__(self, fmt_spec=''):
        components = (format(c, fmt_spec) for c in self)
        return '({}, {})'.format(*components)

        使用内置的format函数把fmt_spec应用到向量的各个分量上,构建一个可迭代的格式化字符串。再把格式化字符串(x,y)代入公式中。如果想以极坐标的方式表示向量,则可以进行下面方法的改动,格式化字符串以‘p’结尾,就代表使用极坐标。

    def __format__(self, fmt_spec=''):
        if fmt_spec.endwith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            out_fmt = '<{}, {}>'
        else:
            coords = self
            out_fmt = '{}, {}'
        components = (format(c, fmt_spec) for c in self)
        return '({}, {})'.format(*components)

6.可散列的Vector2d

        为了把上述实例变成不可散列的,必须使用_hash_和_eq_方法。此外,还要让向量变成不可变。所以,我们需要将分量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

        一般情况下,我们使用一个前导号(_)来把属性标记为私有的。类似的,我们用@property装饰器把读值方法标记为特性。

        注意,我们让这些向量不可变是有原因的,因为这样才能实现_hash_方法。这个方法应该返回一个整数,理想情况下还要考虑对象的散列值,因为相等的对象应该具有相同的散列值。参考某些文档,我们使用异或号(^)混合各分量的散列值。

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

       这样,对象就变成可散列,就可以设置成集合了。如果定义的类型是标量数值,可能还要实现int、float方法,来实现强制类型转换。下面就是整个类的定义代码。

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 __abs__(self):
        return math.hypot(self.x, self.y)

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

    def __format__(self, fmt_spec=''):
        if fmt_spec.endwith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            out_fmt = '<{}, {}>'
        else:
            coords = self
            out_fmt = '{}, {}'
        components = (format(c, fmt_spec) for c in self)
        return '({}, {})'.format(*components)

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

    def __hash__(self):
        return hash(self.x) ^ hash(self.y)
    
@classmethod
def frombytes(cls, octets):
    typecode = chr(octets[0])
    memv = memoryview(octets[1:]).cast(typecode)
    return cls(*memv)

7.Python的私有属性和受保护的属性

        在Python中为了避免发生属性覆盖,我们通常会以_x(名称)的形式命名实例属性,Python会把属性名存入实例的_dict_属性中,而且还会在前面加上一个下划线和类名。因此,对于Dog类来说,就会变成_Dog__x。这个语言特性称为名称改写。

v1 = Vector2d(3, 4)
print(v1.__dict__)
print(v1._Vector2d__x)

        名称改写只能说是一种安全措施,它的目的是避免意外访问,并不能防止故意做错事。

        所以,有些人不喜欢这种语法,他们约定使用一个带下划线的前缀来编写受保护的属性(如:self._x)。Python解释器不会对使用单个下划线的属性名做特殊处理,不过这是个约定成俗的事情,基本上所有的程序员都不会在类外访问这种属性。

8.使用_slots_类属性节省空间

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

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

class Vector2d:
    __slots__ = ('_x', '_y')
    typecode = 'd'

        此外,还有一个实例属性可能需要注意,即_weakref_属性,为了让对象支持弱引用,必须有这个属性。但是,如果我们定义了_slots_,就需要把'_weakref_'方法添加到_slots_中。

        综上,如果使用得当,_slots_可以显著节省内存,不过有以下几点要注意:

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

(2)实例只能拥有_slots_中列出的属性,除非把‘_dict_’加入到_slots_中。但是这样也就失去了节省内存的功效。

(3)如果不把'_weakref_'方法添加到_slots_中,就不能对实例进行弱引用。

9.覆盖类属性

        Python有个很独特的特性,类属性可用于为实例属性提供默认值。如:Vector2d实例本身没有typecode属性,self.typecode默认获取的是Vector2d.typecode类的属性值。但是为不存在的实例属性赋值,就会创建新的实例。

        Vector2d.typecode属性的默认值为‘d’,即转换成字节序列时使用8字节双精度浮点数表示向量的各个分量。在转换之前,将其转变成‘f’,那么,使用四字节单精度表示各个分量。所以,该属性存在的意义就是为了支持不同的格式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值