python风格对象的实现方法

文章目录

  • python风格对象的实现方法
    • 前言
    • __init__
    • __repr__
    • __str__
    • __iter__(变成可迭代对象,优先级高于getitem)
    • __contains__(实用in运算符时调用)
    • __eq__
    • __abs__
    • __bool__
    • __format__
    • __hash__
    • __getitem__(实现切片,在无iter,contains时候会默认调用代替)
    • __setitem__
    • __len__
    • __getattr__
    • __setattr__
    • __dict__
    • __slots__
    • 私有变量
    • 最终案例

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__ 方法进行迭代和索引访问。

如果没有 itercontains 方法,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

如果没有 itercontains 方法,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__()

  1. __hash__() 方法:这个方法返回一个整数值,表示对象的哈希值。哈希值用于确定对象在散列数据结构中的存储位置。对于可变对象来说,默认情况下是没有实现 __hash__() 方法的,因为可变对象的哈希值可能会发生改变。但是,不可变对象(如字符串、元组)默认实现了该方法。
  2. __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__ 方法的常见场景:

  1. 延迟加载:可以将某些属性的计算或获取延迟到实际访问时再执行,从而提高性能和效率。在 __getattr__ 方法中,你可以根据属性的名称动态地加载和返回属性值。
  2. 动态属性:根据不同的属性名称,返回不同的属性值。你可以根据需要在 __getattr__ 方法中编写逻辑,根据属性名称返回相应的值。
  3. 代理操作:当对象本身无法满足某个属性的需求时,可以使用 __getattr__ 方法将属性获取的操作委托给其他对象来处理。这样可以实现属性的动态转发。
  4. 错误处理:当访问不存在的属性时,可以通过 __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__ 方法而没有调用父类的方法,将会导致一些重要的功能失效,例如:

  1. 阻止无限递归调用:在 __setattr__ 方法中直接使用 self.__setattr__(name, value) 进行属性赋值,会导致无限递归调用自身的 __setattr__ 方法。通过调用父类的 __setattr__ 方法,可以避免无限递归的问题。
  2. 保留属性字典的默认行为:父类的 __setattr__ 方法负责维护对象的属性字典,将属性名和属性值保存在 self.__dict__ 中。如果不调用父类的方法,属性赋值将不会被正确地保存在 self.__dict__ 中,可能导致对象无法正常工作。
  3. 其他继承的功能:通过调用父类的 __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 对象的属性字典包含了 nameage 两个键值对。当我们为 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)))

你可能感兴趣的:(python笔记,python,开发语言)