符合Python风格的对象(上)
1、对象表示形式
每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python提供了两种方式。
-
repr()以便于开发者理解的方式返回对象的字符串表示形式。
-
str()以便于用户理解的方式返回对象的字符串表示形式。
前面我们也已经讲过,两者的内部实现是通过__repr__和__str__特殊方法。
除此之外还会用到另外两个特殊方法:__bytes__和__format__。__bytes__方法与__str__方法类似:bytes()函数调用它获取对象的字节序列表示形式。而__format__方法会被内置的format()函数和str.format()方法调用,使用特殊的格式代码显示对象的字符串表示形式。
接下来以一个向量类的例子进行说明:
class Vector():
typecode = 'd'
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __abs__(self):
return (self.x**2 + self.y**2)**.5
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __bool__(self):
return bool(abs(self))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __iter__(self):
return (i for i in (self.x, self.y))
def __str__(self):
return str(tuple(self))
def __repr__(self):
class_name = type(self).__name__
return "{}({},{})".format(class_name, *self)
def __format__(self, fmt_str):
import math
coords = self
if fmt_str.endswith('p'):
fmt_str = fmt_str[:-1]
coords = (abs(self), math.atan(self.y / self.x))
outer_fmt = '<{}, {}>'
else:
outer_fmt = '({}, {})'
compents = (format(i, fmt_str) for i in coords)
return outer_fmt.format(*compents)
def __bytes__(self):
from array import array
return bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))
从__init__到__bool__在第一章我们已经讲过。
-
typecode
typecode是类属性,在Vector实例和字节序列之间转换时使用。在这里我们借助array内置方法进行字节转换,'d’就对应着C中的double。
-
__eq__
支持==比较,这个地方实现有一些问题,因为我们是转换为tuple以后才进行对比的,所以可能会出现Vector(3,4)==[3, 4]这种情况,后续我们会对其进行说明。
-
__iter__
把Vector实例变成可迭代的对象,这样才能拆包。实现方式很简单,直接调用生成器表达式一个接一个产出分量。当然也可以采用yeild来实现,后续我们会讲到。
-
__format__
__format__是内置的format()函数和str.format()方法的内部实现,这个我们前面已经介绍过。这里我们定义了format用法,如果格式说明以p结尾,则使用极坐标表示方式。
format函数的用法为:
# format(my_obj, format_spec) format(3.1415926,'.3f') format(10,'b') # '1010'
format_spec被称之为格式规范微语言,他是可扩展的,可以由各个类自行决定如何解释format_spec参数。
from datetime import datetime now=datetime.now() print(format(now,'%H:%M:%S')) print('现在时间是:{:%H:%M %p}'.format(now)) # 返回 09:27:28 现在时间是:09:27 AM
如果没有定义__format__,使用format函数返回str(my_obj),但这时如果传入格式说明format_spec,会抛出TypeError。
-
__bytes__
array可以直接转换为字节序列,同时将数据类型的ASCII码作为前缀。
2、备选构造方法
前面将Vector转换为array,然后将Vector转换为bytes对象:
v = Vector(3, 4)
bytes(v)
# 返回
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
前面的b就说明是bytes对象,顺便提一下,字符串前面的前缀通常使用的有三个:
前缀 | 说明 |
---|---|
u | 以 Unicode 格式 进行编码,一般用在中文字符串前面 |
r | 去掉反斜杠的转义机制,尤其在文件路径字符串,前缀经常加r |
b | 后面字符串是bytes 类型 |
前面我们借助array来实现从Vector到字节序列的转换,此处我们借助内置方法memoryview实现从字节序列转换为Vector。memoryview()函数我们以前讲过,可以返回给定参数的内存查看对象,而memoryview.cast会把同一块内存里的内容打包成一个全新的memoryview对象。所以你可能会想到在Vector中增加下面的方法:
def frombytes(self, vbytes):
vtype = chr(vbytes[0])
vmem = memoryview(vbytes[1:])
vmem_cast = vmem.cast(vtype)
return(Vector(*vmem_cast))
- 第1行:frombytes接收一个字节序列
- 第2行:字节序列的第一个为具体的类型
- 第3、4行:利用memoryview实现对象的转换
- 第5行:重新生成一个Vector对象
v = Vector(3, 4)
vb = bytes(v)
v.frombytes(vb)
# 返回
Vector(3.0,4.0)
有没有感觉很难受,因为前面的bytes实现使用魔法方法__bytes__来实现,在调用的时候只需要使用bytes(v)就可以实现Vector到字节序列的转换。但是并没有一个内置魔法方法实现字节序列转换为其他对象,换句话说,没有__frombytes__魔法方法,能够实现frombytes(vb)。不过在学过装饰器以后,就有了对应的解决办法。
3、classmethod与staticmethod
前面为了实现frombytes方法,我们在Vector类内部定义了一个函数frombytes,但是在调用类内方法的时候,必须先将类实例化,使用v.frombytes的调用形式。classmethod不需要实例化类就可以调用类的属性,类的方法,实例化对象等:
# 在类内增加如下方法
@classmethod
def frombytes(cls, vbytes):
vtype = chr(vbytes[0])
vmem = memoryview(vbytes[1:])
vmem_cast = vmem.cast(vtype)
return(cls(*vmem_cast))
# 不用实例化v,可以直接从定义类中调用方法
Vector.frombytes(vb)
约定俗成的,这里的cls表示自身类Vector。
为了加深理解,我们再举一个例子:
class A(object):
bar = 1
def func1(self):
print('foo')
@classmethod
def func2(cls):
print('func2')
print(cls.bar)
cls().func1() # 调用 foo 方法
# 实例化以后才可以调用
a = A()
a.func1()
# 不需要实例化即可调用
A.func2()
# 返回
foo
func2
1
foo
可以看出,func1由于没有使用classmethod方法修饰,所以必须通过实例化a才可以调用,而func2经过修饰以后可以使用A.func2()直接调用。
staticmethod方法效果和classmethod方法效果几乎一样,唯一区别在于返回参数的不同:
class Demo():
@classmethod
def clsmeth(*args):
return args
@staticmethod
def stameth(*args):
return args
Demo.clsmeth() # (__main__.Demo,)
Demo.stameth() # ()
Demo.clsmeth('学习') # (__main__.Demo, '学习')
Demo.stameth('学习') # ('学习',)
classmethod第一个参数永远是类本身,staticmethod与普通函数表现一致。
作者认为,classmethod十分有用,但是staticmethod不是特别有用,区别就是上面说的,各位自辨吧。
——未完待续——
欢迎关注我的微信公众号