简介
在这个实验中,你将学习如何在 Python 中创建一个新的原始类型,并为其实现必要的方法。你还将了解 Python 的对象协议。在大多数 Python 程序中,像 int、float 和 str 这样的内置原始类型被用来表示数据。不过,Python 允许你创建自定义类型,标准库中的 decimal 和 fractions 等模块就是例子。
在这个实验中,你将创建一个名为 MutInt(可变整数)的新原始类型。与 Python 的不可变整数不同,MutInt 在创建后可以被修改。这个练习将展示在 Python 中创建一个功能完备的原始类型所需的基本原则。
创建基本的 MutInt 类
让我们从为可变整数类型创建一个基本类开始。在编程中,类就像是创建对象的蓝图。在这一步,我们将为新的原始类型奠定基础。原始类型是编程语言提供的基本数据类型,在这里我们要创建自己的自定义类型。
打开 WebIDE 并导航到
/home/labex/project目录。WebIDE 是一个集成开发环境,你可以在其中编写、编辑和运行代码。导航到这个目录可确保你所有的文件都组织在一处,并且能相互正确交互。打开在设置步骤中为你创建的
mutint.py文件。这个文件将是我们MutInt类定义的所在之处。添加以下代码来定义一个基本的
MutInt类:
## mutint.py
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
__slots__ 属性用于定义这个类可以拥有的属性。属性就像是属于类对象的变量。通过使用 __slots__,我们告诉 Python 使用更节省内存的方式来存储属性。在这种情况下,我们的 MutInt 类将只有一个名为 value 的属性。这意味着 MutInt 类的每个对象只能保存一个数据,即整数值。
__init__ 方法是我们类的构造函数。构造函数是一种特殊的方法,在创建类的对象时会被调用。它接受一个值参数,并将其存储在实例的 value 属性中。实例是根据类蓝图创建的单个对象。
让我们通过创建一个 Python 脚本来使用这个类来测试它:
- 在同一目录下创建一个名为
test_mutint.py的新文件:
## test_mutint.py
from mutint import MutInt
## Create a MutInt object
a = MutInt(3)
print(f"Created MutInt with value: {a.value}")
## Modify the value (demonstrating mutability)
a.value = 42
print(f"Modified value to: {a.value}")
## Try adding (this will fail)
try:
result = a + 10
print(f"Result of a + 10: {result}")
except TypeError as e:
print(f"Error when adding: {e}")
在这个测试脚本中,我们首先从 mutint.py 文件中导入 MutInt 类。然后我们创建一个初始值为 3 的 MutInt 类对象。我们打印初始值,然后将其修改为 42 并打印新值。最后,我们尝试将 10 加到 MutInt 对象上,这会导致错误,因为我们的类还不支持加法运算。
- 在终端中执行以下命令来运行测试脚本:
python3 /home/labex/project/test_mutint.py
终端是一个命令行界面,你可以在其中运行各种命令来与系统和代码进行交互。运行这个命令将执行 test_mutint.py 脚本。
你应该会看到类似以下的输出:
Created MutInt with value: 3
Modified value to: 42
Error when adding: unsupported operand type(s) for +: 'MutInt' and 'int'
我们的 MutInt 类成功地存储和更新了一个值。然而,它有几个局限性:
- 打印时显示效果不佳
- 不支持加法等数学运算
- 不支持比较操作
- 不支持类型转换
在接下来的步骤中,我们将逐一解决这些局限性,使我们的 MutInt 类更像一个真正的原始类型。
改进字符串表示形式
当你在 Python 中打印一个 MutInt 对象时,你会看到类似 <__main__.MutInt object at 0x...> 这样的输出。这个输出没什么用,因为它没有告诉你 MutInt 对象的实际值。为了让你更容易理解这个对象代表什么,我们将为字符串表示形式实现特殊方法。
- 在 WebIDE 中打开
mutint.py文件,并使用以下代码更新它:
## mutint.py
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer-friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
我们为 MutInt 类添加了三个重要的方法:
__str__():当你对对象使用str()函数或直接打印对象时,会调用这个方法。它应该返回一个人类可读的字符串。__repr__():这个方法提供对象的“官方”字符串表示形式。它主要用于调试,理想情况下应该返回一个字符串,如果将这个字符串传递给eval()函数,应该能重新创建该对象。__format__():这个方法允许你对MutInt对象使用 Python 的字符串格式化系统。你可以使用诸如填充和数字格式化之类的格式说明符。
- 创建一个名为
test_string_repr.py的新测试文件,来测试这些新方法:
## test_string_repr.py
from mutint import MutInt
## Create a MutInt object
a = MutInt(3)
## Test string representation
print(f"str(a): {str(a)}")
print(f"repr(a): {repr(a)}")
## Test direct printing
print(f"Print a: {a}")
## Test string formatting
print(f"Formatted with padding: '{a:*^10}'")
print(f"Formatted as decimal: '{a:d}'")
## Test mutability
a.value = 42
print(f"After changing value, repr(a): {repr(a)}")
在这个测试文件中,我们首先导入 MutInt 类。然后创建一个值为 3 的 MutInt 对象。我们通过使用 str() 和 repr() 函数来测试 __str__() 和 __repr__() 方法。我们还测试了直接打印、字符串格式化以及 MutInt 对象的可变性。
- 运行测试脚本:
python3 /home/labex/project/test_string_repr.py
当你运行这个命令时,Python 将执行 test_string_repr.py 脚本。你应该会看到类似以下的输出:
str(a): 3
repr(a): MutInt(3)
Print a: 3
Formatted with padding: '****3*****'
Formatted as decimal: '3'
After changing value, repr(a): MutInt(42)
现在我们的 MutInt 对象显示效果很好。字符串表示形式展示了其底层的值,并且我们可以像处理普通整数一样使用字符串格式化。
__str__() 和 __repr__() 的区别在于,__str__() 旨在产生一个对人类友好的输出,而 __repr__() 理想情况下应该产生一个字符串,当将其传递给 eval() 函数时,能够重新创建该对象。这就是为什么我们在 __repr__() 方法中包含了类名。
__format__() 方法使我们的对象能够与 Python 的格式化系统配合使用,因此我们可以使用诸如填充和数字格式化之类的格式说明符。
添加数学运算
目前,我们的 MutInt 类不支持加法等数学运算。在 Python 中,要为自定义类启用此类运算,我们需要实现特殊方法。这些特殊方法也被称为“魔术方法”或“双下划线方法”,因为它们被双下划线包围。让我们通过实现算术运算的相关特殊方法来添加加法功能。
- 在 WebIDE 中打开
mutint.py文件,并使用以下代码更新它:
## mutint.py
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer-friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
## For commutative operations like +, we can reuse __add__
return self.__add__(other)
def __iadd__(self, other):
"""Handle in-place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
我们为 MutInt 类添加了三个新方法:
__add__():当+运算符用于MutInt对象在左侧时,会调用此方法。在该方法内部,我们首先检查other操作数是MutInt实例还是int类型。如果是,则执行加法并返回一个包含结果的新MutInt对象。如果other操作数是其他类型,我们返回NotImplemented。这会告诉 Python 尝试其他方法或抛出TypeError。__radd__():当+运算符用于MutInt对象在右侧时,会调用此方法。由于加法是可交换运算(即a + b与b + a相同),我们可以直接复用__add__方法。__iadd__():当+=运算符用于MutInt对象时,会调用此方法。它不会创建新对象,而是修改现有的MutInt对象并返回它。
- 创建一个名为
test_math_ops.py的新测试文件,来测试这些新方法:
## test_math_ops.py
from mutint import MutInt
## Create MutInt objects
a = MutInt(3)
b = MutInt(5)
## Test regular addition
c = a + b
print(f"a + b = {c}")
## Test addition with int
d = a + 10
print(f"a + 10 = {d}")
## Test reversed addition
e = 7 + a
print(f"7 + a = {e}")
## Test in-place addition
print(f"Before a += 5: a = {a}")
a += 5
print(f"After a += 5: a = {a}")
## Test in-place addition with reference sharing
f = a ## f and a point to the same object
print(f"Before a += 10: a = {a}, f = {f}")
a += 10
print(f"After a += 10: a = {a}, f = {f}")
## Test unsupported operation
try:
result = a + 3.5 ## Adding a float is not supported
print(f"a + 3.5 = {result}")
except TypeError as e:
print(f"Error when adding float: {e}")
在这个测试文件中,我们首先导入 MutInt 类。然后创建一些 MutInt 对象并执行不同类型的加法运算。我们还测试了原地加法以及尝试不支持的运算(添加浮点数)的情况。
- 运行测试脚本:
python3 /home/labex/project/test_math_ops.py
你应该会看到类似以下的输出:
a + b = MutInt(8)
a + 10 = MutInt(13)
7 + a = MutInt(10)
Before a += 5: a = MutInt(3)
After a += 5: a = MutInt(8)
Before a += 10: a = MutInt(8), f = MutInt(8)
After a += 10: a = MutInt(18), f = MutInt(18)
Error when adding float: unsupported operand type(s) for +: 'MutInt' and 'float'
现在我们的 MutInt 类支持基本的加法运算。注意,当我们使用 += 时,a 和 f 都被更新了。这表明 a += 10 修改了现有对象,而不是创建了一个新对象。
这种可变对象的行为类似于 Python 的内置可变类型,如列表。例如:
a = [1, 2, 3]
b = a
a += [4, 5] ## Both a and b are updated
相比之下,对于元组等不可变类型,+= 会创建一个新对象:
c = (1, 2, 3)
d = c
c += (4, 5) ## c is a new object, d still points to the old one
实现比较运算
目前,我们的 MutInt 对象无法相互比较,也不能与普通整数进行比较。在 Python 中,像 ==、<、<=、>、>= 这样的比较运算在处理对象时非常有用。它们能让我们确定不同对象之间的关系,这在许多编程场景中(如排序、过滤和条件语句)至关重要。所以,让我们通过实现比较运算的特殊方法,为 MutInt 类添加比较功能。
- 在 WebIDE 中打开
mutint.py文件,并使用以下代码更新它:
## mutint.py
from functools import total_ordering
@total_ordering
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer-friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
return self.__add__(other)
def __iadd__(self, other):
"""Handle in-place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
def __eq__(self, other):
"""Handle equality comparison: self == other."""
if isinstance(other, MutInt):
return self.value == other.value
elif isinstance(other, int):
return self.value == other
else:
return NotImplemented
def __lt__(self, other):
"""Handle less-than comparison: self < other."""
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
我们做了几个关键改进:
从
functools模块导入并使用@total_ordering装饰器。@total_ordering装饰器是 Python 中的一个强大工具。在为类实现比较方法时,它能帮我们节省大量时间和精力。我们无需手动定义全部六个比较方法(__eq__、__ne__、__lt__、__le__、__gt__、__ge__),只需定义__eq__和另一个比较方法(在我们的例子中是__lt__)。装饰器会自动为我们生成其余四个比较方法。添加
__eq__()方法来处理相等比较(==)。此方法用于检查两个MutInt对象,或者一个MutInt对象和一个整数是否具有相同的值。添加
__lt__()方法来处理小于比较(<)。此方法用于确定一个MutInt对象,或者一个MutInt对象与一个整数相比,其值是否更小。创建一个名为
test_comparisons.py的新测试文件,来测试这些新方法:
## test_comparisons.py
from mutint import MutInt
## Create MutInt objects
a = MutInt(3)
b = MutInt(3)
c = MutInt(5)
## Test equality
print(f"a == b: {a == b}") ## Should be True (same value)
print(f"a == c: {a == c}") ## Should be False (different values)
print(f"a == 3: {a == 3}") ## Should be True (comparing with int)
print(f"a == 5: {a == 5}") ## Should be False (different values)
## Test less than
print(f"a < c: {a < c}") ## Should be True (3 < 5)
print(f"c < a: {c < a}") ## Should be False (5 is not < 3)
print(f"a < 4: {a < 4}") ## Should be True (3 < 4)
## Test other comparisons (provided by @total_ordering)
print(f"a <= b: {a <= b}") ## Should be True (3 <= 3)
print(f"a > c: {a > c}") ## Should be False (3 is not > 5)
print(f"c >= a: {c >= a}") ## Should be True (5 >= 3)
## Test with a different type
print(f"a == '3': {a == '3'}") ## Should be False (different types)
在这个测试文件中,我们创建了几个 MutInt 对象,并对它们进行不同的比较运算。我们还将 MutInt 对象与普通整数以及不同类型(这里是字符串)进行比较。通过运行这些测试,我们可以验证比较方法是否按预期工作。
- 运行测试脚本:
python3 /home/labex/project/test_comparisons.py
你应该会看到类似以下的输出:
a == b: True
a == c: False
a == 3: True
a == 5: False
a < c: True
c < a: False
a < 4: True
a <= b: True
a > c: False
c >= a: True
a == '3': False
现在我们的 MutInt 类支持所有比较运算。
@total_ordering 装饰器特别有用,因为它让我们无需手动实现全部六个比较方法。只需提供 __eq__ 和 __lt__,Python 就能自动推导出其他四个比较方法。
在实现自定义类时,通常的最佳实践是让它们既能与同类对象协作,也能在合理的情况下与内置类型协作。这就是为什么我们的比较方法既能处理 MutInt 对象,也能处理普通整数。这样,我们的 MutInt 类就能在不同的编程场景中更灵活地使用。
添加类型转换
我们的 MutInt 类目前支持加法和比较运算。但是,它不能与 Python 的内置转换函数(例如 int() 和 float())一起使用。这些转换函数在 Python 中非常有用。例如,当你想要将一个值转换为整数或浮点数以进行不同的计算或操作时,你需要依赖这些函数。因此,让我们为 MutInt 类添加与它们一起使用的功能。
- 在 WebIDE 中打开
mutint.py,并使用以下代码更新它:
## mutint.py
from functools import total_ordering
@total_ordering
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer - friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
return self.__add__(other)
def __iadd__(self, other):
"""Handle in - place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
def __eq__(self, other):
"""Handle equality comparison: self == other."""
if isinstance(other, MutInt):
return self.value == other.value
elif isinstance(other, int):
return self.value == other
else:
return NotImplemented
def __lt__(self, other):
"""Handle less - than comparison: self < other."""
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
def __int__(self):
"""Convert to int."""
return self.value
def __float__(self):
"""Convert to float."""
return float(self.value)
__index__ = __int__ ## Support array indexing and other operations requiring an index
def __lshift__(self, other):
"""Handle left shift: self << other."""
if isinstance(other, MutInt):
return MutInt(self.value << other.value)
elif isinstance(other, int):
return MutInt(self.value << other)
else:
return NotImplemented
def __rlshift__(self, other):
"""Handle reversed left shift: other << self."""
if isinstance(other, int):
return MutInt(other << self.value)
else:
return NotImplemented
我们向 MutInt 类添加了三个新方法:
__int__(): 当你在我们的MutInt类的对象上使用int()函数时,会调用此方法。例如,如果你有一个MutInt对象a,并且你编写了int(a),Python 将调用a对象的__int__()方法。__float__(): 类似地,当你在我们的MutInt对象上使用float()函数时,会调用此方法。__index__(): 此方法用于专门需要整数索引的操作。例如,当你想要使用索引访问列表中的元素,或执行位长度操作时,Python 需要一个整数索引。__lshift__(): 当MutInt对象位于<<运算符的左侧时,此方法处理左移操作。__rlshift__(): 当MutInt对象位于<<运算符的右侧时,此方法处理左移操作。
__index__ 方法对于需要整数索引的操作至关重要,例如列表索引、切片和位长度操作。在我们的简单实现中,我们将其设置为与 __int__ 相同,因为我们的 MutInt 对象的值可以直接用作整数索引。
__lshift__ 和 __rlshift__ 方法对于支持按位左移操作至关重要。它们允许我们的 MutInt 对象参与按位运算,这是类整数类型(integer-like types)的常见要求。
- 创建一个名为
test_conversions.py的新测试文件来测试这些新方法:
## test_conversions.py
from mutint import MutInt
## Create a MutInt object
a = MutInt(3)
## Test conversions
print(f"int(a): {int(a)}")
print(f"float(a): {float(a)}")
## Test using as an index
names = ['Dave', 'Guido', 'Paula', 'Thomas', 'Lewis']
print(f"names[a]: {names[a]}")
## Test using in bit operations (requires __index__)
print(f"1 << a: {1 << a}") ## Shift left by 3
## Test hex/oct/bin functions (requires __index__)
print(f"hex(a): {hex(a)}")
print(f"oct(a): {oct(a)}")
print(f"bin(a): {bin(a)}")
## Modify and test again
a.value = 4
print(f"\nAfter changing value to 4:")
print(f"int(a): {int(a)}")
print(f"names[a]: {names[a]}")
- 运行测试脚本:
python3 /home/labex/project/test_conversions.py
你应该看到类似于这样的输出:
int(a): 3
float(a): 3.0
names[a]: Thomas
1 << a: 8
hex(a): 0x3
oct(a): 0o3
bin(a): 0b11
After changing value to 4:
int(a): 4
names[a]: Lewis
现在,我们的 MutInt 类可以转换为标准 Python 类型,并用于需要整数索引的操作。
__index__ 方法尤为重要。它是在 Python 中引入的,允许对象在需要整数索引的情况下使用,例如列表索引、按位运算和各种函数(如 hex()、oct() 和 bin())。
通过这些添加,我们的 MutInt 类现在是一个相当完整的原始类型(primitive type)。它可以在大多数可以使用常规整数的上下文中使用,并且具有可变的额外好处。
完整的 MutInt 实现
这是我们完整的 MutInt 实现,包含我们添加的所有功能:
## mutint.py
from functools import total_ordering
@total_ordering
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer - friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
return self.__add__(other)
def __iadd__(self, other):
"""Handle in - place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
def __eq__(self, other):
"""Handle equality comparison: self == other."""
if isinstance(other, MutInt):
return self.value == other.value
elif isinstance(other, int):
return self.value == other
else:
return NotImplemented
def __lt__(self, other):
"""Handle less - than comparison: self < other."""
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
def __int__(self):
"""Convert to int."""
return self.value
def __float__(self):
"""Convert to float."""
return float(self.value)
__index__ = __int__ ## Support array indexing and other operations requiring an index
def __lshift__(self, other):
"""Handle left shift: self << other."""
if isinstance(other, MutInt):
return MutInt(self.value << other.value)
elif isinstance(other, int):
return MutInt(self.value << other)
else:
return NotImplemented
def __rlshift__(self, other):
"""Handle reversed left shift: other << self."""
if isinstance(other, int):
return MutInt(other << self.value)
else:
return NotImplemented
此实现涵盖了在 Python 中创建新原始类型(primitive type)的关键方面。为了使其更加完整,你可以为其他操作(如减法、乘法、除法等)实现其他方法。
总结
在这个实验中,你学习了如何在 Python 中创建自己的原始类型。具体来说,你掌握了创建一个类似于内置类型的可变整数类,实现用于对象显示的特殊方法,添加对数学和比较运算的支持,以及为各种 Python 上下文启用类型转换。
这些概念对于理解 Python 的对象模型至关重要,并且可用于创建能与内置操作良好集成的自定义类型。为了进一步提升你的知识水平,你可以考虑实现更多的数学运算,添加对其他内置函数的支持,以及探索像自定义集合这样的复杂类型。Python 中的自定义类型是根据特定需求扩展该语言的强大工具。