符合Python风格的对象

符合Python风格的对象

1.1 前言

本文是《流畅的Python》——符合Python风格的对象的读后笔记与总结。

在其他面向语言中构建自定义类型通常是通过继承和重载来实现,然而在Python中只要构建鸭子类型就可以实现自定义类型:只需按照预定行为实现对象所需的方法即可。

Python中有很多的魔法方法,这是Python语言的魅力之一,通过实现所需的魔法方法,就可以实现功能强大的自定义类型。

通过学习实现一个简单的二维向量该有的方法来收获以下几点知识:

  • 如何及何时使用@classmethod@staticmethod装饰器
  • 如何自定义格式化说明符来实现自定义格式化

1.2 实现向量类

对象表示形式

在Python中获取对象字符串的表示形式有两种方法:

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

    内部实现了__repr__方法

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

    内部实现了__str__方法

  • bytes():获取对象的字节序列表示形式。

    内部实现了__bytes__方法

  • format():格式化字符串的表示形式。

    内部实现了__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)

运行结果:

>>> v1 =Vector2d(3, 4)
>>> v1
Vector2d(3.0, 4.0)
>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> bytes_v1 = bytes(v1)
>>> bytes_v1
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
>>> Vector2d.frombytes(bytes_v1)
Vector2d(3.0, 4.0)
>>> abs(v1)
5.0
>>> bool(v1), bool(Vector2d(0, 0))
(True, False)
>>>

上边的实现方法中,__eq__方法有个缺陷:具有相同数值的可迭代对象相比结果也为true。

>>> Vector2d(3, 4) == [3, 4]
True

1.3 classmethod与staticmethod

Python中的类方法就是只有类本身才能调用的方法,类的实例化对象是不能调用的。

类方法的第一个参数一定是代表类本身,一般使用cls,但是也可以叫别的名字。

静态方法就是普通的函数,只是在类中定义而不是在模块层定义而已。

>>> class Demo:
... @classmethod
... def klassmeth(*args):
... 	return args 
... @staticmethod
... def statmeth(*args):
... 	return args 
...
>>> Demo.klassmeth() 
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth() 
()
>>> Demo.statmeth('spam')
('spam',)

1.4 格式化显示

使用内置的format()函数和str.format()方法格式化前边向量类。

格式说明符

对格式化对象的格式化要求:

  • format(my_obj, format_spec)的第二个参数
  • str.format()方法的格式字符串,{}里代换字段中冒号后面的部分,冒号左边表示代换字段句法中的字段名。
>>> brl = 1/2.43	# 需要格式化的字段
>>> brl
0.4115226337448559
>>> format(brl, '0.4f') # brl代表格式化字段对象,'0.4f'代表格式化说明符
'0.4115'

# rate 代表格式化字段对象, 0.2f 代表格式化说明符
>>> '1 BRL = {rate:0.2f} USD'.format(rate=brl) 
'1 BRL = 0.41 USD'

支持格式化说明符

使自定义向量类支持格式化说明符:

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

自定义格式化说明符

在Python的格式规范微语言中,整数使用的代码有bcdoxXn, 浮点数使用的代码有eEfFGn%,字符串使用的代码有s

我们在自定义格式化规范的时候不要使用这些已经被定义了的代码,比如说自定义向量类的格式化代码定位没有出现过的p:格式为 <r, θ >代表向量的模和角度。

# 返回向量的角度
def angle(self):
	return math.atan2(self.y, self.x)

# 自定义格式化说明符,返回自定义格式化后的字段对象
def __format__(self, fmt_spec=''): # fmt_spec为传入的格式化说明符,默认为空
    if fmt_spec.endswith('p'):	# 如果格式化说明符为自定义的p就构造自定义格式化
        fmt_spec = fmt_spec[:-1]  # 删除p后缀
        coords = (abs(self), self.angle()) # 构建需要格式化字段对象
        outer_fmt = '<{}, {}>'	# 构造格式化格式
    else:
        coords = self	# 使用默认的格式化字段对象
        outer_fmt = '({}, {})'	# 构造格式化格式
        
    # 接下来两步很经典,先根据官方定义的格式化规范格式化coords对象
    # 然后在利用生成器表达式构造为可迭代对象
    # 在将这个可迭代对象以Python中经典的拆包方式作为格式化字段对象来构造我们自定义的outer_fmt格式
    components = (format(c, fmt_spec) for c in coords)
    return outer_fmt.format(*components)

自定义格式化说明符步骤大概如下:

  1. 传入自定义格式化说明符

  2. 根据自定义格式化说明符来构建格式化字段对象格式化后的格式

    这一步需要注意的是要将自定义格式化说明符的后缀(自定义格式化代码)删掉,因为在最后一步的格式化的时候使用的还是官方的格式化代码。

  3. 根据前边构造的格式化字段对象和格式化格式化说明符来初次格式化

    这次格式化主要是根据官方定义的格式化规范来构造格式化后的字段对象。

  4. 最后在按照我们定义的格式化后的格式和上步得到的字段对象来最终格式化

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

1.5 实现自定义类的可散列

首先要知道可散列对象一定是不可变的,不可变对象不一定是可散列的。

要想自定义对象是可散列的必须实现两个特殊方法:

  • __hash__:返回一个hash值,表示为散列值,但是只有不可变对象才能求hash值,所以我们自定义的向量类要把属性设为不可变。

    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把属性装饰为只读
    • __iter__:需要读取x和y分量的方法可以保持不变,通过 self.x 和 self.y 读取公开特性,而不必读取私有属性 。
  • __eq__:相等对象应该有相同的散列值。

此时的向量是可散列的:

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

目前完整的自定义向量类的代码如下:

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)
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如来心秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值