Wlgls 冲鸭!

第九章 符合Python风格的对象


暂时由于日常生活中很少使用类与对象(没有经历过大的项目),所以,对于之后的一些章节很有可能有些不太理解,所以我暂时会只是知道有这些东西,而不是去深究他有什么好处,因为我没有真正的实践过。所以我选择了一些简单,比如classmethod,鸭子协议等,对于抽象基类,我尝试看了,但是我对于其中很大部分都无法理解(或许是几乎未曾用过以至于兴致缺乏,不想去看)。

在这里最好先去温习一下Python的魔术方法。

另外,以Vector类来开展学习,首先是一个二维向量。

class Vector2d:

    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y) 

对象的表示形式

我们可以获得对象的其他表现形式,比如字符串,字节,甚至一些特殊的格式

字符串

每种面向对象的语言至少有一种获取对象的字符串的表示形式的标准方式》Python提供了两种。

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

我们知道魔术方法,所以为了这两个函数,我们需要在类中实现__repr____str__两中特殊方法。

现在我们重构一下__str____repr__方法。

class Vector2d:

    ...# 省略

    def __repr__(self):             # 重构__repr__方法
        class_name = type(self).__name__
        return '{}({},{})'.format(class_name, self.x, self.y)
    
    def __str__(self):              # 重构__str__方法
        return str(tuple([self.x, self.y]))

现在,我们就可以使用str()repr()两个方法了。他将会按照我们重构的方法返回。

if __name__ == '__main__':

    v = Vector2d(2, 3)
    print(str(v))
    print(repr(v))

# 结果
(2.0, 3.0)
Vector2d(2.0,3.0)

classmethod和staticmethod

classmethod装饰器的作用是定义操作类,而不是操作实例的方法,classmethod改变了调用方法的方式,因此类方法的第一个参数是类本身,classmethod最常见的是定义备选构造方法。

staticmethod装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。实际上,静态方法就是普通的方法,只不过碰巧在类的定义体中。

>>> class Demo:
...     @classmethod
...     def klassmeth(*args):
...             return args
...     @staticmethod
...     def statmeth(*args):
...             return args
...     def ordmeth(self *orgs):
...             return args
... 
>>> Demo.klassmeth()
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('s')
(<class '__main__.Demo'>, 's')
>>> Demo.statmeth()
()
>>> Demo.statmeth('s')
('s',)
>>> Demo.orgmeth()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Demo' has no attribute 'orgmeth'

对于类方法,我们的第一个参数使用是类本身,对于类方法和静态方法,我们都可以使用类来调用,但是对于普通的实例方法,我们必须先创建一个实例才可以调用。

可散列的Vector2d

我们甚至可以将自己的对象变成一个可散列的,在前面已经提及,如果要把Vector2d实例变成一个可散列的,我们必须实现__hash____eq__方法。同时我们也应当让对象中的元素不可变。

Python的私有属性和‘受保护’属性

python可以使用两个前导下划线__来命名实例属性,然后Pyhton会将属性名存入实例的__dict__属性中,而且会在前面加一个下划线和类名,比如,对于God类而言,__mode会变成_God__mode。这个语言特性叫做名称改写(name mangling)

现在我们把对象中变为只读属性

class Vector2d:

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    ...

tips: 在Python编程中,许多程序员更喜欢使用_来代表私有属性,但是Python不会一个下划线的变量做任何操作,更多的是Python程序员们的约定俗称

实现__hash__和__eq__方法

class Vector2d:

    ...

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

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

我们对于__eq__定义了比较(x, y)的值,对于__hash__,我们按照官网来使用^混合个分量的散列值。

现在我们就变成了可散列的类型了。

if __name__ == '__main__':

    v = Vector2d(2, 3)
    v2 = Vector2d(2, 3)
    print(v == v2)
    print(hash(v))

# 结果
True
1

可切片的序列

现在的Vector2d只是支持二维的向量,假如我们有多个多维向量。我们可能希望这个对象可以实现切片操作,也就是实现了序列协议。

Pyhton中的序列协议只需要__len____getitem__两个方法。

from array import array

class Vector:

    def __init__(self, components):
        self._components = array('d', components)

    def __len__(self):
        return len(self._components)

    def __getitem__(self, index):
        return self._components[index]

在上面这个类中,我们可以传入列表,元组等,然后我们可以将分量放在数组中。然后我们可以进行序列操作,如len(), []操作等,甚至支持了简单的切片操作。但是这个切片返回的不是Vector类

if __name__ == '__main__':
    v1 = Vector([1, 2, 3])
    print(len(v1))
    print(v1[0], v1[-1])
    print(v1[0:2])

# 结果:
3
1.0 3.0
array('d', [1.0, 2.0])

切片的原理

>>> class Demo:
...     def __getitem__(self, index):
...             return index
... 
>>> s = Demo()
>>> s[1]        
1
#  单个索引返回了1,一个整型

>>> s[1:2]
slice(1, 2, None)
# 1:4竟然变成了slice对象

>>> s[1:4:2]
slice(1, 4, 2)
# 以此对应,可以发现slice(1, 4, 2)表示从1开始,从4结束,步幅为2

>>> s[1:4:2, 9]
(slice(1, 4, 2), 9)
# 产生了元组

>>> s[1:4:2, 7:9]
(slice(1, 4, 2), slice(7, 9, None))
# 元组中存在多个切片对象

根据上述的例子,很明显,切片操作与slice对象密不可分

>>> slice
<class 'slice'>
>>> dir(slice)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
>>> 

我们可以发现,里面有start,step和stop属性。

同时里面还有一个indices属性,这个属性用于优雅的处理缺失索引和负数索引,以及长度超过目标序列的切片。 ` S.indices(len) -> (start, stop, stride)`

>>> slice(None, 10, 2).indices(5)
(0, 5, 2)
>>> slice(-3, None, None).indices(5)
(2, 5, 1)

举两个简单的例子,在第一个中缺失开始,并且过长,所以,indices方法自动补全了开始,并将过长的10改为了5

在第二个例子中,开始的一个负数,然后根据长度为5, 自动将其转化为正数2,其余的自动补全。

处理切片的__getitem__方法

from array import array
import numbers
class Vector:

    ...

    def __getitem__(self, index):
        cls = type(self)    # 得到实例所属的类,供后面使用

        # 由于index对于正数会返回整形,对于切片会返回slice对象,所以具体情况具体分析
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:       # 其余情况报错
            msg = '{cls.__name__} indices must be intergers'
            raise TypeError(msg.format(cls=cls))

现在我们就可以使用切片操作了

if __name__ == '__main__':
    v1 = Vector(range(10))
    print(v1[1])
    print(v1[1:3])
    print(v1[-1:])
    print(v1[1, 2])

#  结果:
1.0
<__main__.Vector object at 0x7f3786993438>
<__main__.Vector object at 0x7f3786993438>
Traceback (most recent call last):
  ...
TypeError: Vector indices must be intergers    

由于我们没有实现__repr__方法,所以其返回的类型是来自其继承的object

假如我们实现了__repr__方法:

class Vector:

    ...

    def __repr__(self):
        #   使用reprlib.repr获取self._components的有限长度表示形式,如(array('d', [0.0, 1.0, 2.0, ...]))
        components = reprlib.repr(self._components)
        # 去掉 array('d',) 和后面的 ‘)’便于插入
        components = components[components.find('['):-1]

现在再试一次



if __name__ == '__main__':
    v1 = Vector(range(10))
    print(v1[1])
    print(v1[1:3])
    print(v1[-1:])
    print(v1[1, 2])
# 结果:
1.0
Vector[1.0, 2.0]
Vector[9.0]
Traceback (most recent call last):
  ...
TypeError: Vector indices must be intergers

覆盖类属性

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

比如:

>>> class Demo:
...     x = 1
...     def __init__(self, y):
...             self.y = y
... 
>>> 

对于其中x就是一个类属性,而self.y是其中的实例属性。

调用

对于类属性,我们可以通过类本身调用,也可以通过实例调用,但是实际上,通过实例调用,也是默认获取的是类属性,也就是说self.x默认获取的是Demo.x.

>>> class Demo:
...     x = 1
...     def __init__(self, y):
...             self.y = y
... 
>>> d = Demo(1)
>>> d.x
1
>>> Demo.x
1

赋值

如果要修改类属性的值,我们应当通过类直接修改,而不是通过实例。因为为不存在的实例属性赋值,将新建实例属性。假如我们通过实例为上述代码中的x赋值,我们不会影响到同名类属性,而且之后使用self.x获取的是实例属性x。也就是说把同名类属性遮盖了。

>>> Demo.x = 2
>>> Demo.x
2

更恰当的使用,应该是创建一个子类,然后定制类的数据属性,而且效果更持久,也更有针对性。

>>> class Demo1(Demo):
...     x = 3
... 
>>>