深入理解 python 虚拟机:花里胡哨的魔术方法
深入分析 hash 方法
在 Python 中,__hash__()
方法是一种特殊方法(也称为魔术方法或双下划线方法),用于返回对象的哈希值。哈希值是一个整数,用于在字典(dict
)和集合(set
)等数据结构中进行快速查找和比较。__hash__()
方法在创建自定义的可哈希对象时非常有用,例如自定义类的实例,以便可以将这些对象用作字典的键或集合的元素。
__hash__() 方法:
- 如果两个对象相等(根据
- 重写
__hash__()
方法通常需要同时重写__eq__()
方法,以确保对象的相等性和哈希值的一致性。 - 如果对象没有定义
__eq__
方法,那么也不要定义__hash__
方法,因为如果遇到哈希值相等的对象时候,如果无法对两个对象进行比较的话,那么也会导致容易当中有多个相同的对象。
__eq__()
方法的定义),它们的哈希值应该相等。即,如果 a == b
为真,则 hash(a) == hash(b)
也为真,这一点非常重要,因为我们在使用集合和字典的时候,就需要保证容器当中每种对象只能够有一个,如果不满足这个条还的话,那么就可能会导致同一种对象在容器当中会存在多个。
import random
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.name == other.name and self.age == other.age
def __hash__(self):
return hash((self.name, self.age)) + random.randint(0, 1024)
def __repr__(self):
return f"[name={self.name}, age={self.age}]"
person1 = Person("Alice", 25)
person2 = Person("Alice", 25)
print(hash(person1))
print(hash(person2))
container = set()
container.add(person1)
container.add(person2)
print(container)
在上面代码当中我们重写了 __hash__
函数,但是对象的哈希值每次调用的时候我们都加入一个随机数,因此即使 name 和 age 都相等,如果 hash 值不想等,那么可能会造成容器当中存在多个相同的对象,上面的代码就会造成相同的对象,上面的程序输出结果如下所示:
1930083569156318318
1930083569156318292
{[name=Alice, age=25], [name=Alice, age=25]}
如果重写上面的类对象:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.name == other.name and self.age == other.age
def __hash__(self):
return hash((self.name, self.age))
def __repr__(self):
return f"[name={self.name}, age={self.age}]"
那么容器器当中只会有一个对象。
__hash__方法的时候也会造成容器当中有多个相同的对象。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# def __eq__(self, other):
# return self.name == other.name and self.age == other.age
def __hash__(self):
return hash((self.name, self.age)) # + random.randint(0, 1024)
def __repr__(self):
return f"[name={self.name}, age={self.age}]"
这是因为如果哈希值相同的时候还需要在比较两个对象是否相等,如果相等那么就不需要将这个对象保存到容器当中,如果不相等那么将会将这个对象加入到容器当中。
bool 方法
object.__bool__() 方法是一种特殊方法,用于定义对象的布尔值。它在使用布尔运算符(如 if
语句和逻辑运算)时自动调用。__bool__()
方法应该返回一个布尔值,表示对象的真值。如果 __bool__()
方法未定义,Python 将尝试调用 __len__()
方法来确定对象的真值。如果 __len__()
方法返回零,则对象被视为假;否则,对象被视为真。
__bool__() 方法:
-
__bool__()
方法应该返回一个布尔值(True
或False
)。 - 如果
__bool__()
方法未定义,Python 将尝试调用__len__()
方法来确定对象的真值。 - 当对象的长度为零时,即
__len__()
方法返回零,对象被视为假;否则,对象被视为真。 - 如果既未定义
__bool__()
方法,也未定义__len__()
方法,则对象默认为真。
__bool__()
方法在对象被应用布尔运算时自动调用。例如,在 if
语句中,对象的真值由 __bool__()
方法确定。
__bool__() 方法:
class NonEmptyList:
def __init__(self, items):
self.items = items
def __bool__(self):
return len(self.items) > 0
my_list = NonEmptyList([1, 2, 3])
if my_list:
print("The list is not empty.")
else:
print("The list is empty.")
对象的属性访问
在Python中,我们可以通过一些特殊方法来定制属性访问的行为。本文将深入介绍这些特殊方法,包括__getitem__()
、__setitem__()
、__delitem__()
和__getattr__()
方法,以帮助更好地理解属性访问的机制和应用场景。
__getitem__()方法是用于索引操作的特殊方法。当我们通过索引访问对象的属性时,Python会自动调用该方法,并传入索引值作为参数。我们可以在该方法中实现对属性的获取操作,并返回相应的值。
class MyList:
def __init__(self):
self.data = []
def __getitem__(self, index):
return self.data[index]
my_list = MyList()
my_list.data = [1, 2, 3]
print(my_list[1]) # 输出: 2
在上面的例子中,我们定义了一个名为MyList的类,它具有一个属性data,该属性是一个列表。通过重写__getitem__()方法,我们使得可以通过索引来访问MyList对象的data属性。当我们使用my_list[1]的形式进行索引操作时,Python会自动调用__getitem__()方法,并将索引值1作为参数传递给该方法。
__setitem__()方法用于属性的设置操作,即通过索引为对象的属性赋值。当我们使用索引操作并赋值给对象的属性时,Python会自动调用__setitem__()
方法,并传入索引值和赋值的值作为参数。
class MyList:
def __init__(self):
self.data = [0 for i in range(2)]
def __setitem__(self, index, value):
self.data[index] = value
my_list = MyList()
my_list[0] = 1
my_list[1] = 2
print(my_list.data) # 输出: [1, 2]
在上述示例中,我们重写了__setitem__()
方法来实现对对象属性的设置操作。当我们执行my_list[0] = 1和my_list[1] = 2的赋值操作时,Python会自动调用__setitem__()
方法,并将索引值和赋值的值传递给该方法。在__setitem__()
方法中,我们将值赋给了对象的data属性的相应索引位置。
__delitem__()方法用于删除对象属性的特殊方法。当我们使用del语句删除对象属性时,Python会自动调用__delitem__()
方法,并传入要删除的属性的索引值作为参数。
class MyDict:
def __init__(self):
self.data = dict()
def __delitem__(self, key):
print("In __delitem__")
del self.data[key]
obj = MyDict()
obj.data["key"] = "val"
del obj["key"] # 输出 In __delitem__
__getattr__()
是一个特殊方法,用于在访问不存在的属性时自动调用。它接收一个参数,即属性名,然后返回相应的值或引发AttributeError
异常。class MyClass: def __getattr__(self, name): if name == 'color': return 'blue' else: raise AttributeError(f"'MyClass' object has no attribute '{name}'") my_obj = MyClass() print(my_obj.color) # 输出: blue print(my_obj.size) # 引发 AttributeError: 'MyClass' object has no attribute 'size'
在上面的示例中,当访问
my_obj.color
时,由于color
属性不存在,Python 会自动调用__getattr__()
方法,并返回预定义的值'blue'
。而当访问my_obj.size
时,由于该属性也不存在,__getattr__()
方法会引发AttributeError
异常。__setattr__() 是一个特殊方法,用于在设置属性值时自动调用。它接收两个参数,即属性名和属性值。我们可以在该方法中对属性进行处理、验证或记录。
class MyClass: def __init__(self): self.color = 'red' # 输出:Setting attribute 'color' to 'red' def __setattr__(self, name, value): print(f"Setting attribute '{name}' to '{value}'") super().__setattr__(name, value) my_obj = MyClass() my_obj.color = 'blue' # 输出: Setting attribute 'color' to 'blue'
当我们使用 . 的方式去访问对象属性的时候,首先会调用对象的
__getattribute__
函数,如果属性不存在才会调用__getattr__
。当__getattribute__
方法无法找到指定的属性时,Python 会调用__getattr__
方法。以下是在之前的示例类CustomClass
上添加__getattr__
方法的代码:class CustomClass: def __init__(self): self.attribute = "Hello, world!" def __getattribute__(self, name): print(f"Accessing attribute: {name}") return super().__getattribute__(name) def __getattr__(self, name): print(f"Attribute {name} not found") return None
在这个示例中,我们在
CustomClass
中添加了__getattr__
方法。当__getattribute__
方法无法找到指定的属性时,会自动调用__getattr__
方法,并打印出属性名称 "attribute" 以及未找到属性的提示信息。obj = CustomClass() print(obj.attribute) print(obj.nonexistent_attribute)
输出结果如下所示:
Accessing attribute: attribute Hello, world! Accessing attribute: nonexistent_attribute Attribute nonexistent_attribute not found None
首先,我们访问存在的属性
attribute
,此时__getattribute__
方法被调用,并打印出属性名称 "attribute",然后返回属性的实际值 "Hello, world!"。接着,我们尝试访问不存在的属性nonexistent_attribute
,由于__getattribute__
方法无法找到该属性,因此会调用__getattr__
方法,并打印出属性名称 "nonexistent_attribute" 以及未找到属性的提示信息,然后返回None
。上下文管理器
__enter__ 和
__exit__
方法来创建自定义的上下文管理器。object.__enter__ 和
object.__exit__
方法来创建一个文件操作的上下文管理器:class FileContextManager: def __init__(self, filename, mode): self.filename = filename self.mode = mode self.file = None def __enter__(self): self.file = open(self.filename, self.mode) return self.file def __exit__(self, exc_type, exc_value, traceback): self.file.close() with FileContextManager('example.txt', 'w') as file: file.write('Hello, world!')
在上述示例中,
FileContextManager
类实现了__enter__
和__exit__
方法。在__enter__
方法中,我们打开文件并返回文件对象,这样在with
语句块中就可以使用该文件对象。在__exit__
方法中,我们关闭文件。__exit__ 方法都会被调用来确保文件被正确关闭。这样可以避免资源泄露和文件锁定等问题。使用上下文管理器可以简化代码,并提供一致的资源管理方式,特别适用于需要打开和关闭资源的情况,如文件操作、数据库连接等。
__exit__ 方法有三个参数:
exc_type
、exc_value
和traceback
。下面是对这些参数的详细介绍:
-
exc_value(异常值):这个参数表示引发的异常的实例。它包含了关于异常的详细信息,如错误消息。如果没有异常被引发,它的值也将为
None
。 -
traceback(回溯信息):这个参数是一个回溯对象,它包含了关于异常的堆栈跟踪信息。它提供了导致异常的代码路径和调用关系。如果没有异常被引发,它的值将为
None
。
exc_type(异常类型):这个参数表示引发的异常的类型。如果在上下文管理器的代码块中没有引发异常,它的值将为 None
。如果有异常被引发,exc_type
将是引发异常的类型。
总结
本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。