教你如何编写Pythonic对象

介绍

在我们开始探索如何写一个Pythonic对象之前,让我们先弄清楚这个术语的含义它不是关于PEP8和尊重其规则来编写漂亮的python代码;而是关于编写对象,最大限度地使用Python数据模型的概念,因此它们可以像Python标准库对象一样被自然地使用。

其思想是将Python ADN注入到我们的用户定义对象中,使它们发生变异,并表现为本地Python对象为此,我们将实现一个向量类来表示多维向量。

下面的代码用最小的实现表示Vector类。矢量由其坐标表示。

from array import array
class Vector:
 __arrayType = "d"
 def __init__(self, coordinates):
 self.__coordinates = array(self.__arrayType, coordinates)
if __name__ == "__main__":
 v = Vector([1, 2, 3])
 print(v)
 #<__main__.Vector object at 0x0082F610>
 v1 = Vector((1, 2, 3))
 print(v1)
#<__main__.Vector object at 0x0317FC88>

向量坐标存储在浮点数组中;请注意将数组中的元素类型强制设置为浮点的array type=“d”。我们可以将任何iterable传递给Vector的构造函数,因为数组的构造函数用作接受任何iterable(元组、列表等)的内部容器。

当我们打印一个向量对象本身时,注意我们得到它的引用(CPython的内存地址)而不是它的坐标。让我们通过在类中实现_str_方法来更改它,使其具有更友好的输出,例如(x,y,z,…)。

from array import array
class Vector:
 ....
 def __str__(self):
 return str(tuple(self.__coordinates))
 ....
if __name__ == "__main__":
 v = Vector([1, 2, 3])
 print(v)
 #(1.0, 2.0, 3.0)

当我们打印一个向量对象本身时,注意我们得到它的引用(CPython的内存地址)而不是它的坐标。让我们通过在类中实现_str_方法来更改它,使其具有更友好的输出,例如(x,y,z,…)注意,我们使用了从数组创建的元组的字符串表示。

__str_不是Python数据模型用于打印对象的唯一方法;_repr_还用于提供更面向调试目的的对象表示可以计算此表示以使用eval函数创建相同的对象。

from array import array
import reprlib
class Vector:
 ....
 def __repr__(self):
 s = reprlib.repr(self.__coordinates)
 return "{}({})".format(self.__class__.__name__, s[s.index('['):-1])
 ...
if __name__ == "__main__":
 v = Vector([1, 2, 3])
 s = repr(v)
 print(s)
 #Vector([1.0, 2.0, 3.0])
 v1 = eval(s)
 print(v1)
 #(1.0, 2.0, 3.0)
 v2 = Vector(range(100))
 print(repr(v2))
 #Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

在前面的代码块中,请注意,与eval一起使用时repr的返回值允许创建新向量reprlib的使用允许我们在数组包含太多元素的情况下不打印数组的所有元素,并用就像向量v2。

Python迭代

为了确保我们可以在向量上循环,并且可以将它们解包,我们需要使它们可迭代为此,必须向我们的类中添加_iter_方法。

class Vector:
 ...
 def __iter__(self):
 return iter(self.__coordinates)
...
if __name__ == "__main__":
 v = Vector([1, 2, 3])
 for i in v:
 print(i)
 #1.0
 #2.0
 #3.0
 t = tuple(v)
 print(t)
 #(1.0, 2.0, 3.0)

Python长度计算

为了能够通过将对象赋给len()函数来获得向量内的坐标数,必须将_Len_方法添加到类中。

from array import array
import reprlib
class Vector:
 ...
 def __len__(self):
 return len(self.__coordinates)
 ...
if __name__ == "__main__":
 v = Vector([1, 2, 3, 4])
 print(len(v))
#4

Python的比较

在不调整类以支持比较的情况下,应用于两个向量对象的==运算符将比较它们的引用。若要更改此行为,必须实现“eq”方法。在我们的例子中,两个向量是相等的,当且仅当它们具有相同的坐标且具有相同的顺序。

rom array import array
import reprlib
class Vector:
...
 def __len__(self):
 return len(self.__coordinates)
 def __iter__(self):
 return iter(self.__coordinates)
 def __eq__(self, other):
 if len(self) == len(other):
 for i, j in zip(self, other):
 if i != j:
 return False
 return True
 else:
 return False
...
if __name__ == "__main__":
 v = Vector([0, 1, 2, 3, 4])
 v1 = Vector((1, 2, 3, 4, 5))
 v2 = Vector(range(5))
 print(v == v2)
 #True
 print(v == v1)
 #False

让我们花点时间分析一下新的方法:

通过调用len()函数来使用len方法。

通过将self和其他参数传递给zip函数来使用iter方法,zip函数接受iterable作为参数。

Pythonic绝对值

对于此示例,我们使用该 __abs__ 方法返回由以下表达式定义的向量的欧几里得范数:

教你如何编写Pythonic对象

from array import array
from math import sqrt
import reprlib
class Vector:
 ...
 def __abs__(self):
 return sqrt(sum((x**2 for x in self)))
 ...
if __name__ == "__main__":
 v = Vector([0, 1, 2, 3, 4])
 a = abs(v)
 print(a)
 #5.477225575051661

Pythonic布尔求值

对于向量类的当前实现,我们在计算向量的布尔值时有以下行为。

if __name__ == "__main__":
 v = Vector([])
 print(bool(v))
 #False
 v1 = Vector([1, 2, 3])
 print(bool(v1))
 #True

在我们的类中,如果没有bool方法,对bool()函数的调用将引用该方法如果长度等于0,则对象的计算结果为false-否则,其计算结果为true。

让我们通过实现一个方法来改变这种行为,如果向量欧几里德范数与0不同,则让它返回True,否则返回false。

from array import array
from math import sqrt
import reprlib
class Vector:
 ...
 def __abs__(self):
 return sqrt(sum((x**2 for x in self)))
 def __bool__(self):
 return bool(abs(self))
...
if __name__ == "__main__":
 v = Vector([])
 print(bool(v))
 #False
 v1 = Vector([1, 2, 3])
 print(bool(v1))
 #True
 v2 = Vector([0, 0])
 print(bool(v2))
 # False

Pythonic切片

Python中的切片旨在通过指示要检索的元素的索引或通过指示切片,从初始集合中获取子集。

一个对象的切片返回同一类型的另一个对象。getitem方法是要更新的方法,以使向量对象具有这种能力。

from array import array
from math import sqrt
import reprlib
class Vector:
 ...
 def __getitem__(self, item):
 if isinstance(item, int):
 return self.__coordinates[item]
 elif isinstance(item, slice):
 return self.__class__(self.__coordinates[item])
 else:
 raise IndexError("{} indexes must be integers".format(type(self).__name__))
...
if __name__ == "__main__":
 v = Vector([1, 2, 3, 4])
 v1 = v[1]
 print(v1)
 # 2.0
 v2 = v[0:3]
 print(type(v2))
 # 
 print(v2)
 # (1.0, 2.0, 3.0)

注意,通过使用Slicing,返回的对象也是一个向量对象。

结论

我们在本教程中实现的Dunder方法并不是唯一可以使用的方法其他的,如字节、散列、getatrr和格式可以用来改变用户定义对象的行为这些方法并不是每次定义新类时都要实现的,这取决于需要,这是Python数据模型的优点。

欢迎关注哦,老子爱你们。