符合Python风格的对象


基于 Python 的数据模型,自定义类型可以实现和内置类型一样自然的行为,实际上靠的是 鸭子类型

鸭子类型:按照预定行为实现对象所需的方法

0、最初的Vector类

from math import hypot   # 两个数平方和开方

class Vector():
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):     # 字符串表示形式
        return 'Vector(%r, %r)' % (self.x, self.y)

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

    def __bool__(self):
        return bool(abs(self))
        # OR return bool(self.x or self.y)

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

1、对象表现形式

Python 提供了两种获取对象的字符串表现形式的标准方式:

repr() :便于开发者理解的方式返回对象字符串表现形式,由 __repr__ 特殊方法提供支持
str() :便于用户理解的方式返回对象字符串表现形式,由 __str__ 特殊方法提供支持

为了给对象提供其他的表现形式,还涉及到另外两个特殊方法:

__bytes__ :与 __str__ 类似;bytes() 调用它获取对象的 字节序列表现形式
__format__ :被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式代码显示对象的字符串表现形式

! PS:

Python3 中,__repr____str____format__ 都必须返回 Unicode 字符串 (str 类型);__bytes__ 返回 字节序列 (bytes 类型)


2、Vector v0

from array import array
import math

class Vector2d:
    typecode = 'd'
    
    # 尽早转换为float,尽早捕获错误,防止传入非法参数
    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))    # OR yield self.x; yield self.y
    
    # !r 获取各个分量的表现形式
    def __repr__(self):
        class_name = type(self).__name__     # OR self.__class__.__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)
    
    def __bool__(self):
        return bool(abs(self))
    
    @classmethod   # 定义类方法:从字节序列转换成该类的实例
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)    # 传入 octets 字节序列创建一个内存视图,使用 typecode 转换
        return cls(*memv)                          # 拆包内存视图得到一对参数传入构造方法
v1 = Vector2d(3, 4)
# 无需调用读值方法,直接通过属性访问 Vertor 实例的分量
print(v1.x, v1.y)
# Out
3.0 4.0
# 因为可迭代,可以拆包成变量元组
x, y = v1
x, y
# Out
(3.0, 4.0)
# repr 字符串表现形式
v1
# Out
Vector2d(3.0, 4.0)
# 使用 eval 表明 repr 函数调用实例得到的是对构造方法的准确表述
v1_clone = eval(repr(v1))
v1 == v1_clone
# Out
True
# str 字符串表现形式
print(v1)
# Out
(3.0, 4.0)
# 二进制表现形式
bytes(v1)
# Out
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
# Vertor模值
abs(v1)
# Out
5.0
# 布尔值
bool(v1), bool(Vector2d(0, 0))
# Out
(True, False)
# 字节序列转换成实例
v2 = Vector2d.frombytes(bytes(v1))
v2
# Out
Vector2d(3.0, 4.0)

3、classmethodstaticmethod

两者都是装饰器

classmethod :定义操作类的方法(类方法),而不是操作实例;类方法的第一个参数是类本身;最常见的用途是定义备选构造方法(Vector v0 中的frombytes 方法)
staticmethod :静态方法,其实就是普通函数,只不过在类的定义体中定义,而不在模块层定义

class Demo:
    @classmethod
    def klassmeth(*args):
        return args
    
    @staticmethod
    def statmeth(*args):
        return args
# 类方法的第一个参数始终是类本身
Demo.klassmeth(), Demo.klassmeth('spam')
# Out
((__main__.Demo,), (__main__.Demo, 'spam'))
# 静态方法的行为和普通函数相似
Demo.statmeth(), Demo.statmeth('spam')
# Out
((), ('spam',))

4、格式化显示

  • 内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的 __format__(format_spec) 方法。
  • format_spec —— 格式说明符,使用格式规范微语言(Format Specification Mini-Language)表示法:

format(obj, format_spec) 的第二个参数
str.format() 方法的格式字符串中,{} 里冒号之后的部分

它和 % 格式化方法 % 后面的部分基本一样,但是有所不同(如对齐方式)
详细的格式化符号及用法见 官方文档fat39 的博客

brl = 1 / 2.43
brl
# Out
0.4115226337448559
format(brl, '0.4f')   # 0可以省略
# Out
'0.4115'
'1 BRL = {rate:^15.2e} USD'.format(rate=brl)
# Out
'1 BRL =    4.12e-01     USD'

上例中的 0.4f^15.2e(取小数点后两位,科学计数法表示,居中并占15个位置单位)就是 format_spec

类似于 {0.attr:5.3e} 这样的格式字符串包括两个部分:

  • 冒号前的是 字段名,可以是数字表示,也可以是有具体含义的名称,如果它是某个类的实例并且要使用它的某个属性,可以使用 . 来获取
  • 冒号后的就是 格式说明符 (format_spec)

4 种十进制转十六进制的写法

# 1. hex 内置函数
hex(100)
# Out
'0x64'
# 2. % 格式化
'%x' % 100
# Out
'64'
# 3. format 函数
format(100, 'x')
# Out
'64'
# 4. str.format() 方法
'{:x}'.format(100)
# Out
'64'

二进制、八进制、十进制、十六进制 之间都可以用上述类似的方法相互转换。格式规范微语言为这些内置类型提供了专用的表示代码(如十六进制的 x)

格式规范微语言是可扩展的,每个类可以自行决定如何解释 format_spec 参数
datetime 模块中的类,它们的 __format__ 方法使用的格式代码和 strftime() 函数一样

from datetime import datetime
now = datetime.now()
format(now, '%Y-%m-%d  %H:%M:%S')
# Out
'2018-12-28  20:52:25'
"It's now {:%I:%M %p}".format(now) 
# Out
"It's now 08:52 PM"
now.strftime('Today is %D')
# Out
'Today is 12/28/18'

如果类没有定义 __format__ 方法,从 object 继承的方法会返回 str(my_object),但是如果传入格式说明符就会抛出 TypeError

format(v1)
# Out
'(3.0, 4.0)'
format(v1, '.3f')
# Out
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-87-9f7764dc3848> in <module>()
----> 1 format(v1, '.3f')


TypeError: unsupported format string passed to Vector2d.__format__

自定义的格式代码选择字母时应该避免其他类型用过的

  • 整数使用的代码:'bcdoXn'
  • 浮点数使用的代码:'eEfFgGn%'
  • 字符串使用的代码:'s'

在现有的 Vector 类的基础上实现自定义格式化,包括两点:

  1. 一般的格式化,分别进行向量两个分量的格式化,返回形式 (fmt_x, fmt_y)
  2. 格式化为极坐标,选用代码 'p',返回形式 <r, θ>r 是模长,即已经实现的 __abs__ 方法;θ 是弧度)
# 计算弧度
def angle(self):
    return math.atan2(self.y, self.x)

def __format__(self, fmt_spec=''):
    # 格式化为极坐标
    if fmt_spec.endwith('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)

关于自定义格式化的完整代码和测试,和之后的内容一起给出(不然代码会重复冗长)


5、可散列的Vector

类的实例可散列,需要满足以下三个条件:

  1. 实例不可变
  2. 实现 __hash__ 方法
  3. 实现 __eq__ 方法

** 针对 Vector 类实现以上三个条件 **

1. 实例不可变

使用 私有属性property 装饰器,将向量的两个分量设为只读属性,来保证实例“不可变”(因为属性不是真正的私有,所以不是真正的不可变)

class Vector2d:
    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__ 方法里面用的那样)
  • 之所以要实例不可变,是因为要实现 __hash__ 方法和 __eq__ 方法,保证相等的对象具有相同的散列值

2. 实现 __hash__ 方法

__hash__ 方法应该返回一个整数,根据官方的文档,建议返回各分量散列值的异或值

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

3. 实现 __eq__ 方法

之前已经实现

关于可散列 Vector 的完整代码和测试,和之后的内容一起给出(不然代码会重复冗长)


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

首先说明一点,Python 中没有像 Java 中那样的类似于 private 的限定修饰符,所以没有真正的实现私有和受保护,而是一种防护措施,更像是一种越定,就好比在 Java 中“私有”是法律规定,而在 Python 中“私有” 是道德规范

  • 私有属性:防止某个属性在被直接使用或者是创建子类时被覆盖。Python 中使用两个前导下划线(尾部没有或者最多有一个下划线)命名的属性就是私有属性。
  • 名称改写:被命名为私有的属性,会被内部改写成 _class__attr 的形式,被存入实例的 __dict__ 属性中。
class A:
    def __init__(self, x):
        self.__x = x
        
    @property
    def x(self):
        return self.__x
a.x = 2
# Out
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-10-4930f24ffcf0> in <module>()
----> 1 a.x = 2


AttributeError: can't set attribute
a = A(1)
a.__dict__
# Out
{'_A__x': 1}
a._A__x = 2

所以实际上私有属性的名称改写仅仅是一种防护措施,避免无意地覆盖了我们所想的变量,但是如果是要故意通过改写后的名称来覆盖属性也没有被禁止。
一种比较好的处理属性的方案就是用 私有属性 + @property 装饰器,不去使用改名后的名称,而是使用我们真正想命名的名称。

对于前导单下划线的属性,Python 不会做任何处理,但却是一个不成文的约定,约定它是一个“私有的”或者是“受保护的”属性,避免意外覆盖属性。但是需要注意的是,不能用 from module import * 导入


7、__slots__ 类属性

慎用

  • __slots__ 类属性,在处理百万个属性不多的实例时,能够节省大量内存,它让解释器在元组中存储实例属性来代替字典(__dict__);
  • __slots__ 类属性的值设为一个字符串构成的可迭代对象,一般是元组,其中每个元素表示各个实例属性;
  • 在类中定义了 __slots__ 属性后,实例不能再有 __slots__ 中所列名称之外的其他属性;但是不要滥用它来禁止类的用户新增实例属性;
  • 如果把 '__dict__' 加入到 __slots__ 中,实例会在元组中保存各个实例的属性,也会支持动态创建属性,存在 __dict__ 中;
  • __weakref__ 实例属性为了让对象支持弱引用,自定义类中默认就有,但是如果定义了 __slots__ 属性,而且想支持弱引用,必须把 '__weakref__' 加入 __slots__ 中。

8、覆盖类属性

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

就像 Vector 类中的 typecode 属性,定义为了类属性,并且可以通过 self.typecode 来访问。

  • 如果实例本身并没有这个属性,就会使用类的同名属性,否则会用实例自己的属性
  • 为不存在的 实例属性 赋值,会新建 实例属性,并且同名 类属性 不受影响
  • 要修改 类属性 的值,必须直接通过类获取它然后修改,不能通过实例修改

一个更符合 Python 风格的覆盖类属性的方法:创建一个子类,只用于定制类的数据属性 (e.g. Django 基于子类的视图)


- 最终的Vector类代码以及测试 -

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

格式化测试

v1 = Vector2d(3, 4)
format(v1)
# Out
'(3.0, 4.0)'
format(v1, '.2f')
# Out
'(3.00, 4.00)'
format(v1, '.3e')
# Out
'(3.000e+00, 4.000e+00)'
format(v1, 'p')
# Out
'<5.0, 0.9272952180016122>'
format(v1, '.3ep')
# Out
'<5.000e+00, 9.273e-01>'
format(v1, '0.5fp')
# Out
'<5.00000, 0.92730>'

hash 测试

v2 = Vector2d(5, 12)
hash(v1), hash(v2)
# Out
(7, 9)
len(set([v1, v2]))
# Out
2
d = {}
d[v1] = 'Python'
d[Vector2d(3.0, 4.0)]
# Out
'Python'


md效果不好,原先是 jupyter notebook 版本

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容