文章目录
python风格对象的实现方法
前言
Keep it simple,stupid
不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则
class Vector2d:
typecode = 'd'
def __init__(self,x,y):
self.__x = float(x)
self.__y = float(y)
self._l = 1
self.__l = 1
def __hash__(self):
return hash(self.x) ^ hash(self.y)
def __iter__(self):
# 元祖表达式返回的是一个迭代器generator
return ( i for i in (self.x,self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)])+bytes(array(self.typecode, self)))
def __bool__(self):
return bool(0)
def __eq__(self, other):
if isinstance(other,self.__class__):
return self.x == other.x and self.y == other.y
return False
def __format__(self, format_spec=''):
if format_spec == 'r':
return f"({self.y}, {self.x})"
else:
return f"({self.x}, {self.y},haha)"
def __abs__(self):
return abs(self.x)
@property
def x(self):
return self.__x
@x.setter
def x(self,value):
self.__x = value
@property
def y(self):
return self.__y
def ldsx1(self):
print(self.y,'aa')
return self.y
init
在 __init__
方法中设置属性时,会执行 __setattr__
方法。
在 Python 中,当在 __init__
方法中设置属性时,会调用 __setattr__
方法来实际设置属性的值。这意味着 __setattr__
方法在对象初始化期间会被调用。
以下是一个修正后的示例代码来说明这一点:
class MyClass:
def __init__(self):
self.attribute = 0
def __setattr__(self, name, value):
print("__setattr__ called")
super().__setattr__(name, value)
obj = MyClass()
输出结果为:
__setattr__ called
可以看到,在 __init__
方法中设置属性时,__setattr__
方法被调用,并打印了相应的信息。
repr
repr() 以便于开发者理解的方式返回对象的字符串表示形式,实现__reper__的作用就是支持repr()方法
print(repr(实例))打印的就是__repr__实现的内容
str
str() 以便于用户理解的方式返回对象的字符串表示形式,实现__str__的作用就是支持str()方法
print(实例)打印的就是__str__实现的内容,print()默认获取str方法
iter(变成可迭代对象,优先级高于getitem)
把实例变成可迭代对象,可以直接return generator也可以yield
当同时实现了 __iter__
和 __getitem__
方法时,__iter__
方法具有更高的优先级。这意味着如果一个对象既实现了 __iter__
方法又实现了 __getitem__
方法,那么在迭代该对象时,Python 会首先尝试调用 __iter__
方法。
在迭代对象时,__iter__
方法被首先调用。而在索引访问对象时,__getitem__
方法被调用。
因此,在同时实现了 __iter__
和 __getitem__
方法时,__iter__
方法具有更高的优先级。但如果只实现了 __getitem__
方法,而没有实现 __iter__
方法,则会直接使用 __getitem__
方法进行迭代和索引访问。
如果没有 iter 和 contains 方法,Python 会调用 getitem 方法,设法让迭代和 in 运算符可用。
contains(实用in运算符时调用)
用于判断对象是否包含某个元素。它在使用 in
运算符时被调用
当定义了一个类,并且希望该类的实例能够支持 in
运算符来检查是否包含某个元素时,可以在类中实现 __contains__
方法。
下面是一个示例来说明 __contains__
的作用:
class MyContainer:
def __init__(self, items):
self.items = items
def __contains__(self, item):
return item in self.items
my_container = MyContainer([1, 2, 3, 4, 5])
print(3 in my_container) # 输出: True
print(6 in my_container) # 输出: False
如果没有 iter 和 contains 方法,Python 会调用 getitem 方法,设法让迭代和 in 运算符可用。
eq
可以让实例可以进行比较 可使用==方法,如果不重写应该默认比对实例的内存地址是否一致
abs
用于计算对象的绝对值。当你对一个对象调用内置的 abs()
函数时,会自动调用该对象的 __abs__
方法来执行相应的操作。
abs(实例)
bool
用于确定对象的布尔值。当需要将一个实例作为条件表达式进行布尔判断时,会自动调用该对象的 __bool__
方法来执行相应的操作。bool(实例触发)
根据默认规则,如果一个类没有定义 __bool__
方法,则会尝试调用其父类(如果有的话)的 __bool__
方法。如果没有找到任何定义的 __bool__
方法,则会尝试调用 __len__
方法(如果定义了),并根据返回值是否为零来确定布尔值。如果既没有定义 __bool__
方法,也没有 __len__
方法,则对象的布尔值默认为 True
。
format
在我们的示例中,如果传入的格式规范字符串为 'r'
,则 __format__
方法返回反转的坐标形式 (y, x)
。否则,它将返回默认的坐标形式 (x, y)
。
通过重写 __format__
方法,你可以自定义对象的格式化输出,以适应特定的需求。你可以根据实际情况和格式规范字符串来编写逻辑,从而生成你想要的格式化结果。
hash
在 Python 类中,如果你希望对象能够被正确地用作键(key)存储在散列数据结构(如字典)中,你需要实现两个特殊方法:__hash__()
和 __eq__()
。
__hash__()
方法:这个方法返回一个整数值,表示对象的哈希值。哈希值用于确定对象在散列数据结构中的存储位置。对于可变对象来说,默认情况下是没有实现__hash__()
方法的,因为可变对象的哈希值可能会发生改变。但是,不可变对象(如字符串、元组)默认实现了该方法。__eq__()
方法:这个方法用于比较两个对象是否相等。在散列数据结构中,在两个对象的哈希值相等的情况下,会调用__eq__()
方法进行进一步的比较。如果不实现__eq__()
方法,默认比较的是对象的身份标识(即内存地址),这通常不是我们想要的行为。
通过实现这两个方法,你可以确保对象在散列数据结构中被正确处理。__hash__()
方法提供了对象的哈希值以便快速查找,而 __eq__()
方法用于确保散列冲突时能够正确判断对象相等性。
需要注意的是,当你自定义类时,默认情况下继承自 object
的类已经实现了适当的 __hash__()
和 __eq__()
方法。但是,如果你自定义了 __eq__()
方法,则通常也需要相应地自定义 __hash__()
方法,以确保两个相等的对象具有相同的散列值。
总而言之,为了正确使用自定义类对象作为键存储在散列数据结构中,你需要实现 __hash__()
方法来提供哈希值,并且最好也实现 __eq__()
方法来确保正确比较对象的相等性。这样可以保证在键的查找和比较时得到期望的结果。
getitem(实现切片,在无iter,contains时候会默认调用代替)
__getitem__
方法用于通过索引访问对象中的元素。当你使用类似obj[index]
的语法时,会自动调用该对象的__getitem__
方法,将索引作为参数传递给该方法。- 可索引对象可以是任何支持按索引访问的数据结构,例如列表、字典、字符串等。
- 在
__getitem__
方法中,你可以根据传递的索引值执行相应的操作,并返回对应的元素。如果索引超出范围,则可以引发IndexError
异常。 __iter__
用于返回一个迭代器对象,使对象可以通过迭代方式访问元素。__getitem__
用于根据索引值获取对象中的元素,使对象可以像序列一样按索引进行访问。
def __getitem__(self, index):
cls = type(self)
if isinstance(index, slice):
return cls(self._components[index])
elif isinstance(index, numbers.Integral):
return self._components[index]
else:
msg = '{.__name__} indices must be integers'
raise TypeError(msg.format(cls))
在上述代码中,cls(self._components[index])
和 self._components[index]
是两种不同的操作。
self._components[index]
:这是直接访问对象的_components
属性,并使用索引index
获取对应的元素。这是一种直接的访问方式,返回的是_components
中索引位置处的值。cls(self._components[index])
:这是通过调用类的构造函数创建一个新的类实例。该实例的_components
属性是根据索引index
在原_components
上进行切片操作得到的结果。这种方式可以用于创建一个包含部分元素的新对象。
总结来说,self._components[index]
返回的是 _components
中索引位置处对应的值,而 cls(self._components[index])
创建了一个新的类实例,其中的 _components
是根据索引截取后的结果。具体使用哪种方式取决于需要何种行为和返回类型。
setitem
-
__setitem__
方法被用于实现映射类型(如字典)的对象,例如通过索引访问元素。 -
__setitem__(self, key, value)
:-
用于映射类型的对象,例如自定义的字典类。
-
在给对象的键(或索引)赋值时调用。
-
接受三个参数:self 是当前对象,key 是要设置的键,value 是要设置的值。
-
可以使用
obj[key] = value
的形式来调用该方法。 -
示例代码:
class MyDict: def __init__(self): self.data = {} def __setitem__(self, key, value): self.data[key] = value my_dict = MyDict() my_dict['key1'] = 'value1' # 调用 __setitem__
-
len
__len__
是一个特殊方法,用于返回对象的长度或大小。它是 Python 内置的一个魔术方法,可以通过调用 len(object)
来触发。
当你在自定义类中实现了 __len__
方法时,就可以对该类的实例应用内置函数 len()
来获取对象的长度信息。
getattr
__getattr__
是 Python 中的一个特殊方法,用于在访问不存在的属性时进行处理。当你使用点号(.)访问一个对象的属性,而该属性不存在时,Python 会尝试调用该对象的 __getattr__
方法来处理这个情况。
下面是一个示例,演示如何定义和使用 __getattr__
方法:
class Database:
def __init__(self):
self.data = {
"user1": "password1",
"user2": "password2"
}
def __getattr__(self, name):
if name.startswith("get_password_"):
username = name[len("get_password_"):]
return self.get_password(username)
else:
raise AttributeError(f"'Database' object has no attribute '{name}'")
def get_password(self, username):
return self.data.get(username)
# 使用 Database 类
db = Database()
# 获取 user1 的密码
password = db.get_password_user1
print(password) # 输出: password1
# 尝试获取不存在的属性
attribute_error = db.some_attribute # 抛出 AttributeError 异常
在上述示例中,我们定义了一个名为 Example
的类,并实现了 __getattr__
方法。当我们通过 obj.foo
访问对象 obj
的属性 foo
时,由于该属性不存在,Python 会自动调用 __getattr__
方法,并将属性名作为参数传递给该方法。在我们的示例中,__getattr__
方法会打印一条消息,并返回 None
。
需要注意的是,__getattr__
方法只会在访问不存在的属性时被调用一次。如果在之后再次访问相同的不存在的属性,它将不会被触发,而是直接返回 AttributeError
异常。
除了 __getattr__
方法,还有一个相关的方法 __getattribute__
,它会在每次访问属性时都被调用,而不仅仅是在访问不存在的属性时。请根据具体需求选择合适的方法进行处理。
在实际开发中,__getattr__
方法可以用于动态地处理对象属性的访问和获取。它提供了一种灵活的方式来处理属性不存在的情况,并可以根据需要进行自定义操作。
以下是一些在实际开发中使用 __getattr__
方法的常见场景:
- 延迟加载:可以将某些属性的计算或获取延迟到实际访问时再执行,从而提高性能和效率。在
__getattr__
方法中,你可以根据属性的名称动态地加载和返回属性值。 - 动态属性:根据不同的属性名称,返回不同的属性值。你可以根据需要在
__getattr__
方法中编写逻辑,根据属性名称返回相应的值。 - 代理操作:当对象本身无法满足某个属性的需求时,可以使用
__getattr__
方法将属性获取的操作委托给其他对象来处理。这样可以实现属性的动态转发。 - 错误处理:当访问不存在的属性时,可以通过
__getattr__
方法捕获这些错误,并采取相应的处理措施,以防止程序崩溃或异常抛出。
需要注意的是,__getattr__
方法只会在访问不存在的属性时被调用。对已经存在的属性,Python 不会调用该方法。如果要在每次访问属性时都进行处理,包括已存在的属性,可以使用 __getattribute__
方法。
在实际开发中,你可以根据具体需求和场景,合理利用 __getattr__
方法来增强对象的灵活性和功能。
setattr
在__init__的时候 会马上去_stattr__中进行实例存储_dict
__setattr__
是 Python 中的一个特殊方法,用于在给属性赋值时进行处理。当你使用赋值语句给对象的属性赋值时,Python 会尝试调用对象的 __setattr__
方法来处理这个操作。
__setattr__
方法可以用于自定义属性赋值的行为,例如对属性值进行验证、记录赋值历史、限制属性的修改等。
下面是一个示例,演示如何定义和使用 __setattr__
方法:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __setattr__(self, name, value):
if name == "age" and value < 0:
raise ValueError("Age cannot be negative")
else:
super().__setattr__(name, value)
# 使用 Person 类
person = Person("John", 25)
print(person.name) # 输出: John
print(person.age) # 输出: 25
person.age = -10 # 尝试给年龄赋值为负数,抛出 ValueError 异常
输出结果:
Setting attribute: foo = 42
在上述示例中,我们定义了一个名为 Example
的类,并实现了 __setattr__
方法。当我们通过赋值语句 obj.foo = 42
给对象 obj
的属性 foo
赋值时,Python 会自动调用 __setattr__
方法,并将属性名和属性值作为参数传递给该方法。在我们的示例中,__setattr__
方法会打印一条消息,并将属性名和属性值存储到对象的 __dict__
属性中。
需要注意的是,在 __setattr__
方法内部,如果直接对属性进行赋值,会导致无限递归调用 __setattr__
方法。为了避免这种情况,我们可以使用 self.__dict__[name] = value
来绕过 __setattr__
方法并直接操作对象的属性字典。
在重写 __setattr__
方法时,通常需要调用父类的 __setattr__
方法来处理属性赋值的默认行为。这是因为 __setattr__
方法是一个特殊方法,负责处理对象属性的赋值操作。如果我们完全重写了 __setattr__
方法而没有调用父类的方法,将会导致一些重要的功能失效,例如:
- 阻止无限递归调用:在
__setattr__
方法中直接使用self.__setattr__(name, value)
进行属性赋值,会导致无限递归调用自身的__setattr__
方法。通过调用父类的__setattr__
方法,可以避免无限递归的问题。 - 保留属性字典的默认行为:父类的
__setattr__
方法负责维护对象的属性字典,将属性名和属性值保存在self.__dict__
中。如果不调用父类的方法,属性赋值将不会被正确地保存在self.__dict__
中,可能导致对象无法正常工作。 - 其他继承的功能:通过调用父类的
__setattr__
方法,我们可以保留其他可能由父类提供的功能或逻辑,确保子类在赋值属性时能够获得完整的继承特性。
因此,为了避免上述问题,并确保对象的属性赋值行为符合预期,通常需要在重写的 __setattr__
方法中调用父类的 __setattr__
方法。这样可以将控制流程交给父类处理默认行为,同时允许我们添加自定义的逻辑或修改默认行为。
在实现 __setattr__
方法时,不直接修改 __dict__
是推荐的做法。
__setattr__
方法用于在给对象属性赋值时进行拦截和处理。通常,我们会使用 self.__dict__[name] = value
或者 super().__setattr__(name, value)
的方式将属性存储在对象的 __dict__
中。
直接使用 self.__dict__[name] = value
的方式可以绕过 __setattr__
方法,可能导致特殊逻辑或校验被绕过,从而引入潜在的错误或意外行为。
使用 super().__setattr__(name, value)
的方式可以确保遵循属性赋值的规范,并触发所需的逻辑,例如类型检查、拦截器等。
下面是一个示例,展示了如何在自定义类中实现 __setattr__
方法:
class MyClass:
def __init__(self):
self._data = {}
def __setattr__(self, name, value):
if name.startswith("_"):
super().__setattr__(name, value)
else:
self._data[name] = value
obj = MyClass()
obj.name = "John" # 调用 __setattr__
print(obj.name) # 输出: John
在上述代码中,当我们给 obj
对象的属性赋值时,__setattr__
方法会被调用。如果属性名以 “_” 开头,则使用 super().__setattr__(name, value)
将属性存储在对象的 __dict__
中;否则,将属性存储在 _data
字典中。这样,我们可以有选择地进行属性赋值的处理和存储。
dict
在 Python 中,__dict__
是一个特殊的属性,它是对象的一个字典,用于存储对象的属性和方法。
每个对象都有一个 __dict__
属性,它是一个字典(或其他可映射对象),其中的键是属性名,值是属性的值。通过 obj.__dict__
可以访问对象的 __dict__
属性。
下面是一个简单的示例,展示了如何使用 __dict__
属性来查看对象的属性和方法:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def say_hello(self):
print("Hello!")
person = Person("John", 25)
print(person.__dict__) # 输出: {'name': 'John', 'age': 25}
person.city = "New York"
print(person.__dict__) # 输出: {'name': 'John', 'age': 25, 'city': 'New York'}
在上述示例中,我们定义了一个 Person
类,并创建了一个 person
对象。通过 person.__dict__
,我们可以查看 person
对象的属性字典。
初始时,person
对象的属性字典包含了 name
和 age
两个键值对。当我们为 person
对象动态添加一个新的属性 city
后,该属性也会出现在 __dict__
字典中。
需要注意的是,由于 __dict__
是一个字典,所以它具有字典的特性,例如可以使用 keys()
、values()
和 items()
等方法来查看字典的键、值和键值对。但是,直接修改 __dict__
可能会绕过对象的属性访问控制和一些特殊行为,因此应该小心使用。
在 Python 中,我们不能直接重写 __dict__
属性,因为它是一个特殊的属性而不是一个可重写的方法。__dict__
是由解释器自动创建和管理的对象字典,用于存储对象的属性和方法。
然而,我们可以通过重写 __getattr__
和 __setattr__
方法来控制属性的访问和赋值行为。下面是一个示例,演示了如何通过重写这两个方法来自定义对象的属性字典:
class CustomDict:
def __init__(self):
self._custom_dict = {}
def __getattr__(self, name):
return self._custom_dict.get(name)
def __setattr__(self, name, value):
self._custom_dict[name] = value
obj = CustomDict()
obj.name = "John"
obj.age = 25
print(obj.__dict__) # 输出: {'_custom_dict': {'name': 'John', 'age': 25}}
print(obj.name) # 输出: John
print(obj.age) # 输出: 25
在上述示例中,我们创建了一个名为 CustomDict
的类,并在其内部使用一个名为 _custom_dict
的字典属性来代替默认的 __dict__
。通过重写 __getattr__
方法,当我们访问对象的属性时,会从 _custom_dict
中获取相应的值。通过重写 __setattr__
方法,当我们为对象赋值属性时,会将属性名和属性值保存到 _custom_dict
中。
需要注意的是,这只是一种近似于重写 __dict__
的方式,并且在某些情况下可能会有一些限制和副作用。一般来说,直接操作 __dict__
是不常见且不推荐的做法,通常更好地利用 Python 提供的属性访问控制机制和特殊方法来管理对象的属性字典。
slots
默认情况下,Python 在各个实例中名为_dict_ 的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万 个属性不多的实例,通过 slots 类属性,能节省大量内存,方法是让解释器在元 组中存储实例属性,而不用字典。
定义 slots 的方式是,创建一个类属性,使用 slots 这个名字,并把它的 值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元 组,因为这样定义的 slots 中所含的信息不会变化
class Vector2d:
__slots__ = ('__x', '__y')
typecode = ‘d’
在类中定义 slots 属性的目的是告诉解释器:“这个类中的所有实例属性都在这 儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使 用消耗内存的 dict 属性。如果有数百万个实例同时活动,这样做能节省大量内 存。
在类中定义 slots 属性之后,实例不能再有 slots 中所列名 称之外的其他属性。这只是一个副作用,不是 slots 存在的真正原因。不要 使用 slots 属性禁止类的用户新增实例属性。slots 是用于优化的, 不是为了约束程序员。
私有变量
如果以 __mood 的形式(两个前导下划线,尾部没有或最多有一个 下划线)命名实例属性,Python 会把属性名存入实例的 dict 属性中,而且会在 前面加上一个下划线和类名。因此,对 Dog 类来说,__mood 会变成 _Dog__mood; 对 Beagle 类来说,会变成 _Beagle__mood。这个语言特性叫名称改写(name mangling)。
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}
>>> v1._Vector2d__x
3.0
也不是所有人都喜欢 self.__x 这种不 对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀编写“受保护”的属 性(如 self._x)。批评使用两个下划线这种改写机制的人认为,应该使用命名约定 来避免意外覆盖属性。本章开头引用了多产的 Ian Bicking 的一句话,那句话的完整表 述如下:
绝对不要使用两个前导下划线,这是很烦人的自私行为。如果担心名称冲突,应该 明确使用一种名称改写方式(如 _MyThing_blahblah)。这其实与使用双下划 线一样,不过自己定的规则比双下划线易于理解。
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序 员严格遵守的约定,他们不会在类外部访问这种属性。遵守使用一个下划线标记对象 的私有属性很容易,就像遵守使用全大写字母编写常量那样容易。
最终案例
from array import array
import reprlib
import math
import numbers
import functools import operator import itertools ➊
class Vector:
typecode = 'd'
def __init__(self, components):
self._components = array(self.typecode, components)
def __iter__(self):
return iter(self._components)
def __repr__(self):
components = reprlib.repr(self._components)
components = components[components.find('['):-1]
return 'Vector({})'.format(components)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components))
def __eq__(self, other):
return (len(self) == len(other) and
all(a == b for a, b in zip(self, other)))
def __hash__(self):
hashes = (hash(x) for x in self)
return functools.reduce(operator.xor, hashes, 0)
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __bool__(self):
return bool(abs(self))
def __len__(self):
return len(self._components)
def __getitem__(self, index):
cls = type(self)
if isinstance(index, slice):
return cls(self._components[index])
elif isinstance(index, numbers.Integral):
return self._components[index]
else:
msg = '{.__name__} indices must be integers'
raise TypeError(msg.format(cls))
shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self)
if len(name) == 1:
pos = cls.shortcut_names.find(name)
if 0 <= pos < len(self._components):
return self._components[pos]
msg = '{.__name__!r} object has no attribute {!r}'
raise AttributeError(msg.format(cls, name))
def angle(self, n): ➋
r = math.sqrt(sum(x * x for x in self[n:]))
a = math.atan2(r, self[n-1])
if (n == len(self) - 1) and (self[-1] < 0):
return math.pi * 2 - a
else:
return a
def angles(self): ➌
return (self.angle(n) for n in range(1, len(self)))
标签:__,name,setattr,python,self,对象,风格,方法,属性
From: https://blog.csdn.net/weixin_43322583/article/details/141690533