70 面向对象常用魔法方法(内置方法)

164次阅读
没有评论

共计 21033 个字符,预计需要花费 53 分钟才能阅读完成。

引入

众所周知,方法是需要调用执行的,而魔法方法则不一样,他无需你的调用,在特定的时候会自己执行, 例如我们之前所学的__init__, 在示例对象 ([类名]+()) 的时候触发执行它

1. 什么是内置方法

  • 定义在类的内部, 以双下滑线开头 __, 以双下滑线__ 结尾的方法
  • 特点 : 在某种情况下自动触发执行

2. 为什么使用内置方法

  • 为了 高度定制化 我们使用的类或者实例

一. 点拦截方法__getattr__,__setattr__,__delattr__

  • __setattr__ : 在 [对象].[属性] = [值] 设置属性值 的时候就会触发它的执行
  • __getattr__ : 在 [对象].[属性] 获取属性不存在 的时候会触发它的执行
  • __delattr__ : 在 del [对象].[属性] 删除属性 的时候会触发它的执行
class Panda:
    def __init__(self, name):
        self.name = name

    def __getattr__(self, item):
        print('通过点的方式找属性没有找到, 于是触发了我')

    def __setattr__(self, key, value):
        print('你通过点的方式修改属性值, 触发了我')
        # 📛self.key=value
        # 📛如此设置值得时候(又通过点的方式设置), 于是又触发__setattr__的执行, 又开始设置, 无限递归了
        self.__dict__[key]=value # 可以通过操作对象字典 key = value 方式设置

    def __delattr__(self, item):
        print('通过点的方式删除属性, 触发了我')
        # 📛del self.item  # 与上面一样, 无限递归了
        self.__dict__.pop(item)  # 同样通过操作对象字典的方式去删属性

# 🔰实例对象的初始化方法其实也进行了设置属性值的操作
P = Panda("hangd")  # 你通过点的方式修改属性值, 触发了我

# 🔰获取一个不存在的属性
P.age               # 通过点的方式找属性没有找到, 于是触发了我

# 🔰设置属性值
P.age = 99          # 你通过点的方式修改属性值, 触发了我
print(P.__dict__)   # {'name': 'hangd', 'age': 99}

# 🔰删除属性
del P.name          # 通过点的方式删除属性, 触发了我
print(P.__dict__)   # {'age': 99}
  • 应用小示例 : 重写字典, 实现字典可以通过点 . 取值赋值, 也可以 [] 取值赋值
class MyDict(dict):
    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, item):
        return self[item]

DD = MyDict()
print(DD)  # {}

# 🔰点取值赋值
DD.name = "shawn"
print(DD.name)    # shawn
print(DD)         # {'name': 'shawn'}

# 🔰[] 取值赋值
DD["age"] = 20
print(DD["age"])  # 20
print(DD)         # {'name': 'shawn', 'age': 20}

二. __getattribute__

1. 先看看 : __getattr__

  • 点 . 属性 没找到触发
class Bar:

    def __getattr__(self, item):
        print(" 没找到, 触发了我 ")

bb = Bar()
bb.name    # 没找到, 触发了我

2.__getattribute__

  • 点 . 属性 无论找没找到都触发
class Bar:
    def __init__(self,name):
        self.name = name

    def __getattribute__(self, item):
        print(f" 无论找没找到, 都触发了我 -->{item}")

bb = Bar("shawn")
bb.name  # 无论找没找到, 都触发了我 -->name
bb.age   # 无论找没找到, 都触发了我 -->age

3. 两者同时存在

🍔两者同时存在
class Bar:
    def __init__(self,name):
        self.name = name

    def __getattr__(self, item):
        print(" 没找到, 触发了我 ")

    def __getattribute__(self, item):
        print(f" 无论找没找到, 都触发了我 -->{item}")

bb = Bar("shawn")
bb.name  # 无论找没找到, 都触发了我 -->name
bb.age   # 无论找没找到, 都触发了我 -->age

🍔设置异常
class Bar:
    def __init__(self,name):
        self.name = name

    def __getattr__(self, item):
        print(" 没找到, 触发了我 ")

    def __getattribute__(self, item):
        print(f" 无论找没找到, 都触发了我 -->{item}")
        raise AttributeError('让小弟接管')  # 设置异常, 直接交给__getattr__

bb = Bar("shawn")
bb.name
'''
无论找没找到, 都触发了我 -->name
没找到, 触发了我
'''bb.age'''
无论找没找到, 都触发了我 -->age
没找到, 触发了我
'''
  • [对象] . [属性] 的调用顺序 : 先执行 __getattribute__--->去类的名称空间找--->__getattr__(本质是去对象自己的名称空间找)
  • [对象] . [属性] 的查找顺序 : 对象自己 --->--->父类 ---> 父类

4. 总结

  • __getattribute__方法优先级比 __getattr__
  • 没有重写 __getattribute__ 的情况下, 默认使用的是父类的 __getattribute__ 方法
  • 只有在使用默认 __getattribute__ 方法中找不到对应的属性时,才会调用__getattr__
  • 如果是对不存在的属性做处理,尽量把逻辑写在 __getattr__ 方法中
  • 如果非得重写 __getattribute__ 方法,需要注意两点:
  • 第一是避免. 操作带来的死循环
  • 第二是不要遗忘父类的 __getattribute__ 方法在子类中起的作用

三.item 系列 __getitem__,__setitem__,__delitem__

  • __getitem__ : 通过中括号 取值, 触发它的执行

  • __setitem__ : 通过中括号 赋值, 触发它的执行

  • __delitem__ : 通过中括号 删值, 触发它的执行

class Person:
    def __getitem__(self, item):
        print('取值操作, 触发了执行')

    def __setitem__(self, key, value):
        print('赋值操作, 触发了执行')

    def __delitem__(self, key):
        print('删值操作, 触发了执行')

p=Person()
p['name'] = " 派小星 "  # 赋值操作, 触发了执行
p['name']            # 取值操作, 触发了执行
del p["name"]        # 删值操作, 触发了执行
  • 小示例 : 在不继承字典类的情况下, 自定义一个字典, 支持 [] 取值赋值
class Mydict:
    def __init__(self,**kwargs):
        # 🔰方案一
        # for key,value in kwargs.items():
        #     setattr(self,key,value)

        # 🔰方案三
        # for key,value in kwargs.items():
        #     self.__dict__[key]=value

        # 🔰方案二
        self.__dict__.update(kwargs)

    def __getitem__(self, item):
        # 🔰方案一
        # return self.__dict__[item]

        # 🔰方案二
        return getattr(self,item)

    def __setitem__(self, key, value):
        # 🔰方案一
        # setattr(self,key,value)

        # 🔰方案二
        self.__dict__[key]=value

    def __delitem__(self, key):
        del self.__dict__[key]

dd = Mydict()
print(dd.__dict__)    # {}

dd["name"] = "shawn"
print(dd.__dict__)    # shawn

print(dd["name"])     # shawn

四. __format__

自定义格式化字符串

1.format() 函数

前面 字符串类型的内置方法 已经详细的介绍了 format() 函数的玩法, 下面简单回顾一下

🍉直接传变量名
res="my name is {name} my age is {age}".format(age=18,name="shawn")
print(res)  #my name is shawn my age is 18

🍉不放任何值, 让其按位置自动传值
res="my name is {} my age is {}".format(18,"shawn")
print(res)  #my name is 18 my age is shawn

🍉通过索引传值
res="my name is {0}{0}{0} my age is {1}{0}".format(18,"shawn")
print(res)  #my name is 181818 my age is shawn18

2.__format__ 方法

  • 其实我们使用format() 函数的时候触发的就是 __format__ 方法
class For:
    def __format__(self, format_spec):
        print(type(format_spec))  # 看看这个参数是什么类型
        print(f" 使用 format()函数, 触发了我 -->({format_spec})")
        return "aaa"   # 必须返回一个字符串(不然报错), 可以是任意的字符串(后面可以使用此特性来操作自定义)

F = For()

🔰当第二个参数不传入时默认为空
format(F)
''' 输出
<class 'str'>
使用 format()函数, 触发了我 -->()
'''
🔰传参类型 "str"
format(F," 你好 ")
''' 输出
<class 'str'>
使用 format()函数, 触发了我 -->(你好)

3. 小示例 : 制作一个输出日期类, 实例的时候传入年, 月, 日, 可以自定义格式

class Date:
    __format_dic = {"1": "{obj.year}-{obj.mon}-{obj.day}",
        "2": "{obj.year}:{obj.mon}:{obj.day}",
        "3": "{obj.year}/{obj.mon}/{obj.day}"
    }
    def __init__(self,Year,Mon,Day):
        self.year = Year
        self.mon = Mon
        self.day = Day

    def __format__(self, format_spec):
        if not format_spec or not format_spec in self.__format_dic:  # 如果格式编号为空或者不在自定义字典里面
            f = self.__format_dic["1"]  # 那我们就给一个默认的格式
        else:
            f = self.__format_dic[format_spec]  # 否则就使用你传入的格式
        return f.format(obj=self)  # 使用 "obj=self" 赋值的方法避免递归调用

mm = Date(2020,12,28)

print(format(mm,"1"))     # 2020-12-28
print(format(mm,"2"))     # 2020:12:28
print(format(mm,"3"))     # 2020/12/28
print(format(mm,))        # 2020-12-28 (空, 选择默认)
print(format(mm,"3iop"))  # 2020-12-28 (不存在, 选择默认)

五. 析构方法 __del__

前面我们学了 __init__ 这个构造方法, 在实例对象的时候自动触发, 也叫初始化, 而析构方式是在执行 del对象的时候, 也就是在 对象被清理之前 自动触发 __del__ 的执行, 那么我们就可以在这方法里面进行一些操作

class Test:
    def __del__(self):
        print(" 删除对象, 触发了我 ")

T = Test()
del T  # 删除对象, 触发了我

1.__del__的使用场景

  • 一般用来进行回收系统资源的操作
  • 如果一个对象仅仅占用应用程序 (用户级) 的资源时, 删除对象时, 垃圾回收机制会自动回收该对象所占用的应用程序资源, 即用户态内存
class A:
    def __del__(self):
        print("--- 触发 del---")

a = A()
del a
print("--- 程序结尾 ---")
''' 输出
--- 触发 del---
--- 程序结尾 ---
'''
  • 当一个程序运行完毕时, 也会自动触发 GC, 进而 GC 触发 __del__
class A:
    def __del__(self):
        print("--- 触发 del---")

a = A()

print("--- 程序结尾 ---")
''' 输出
--- 程序结尾 ---
--- 触发 del---
'''
  • 当对象涉及到申请了操作系统的资源, 比如 open 打开了文件, 或者与网络连接等, GC 无法派上用场, 那么就需要我们重写一下 __del__ 的功能
🔰重写示例 : 对象中打开了一个文件
class Open:
    def __init__(self):
        self.f = open("test26.py","rt",encoding="utf-8")  # 打开了一个文件

    def __del__(self):
        self.f.close()  # 触发执行的时候, 关闭文件(系统资源的回收)
        print(" 删除对象之前我关闭了文件, 哈哈 ")

O = Open()
del O      # 删除对象之前我关闭了文件, 哈哈

六. 注释文档 __doc__

  • 返回类的注释信息
class Test:
    '''
    这是用来测试__doc__的注释信息
    信息信息
    '''
    ...

T = Test()
print(T.__doc__)
''' 输出
这是用来测试__doc__的注释信息
信息信息
'''
  • 该属性无法被继承
class Foo:
    '我是描述信息'
    pass

class Bar(Foo):
    pass

F = Foo()
print(F.__doc__)  # 我是描述信息

B = Bar()
print(B.__doc__)  # None

七. 描述符 __get__, __set__, __delete__

1. 什么是描述符

  • 描述符的本质就是一个新式类, 在这个新式类中至少实现了 __get__(), __set__(), __delete__() 中的一个就称为 描述符 , 也被称为 描述符协议

  • __get__(self,inatance,owener) : 当访问一个属性的时候触发

  • __set__(self,instance,value) : 为一个属性赋值时触发
  • __delete__(self,instance) : 使用 del 删除一个属性的时候触发
self 描述符的对象
instance 使用描述符的对象
owner 设置了描述符的类(也就是 instance 的类)
value instance 的值
  • 定义一个描述符
class MyDecriptor:
    def __get__(self, instance, owner):
        print('触发 get')

    def __set__(self, instance, value):
        print('触发 set')

    def __delete__(self, instance):
        print('触发 delete')

🔰以下方法是描述符的实例进行的操作, 并不会触发上面三种方法, 不要搞混了
f1=MyDecriptor()
f1.name='shawn'
print(f1.name)
del f1.name
  • 设置简单代理, 定义成另一个类的类属性
🌙描述符一
class Str:
    def __get__(self, instance, owner):
        print('---> 触发 Str_get')

    def __set__(self, instance, value):
        print('---> 触发 Str_set')

    def __delete__(self, instance):
        print('---> 触发 Str_Delete')

🌙描述符二
class Int:
    def __get__(self, instance, owner):
        print('---> 触发 Int_get')

    def __set__(self, instance, value):
        print('---> 触发 Int_set')

    def __delete__(self, instance):
        print('---> 触发 Int_Delete')

🌙普通类
class Person:
    name = Str()  # 将 name 给 Str 代理 (也可以说 name 是描述符对象)
    age = Int()   # 将 age 给 Int 代理  (也可以说 age 是描述符对象)

    def __init__(self, name, age):
        self.name = name  # 这里赋值就会触发描述符__set__方法
        self.age = age

⭐实例对象
P1 = Person(" 派大星 ",22)
# ---> 触发 Str_set
# ---> 触发 Int_set

⭐查找属性
print(P1.name)
# ---> 触发 Str_get
# None

⭐查找属性
print(P1.age)
# ---> 触发 Int_get
# None

⭐设置属性
P1.name = " 海绵宝宝 "  
# ---> 触发 Str_set

⭐查找属性
print(P1.name)
# ---> 触发 Str_get
# None
  • 查看类与对象属性字典
print(P1.__dict__)    # {} 为空

print(Person.__dict__)
'''从中取出了两个(name,age)'name': <__main__.Str object at 0x00000267479E7AC8>,'age': <__main__.Int object at 0x00000267479E7B08>'''
  • 小结
  • 只要类属性被描述符类代理了, 以后使用 [对象] . [属性], 就会触发描述符类的__set__, __get__, __delete__方法
  • 并且被代理的属性只在类名称空间有, 对象名称空间就没有该属性了

2. 描述符是做什么的

描述符的作用是用来代理另外一个类的属性的, 并且必须把描述符定义成这个类的类属性,不能定义到构造函数中

class Str:
    def __get__(self):
        pass

class Duck:
    name = Str()           # 正确 : name 被 Str 代理
    def __init__(self):
        self.name = Str()  # 错误 : 只能代理类属性, 不能代理对象属性
  • 代理属性小 示例 : 代理属性, 并限制传入数据的类型
🌙数据描述符
class Str:

    def __init__(self, agencyName, agencyType):
        '''
        :param agencyName: 被代理的属性名字
        :param agencyType: 被代理的属性类型
        '''
        self.name = agencyName
        self.type = agencyType
        # 将属性存在描述符的 __dict__ 里面, 也可也直接放到 "P1" 对象属性字典中去, 看自己需求
        self.date_dict = self.__dict__

    def __get__(self, instance, owner):
        return self.date_dict[instance]

    def __set__(self, instance, value):
        # 在这里可以进行属性类型的判断
        if isinstance(value, self.type):
            self.date_dict[instance] = value
        else:
            print(" 类型不正确 ")

    def __delete__(self, instance):
        del self.date_dict[instance]

🌙普通类
class Panda:
    name = Str("name", str) # 将描述符实例当做属性保存
    age = Str("age", int)

    def __init__(self, name, age):
        self.name = name
        self.age = age

P1 = Panda(" 派大星 ", 18)
print(P1.__dict__)  # {} 空的, 因为全被代理了

print(P1.name)      # 派大星
print(P1.age)       # 18

P1.name = " 海绵宝宝 "
P1.age = 22

print(P1.name)      # 海绵宝宝
print(P1.age)       # 22
print(P1.__dict__)  # {}

# 类型错误时
P1.name = 2222      # 类型不正确
P1.age = "3333"     # 类型不正确
print(Panda.__dict__["name"].__dict__)
# {'name': 'name', 'type': <class 'str'>, 'date_dict': {...}, <__main__.Panda object at 0x000001DBE96F7A88>: '海绵宝宝'}

由上面示例我们可以知道, 一个类中被代理的属性将会保存在代理类 (也就是描述符) 的属性字典中, 而描述符对象又被当做属性保存在类的属性字典中

  • 将进行过类型判断的属性放入到 "P1" 对象属性字典中, 方法一样, 只需要将 "self" 改成 "instance"
class Str:
    def __init__(self,agencyName, agencyType):
        self.name = agencyName
        self.type = vagencyType

    def __get__(self, instance, owner):
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value,self.type):
            print(" 类型不符合 ")
            return
        else:
            instance.__dict__[self.name] = value
            return

    def __delete__(self, instance):
        instance.__dict__.pop(self.name)

3. 描述符的种类与属性查找优先级

  • 通常的只要定义了 __get__ 和 另一个或两个方法的类, 我们就称之为 数据描述符
  • 如果一个类只定义了 __get__ 方法 (没有修改数据的功能) , 我们称之为 非数据描述符

  • 属性查找优先级 :

  • 无论有没有都会先触发 __getattribute__
  • 类属性
  • 数据描述符 (如果数据描述符中重写了 __getattribute__ 可能会导致无法调用描述符)
  • 实例属性 (对象属性)
  • 非数据描述符
  • 找不到触发 __getattr__() 的执行

  • 示例 : 类属性 > 数据描述符

🌙数据描述符
class Str:
    def __get__(self, instance, owner):
        print('---> 触发 Str_get')

    def __set__(self, instance, value):
        print('---> 触发 Str_set')

    def __delete__(self, instance):
        print('---> 触发 Str_Delete')

🌙普通类
class Person:
    name = Str()

    def __init__(self,name):
        self.name = name
        Person.name = " 派大星 "  # 类的属性

p = Person("name")  # ---> 触发 Str_set
print(p.name)       # 派大星
🔰由上面的实验可知, 最先找的是类属性
  • 示例 : 数据描述符 > 实例属性
🌙数据描述符
class Str:
    def __get__(self, instance, owner):
        print('---> 触发 Str_get')

    def __set__(self, instance, value):
        print('---> 触发 Str_set')

    def __delete__(self, instance):
        print('---> 触发 Str_Delete')

🌙普通类
class Person:
    name = Str()  # name 是描述符对象

    def __init__(self,name,age):
        self.name = name
        self.age = age

P1 = Person(" 海绵哥哥 ",99)  # ---> 触发 Str_set

P1.name = " 海绵爸爸 "        # ---> 触发 Str_set
P1.name                   # ---> 触发 Str_get
del P1.name               # ---> 触发 Str_Delete
print(P1.__dict__)        # {'age': 99} (查看对象属性字典, 没有 name 属性)
print(Person.__dict__)    # ['name'] (查看类属性字典, 存在 name, 这个 name 就是描述符对象)
  • 示例 : 示例属性 > 非数据描述符
🔰以下比较并没有实际意义, 只是便于对描述符的理解
🌙非数据描述符
class Str:
    def __get__(self, instance, owner):
        print('---> 触发 Str_get')

    def __delete__(self, instance):
        print('---> 触发 Str_Delete')

🌙普通类
class Person:
    name = Str()  # name 是描述符对象

    def __init__(self,name,age):
        self.name = name
        self.age = age

P1 = Person(" 海绵妈妈 ",88)  # 抛出异常 : "AttributeError" 属性中没有__set__方法

八. __call__

  • 触发条件 : [对象] + () 就触发 __call__ 的执行
class Person:
    def __init__(self,name):
        self.name = name

    def __call__(self, *args, **kwargs):
        print(args)
        print(kwargs)
        print(self.name + " 触发了__call__")

P1 = Person(" 派大星 ")
P1()  
# ()
# {}
# 派大星触发了__call__

P1(1,2,3,4,name="shawn")
# (1, 2, 3, 4)
# {'name': 'shawn'}
# 派大星触发了__call__

一切皆对象, Person 类也是一个对象, Persion+() 触发的是生成 Persion 类的类里的__call__

而 Persion 类生成的对象 +() 触发的才是 Persion 类的__call__

九. __init__ 和 __new__

1. 先看代码 :

class Person:
    def __init__(self,name):
        print(" 触发__init__")
        self.name = name

    def __new__(cls, *args, **kwargs):
        print(" 触发__new__")
        # obj = object.__new__(cls)  # 这个是直接使用 object 实例出来的对象
        obj = super(Person,cls).__new__(cls)  # 这个是通过当前类的父类实例出来的对象
        return obj

p = Person("shawn")
# 触发__new__
# 触发__init__

print(p.name)  # shawn
  • 在一个类中如果没有重写 __new__() 方法, 那么就会使用父类中的 __new__() 方法, 如果父类中没有, 则会一直追溯到 object 中的 __new__() 方法

  • 如果在类中重写了 __new__() 方法, 我们可以通过 return 出一个实例, 该实例可以直接是 object 实例出来的对象, 也可以是当前类的父类实例出来的对象 (看上面代码)

  • return 当前类的对象就会自动触发 __init__() 方法, 该实例是通过 cls 参数来控制是当前类的实例, 如果是别的类的实例或者其他任意东西则不会触发 __init__() 的执行

  • 无法触发__init__ 示例 :
🐞当 "return" 的不是当前类的实例, 是其它的一些任意数据
class Persion:
    def __init__(self,name):
        print(" 触发__init__")
        self.nam = name

    def __new__(cls, *args, **kwargs):
        print(" 触发__new__")
        obj = super(Person,cls).__new__(cls)
        return 1111

p = Person("shawn")  # 触发__new__ (没有触发__init__的执行)
print(p.name)  # 抛出异常 : "AttributeError" 没有这个属性

🐞当 "return" 的不是当前类的实例, 是其它类的实例(这里我们让 Person 继承 Str, 然后 return 出 Str 的实例)
class Str:
    '''Str_obj'''

class Person(Str):
    def __init__(self,name):
        print(" 触发__init__")
        self.name = name

    def __new__(cls, *args, **kwargs):
        print(" 触发__new__")
        obj = super(Str,Str).__new__(Str)
        print(obj.__doc__)   # Str_obj
        return obj

p = Person("shawn")  # 触发__new__  (同样只是触发__new__, 并没有触发__init__方法)
print(p.name)  # 同样抛出异常 : "AttributeError" 没有这个属性

2. 使用方法比较

  • __new__() 用于 创建实例 ,所以该方法是在 实例创建之前 被调用,它是 类级别 的方法,是个静态方法
  • __init__() 用于 初始化实例 ,所以该方法是在 实例对象创建后 被调用,它是 实例级别 的方法,用于设置对象属性的一些初始值

  • 由此可知,__new__()__init__() 之前被调用。如果 __new__() 创建的是当前类的实例,会自动调用 __init__() 函数

3. 传入参数比较

  • __new__() 至少有一个参数 cls, 代表当前类, 此参数在实例化的时候由 Python 解释器自动识别
  • __init__() 至少有一个参数 self, 就是 __new__() 返回的实例, __init__()__new__() 的基础上完成一些初始化的操作

4. 返回值的比较

  • __new__()必须要有返回值, 返回实例对象
  • __init__()不需要返回值

十. __str__ 和 __repr__

1.__str__

  • 触发条件 : print([对象]) 的时候触发 __str__ 的执行
class Person:
    def __init__(self,name):
        self.name = name

    def __str__(self):
        print(" 打印对象时触发了__str__")
        return f" 对象名字 : {self.name}"  # 🔰返回值必须是一个字符串, 否则报错

P1 = Person("shawn")
P2 = Person(" 乔治 ")

print(P1)
# 打印对象时触发了__str__
# 对象名字 : shawn

print(P2)
# 打印对象时触发了__str__
# 对象名字 : 乔治

print(P2.name)  # 乔治
  • 返回一个看起来字典格式的字符串示例
🔰示例一:
class Person:
    def __init__(self,name,age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.__dict__}"

P1 = Person(" 木之本樱 ",88)
print(P1)  # {'name': '木之本樱', 'age': 88}

🔰示例二:
import json

class Person:
    def __init__(self,name,age):
        self.name = name
        self.age = age

    def __str__(self):
        return json.dumps(self.__dict__)

P1 = Person("shawn",22)
print(P1)  # {"name": "shawn", "age": 22}

2.__repr__

  • 触发条件 : 在命令窗口中直接写 [对象] 时触发 __repr__ 的执行
>>> class Person:
...     def __init__(self, name):
...             self.name = name
...     def __repr__(self):
...             print(" 命令窗口直接写对象触发了__repr__")
...             return f" 名字 : {self.name}"  # 🔰返回值也必须是一个字符串, 否则报错
...
>>> P1 = Person(" 佩奇 ")
>>> P1
命令窗口直接写对象触发了__repr__
名字 : 佩奇
>>> P1.name
'佩奇'

十一. __module__ 和 __class__

1.__module__

  • 可以查看当前操作的对象在哪个模块
🐞文件 "test.py" 内容
class Foo:
    def __init__(self,name):
        self.name = name
🐞当前执行文件内容
from test import Foo

obj = Foo("shawn")
print(obj.__module__)  # test  (test 来自模块)

class Bar:
    def __init__(self,name):
        self.name = name

B = Bar(" 野猪 ")
print(B.__module__)  # __main__ (属于当前文件)

2.__class__

  • 可以查看当前操作的对象所属的类
from test import Foo

obj = Foo("shawn")

print(obj.__class__)  # <class 'test.Foo'>
print(obj.__class__.__name__)  # Foo

class Bar:
    def __init__(self,name):
        self.name = name

B = Bar(" 野猪 ")
print(B.__class__)  # <class '__main__.Bar'>
print(B.__class__.__name__)  # Bar

十二. __slots__ 和 __dict__

1.__slots__

默认我们可以给 class 实例绑定任何属性和方法,这就是动态语言的灵活性如果我们想要限制 class 的属性怎么办?比如,只允许对 Persion 实例添加 nameage属性。为了达到限制的目的,Python允许在定义 class 的时候,定义一个特殊的__slots__变量,来限制该 class 能添加的属性

  • slots是一个类变量,变量值可以是列表,元祖,或者可迭代对象,也可以是一个字符串(意味着所有实例只有一个数据属性)
class Str:
    # __slots__ = "name"
    # __slots__ = ["name","age"]
    __slots__ = ("name","age")  # 对象只拥有 name,age 属性

    def __init__(self,name,age):
        self.name = name
        self.age = age
        # self.sex = sex

p1 = Str("shawn",22)

print(p1.__slots__)      # ('name', 'age')
print(p1.name,p1.age)    # shawn 22

p1.friend = " 水冰月 "  # 报错 : "AttributeError" (friend 没有放到 slots 中)

一旦在类中使用了 slots 属性, 实例出来的对象将不会有自己的 dict, 所有的对象, 都共享使用类名称空间中的属性, 在此之前每生成一个对象, 都会申请一份内存空间, 对象越多, 占用的空间就越多, 使用了 slots 后, 对象越多, 越节省内存

  • 共享类的名称空间属性示例
class Str:
    __slots__ = ["name","age"]

    def __init__(self,name,age):
        self.name = name
        self.age = age

p1 = Str(" 大雄 ",22)
p2 = Str(" 静香 ",90)

print(p1.name,p1.age)  # 大雄 22
print(p2.name,p2.age)  # 静香 90

print(p1.__slots__)    # ['name', 'age']
print(p2.__slots__)    # ['name', 'age']

print(p1.__dict__)     # 报错 : "AttributeError" 没有该属性
print(p1.__dict__)     # 报错 : "AttributeError" 没有该属性

print(Str.__dict__)
''' 输出
{'__module__': '__main__', '__slots__': ['name', 'age'], '__init__': <function Str.__init__ at 0x0000025344BEC828>, \
'age': <member 'age' of 'Str' objects>, 'name': <member 'name' of 'Str' objects>, '__doc__': None}
'''
  • 当改变类的 name 和 age 时
Str.name = " 小玉 "
Str.age = 77

print(p1.name,p1.age)  # 小玉 77
print(p2.name,p2.age)  # 小玉 77
# 🔰发现所有对象的属性都发生了改变

总结 : 第一 : slots 属性是类属性, 可以用来限制实例对象能拥有的属性, 并且不再有自己的名称空间

第二 : 对象属性字典会占用大量内存,如果你有一个属性很少的类,但是有很多实例,为了节省内存可以使用 slots 取代实例的 dict

2.__dict__

  • dict 在前面的许多地方都使用到了 dict 这个属性, 它用来查看一个对象或类的名称空间属性, 也可以说属性字典
class Person:
    def __init__(self,name,age):
        self.name = name
        self.age = age

P1 = Person(" 夏尔凡多姆海恩 ",1250)

print(P1.__dict__)  
# {'name': '夏尔凡多姆海恩', 'age': 1250}

print(Person.__dict__)
'''{'__module__':'__main__','__init__': <function Person.__init__ at 0x0000021CDDDAC4C8>,'__dict__': <attribute'__dict__'of'Person'objects>,\'__weakref__': <attribute'__weakref__'of'Person'objects>,'__doc__': None}'''

十三. __all__

前面模块介绍章节其实已经介绍过了 __all__ 的使用, 它用来在使用 from [模块名] import * 的时候, 针对这个 * 来限制能够使用的属性

🐞文件 "test.py" 内容
__all__ = ["Foo","Bar"]  # 如此只能使用 Foo,Bar 两个属性

class Foo:
    def __init__(self):
        print("Foo")

class Bar:
    def __init__(self):
        print("Bar")

class Abc:
    def __init__(self):
        print("Abc")
🐞当前执行文件内容
from test import * 

f = Foo()  # Foo
b = Bar()  # Bar
A = Abc()  # 报错 : "NameError"  这个名字没有找到

十四. __iter__ 和 __next__

前面 迭代器 那一章节我们讲解了可迭代对象和迭代器对象的特点以及 For 循环的原理 :

  • 可迭代对象 (iterable) : 在 python 中,但凡内置有 __iter__方法的对象,都是可迭代的对象
  • 迭代器对象 (iterator) : 内置有 __next____iter__方法的对象, 就是迭代器对象, 可迭代对象执行 __iter__ 得到的返回值就是迭代器对象
  • For 循环原理请到上面链接查看 (重点)

接下来我们来给一个类重写 __iter__ 以及 __next__ 方法, 让其可以迭代, 也可以被 for 循环

class Foo:
    def __init__(self,start,end):
        self.__start = start-1
        self.__end=end

    def __iter__(self):
        return self

    def __next__(self):
        self.__start+=1
        if self.__start>=self.__end:
            raise StopIteration('超出值')  # 抛出一个异常,For 循环会检测异常自动结束循环
        return self.__start

for i in Foo(1,10):
    print(i)  # 1 2 3 4 5 6 7 8 9 

在函数学习的那一章节, 我们通过函数实现了 MyRange, 现在我们试试使用类来实现

class MyRange:
    def __init__(self,start,stop,step=1):
        self.__start = start-1
        self.__stop = stop
        self.__step = step

    def __iter__(self):
        return self

    def __next__(self):
        self.__start += self.__step
        if self.__start >= self.__stop:
            raise StopIteration(" 超出值 ")  # 抛出异常
        return self.__start

for i in MyRange(0,10,2):
    print(i)  # 1 3 5 7 9

应用小示例 : 产生一个小于某值的斐波那契数列

class Fib:
    def __init__(self,count):
        self.__start = 0
        self.__stop = 1
        self.__count = count

    def __iter__(self):
        return self

    def __next__(self):
        self.__start,self.__stop = self.__stop,self.__start+self.__stop
        if self.__start >= self.__count:
            raise StopIteration(" 超出 ")  # 抛出异常
        return self.__start

for i in Fib(50):
    print(i)  # 1 1 2 3 5 8 13 21 34 

补充链式调用知识

  • 链式调用是设计程序的模式, 在 Java 和 js 中被广泛应用
  • 链式调用格式 : A. 方法. 方法. 方法. 方法
  • 优势和好处 : 代码量大大减少, 逻辑集中清晰明了, 且易于查看和修改
class Person:
    def __init__(self,name,age,sex):
        self.name= name
        self.age = age
        self.sex = sex

    def print_name(self):
        print(self.name)
        return self  # 返回对象本身

    def print_age(self):
        print(self.age)
        return self  # 返回对象本身

    def print_sex(self):
        print(self.sex)
        return self  # 返回对象本身

P1 = Person(" 市丸银 ",45,"man")
P1.print_name()  # 市丸银
P1.print_age()   # 45
P1.print_sex()   # man

P1.print_name().print_age().print_sex()  # 市丸银 45 man

十五. __len__

计算对象的长度, return 是多少, len([对象])的结果就是多少

  • 触发条件 : 使用 len([对象]) 时触发, 返回值必须为整数
🐞"return" 多少就是多少
class Person:
    def __len__(self):
        return 11111  # 只能返回整数, 不然报错 : "TypeError" 类型错误

p = Person()
print(len(p))  # 11111

🐞
class Person:
    def __init__(self,name,age):
        self.name = name
        self.age = age

    def __len__(self):
        return len(self.__dict__)

p = Person(" 我爱罗 ",5)

print(len(p))  # 2
p.sex = "man"
print(len(p))  # 3

十六. __hash__

  • 触发条件 : 使用 hash([对象]) 函数的时候触发, 里面返回什么值就是什么值, 返回值必须是整数
class Str:
    def __hash__(self):
        return 1231  # 只能返回整数, 不然报错 : "TypeError" 类型错

p = Str()
print(hash(p))  # 1231
  • 可变类型 -----> 不可 hash 类型
  • 不可变类型 -----> 可 hash 类型
print(hash(132))      # 132
print(hash(1.1))      # 230584300921369601
print(hash((4,5)))    # 3713084879518070856
print(hash([11,23,4,4]))  # 报错
print(hash({"age":12}))   # 报错
  • 如果一个对象不可hash, 我们可以通过重写__hash__ 方法让它变得可hash
class Foo:
    def __init__(self):
        self.name= " 黑子 "
        self.l=[1,2,3,]
    def __hash__(self):
        return hash(self.name)

f=Foo()
print(hash(f))  # -3176404254760418957

十七. __eq__

  • 触发条件 : 在两个对象进行 == 比较值的时候触发 __eq__() 的执行, 在该方法内部可以自定义比较规则
print(12 == [1,2,3])    # False
print("w" == (2,3,5,))  # False
l1 = [1,2,3]
l2 = [1,2,3]
print(l1 == l2)         # True (只比较值, 不比较 ID)
  • 自定义规则示例
class Str:
    def __init__(self,name):
        self.name = name

    def __eq__(self, other):  # self(本身对象),other(另一个对象)
        if self.name == other.name:  # 只要名字相同, 我就认为相同
            return True
        else:
            return False

p1 = Str(" 黑子 ")
p2 = Str(" 黑子 ")
p3 = Str(" 白子 ")

print(p1 == p2)  # True
print(p1 == p3)  # False

十八. 上下文管理协议 __enter__ 和 __exit__

1. 什么是上下文管理协议

上下文管理协议就是 with 语句, 为了让一个对象兼容 with 语句,必须在这个对象的类中声明 __enter__()__exit__()方法

  • with 对象, 触发对象的 __enter__ 的执行
  • with 同一级别写代码, 脱离了with, 就会执行 __exit__
class Open:
    def __enter__(self):
        print("----->enter 执行了 ")
        return "enter 的返回值 "

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("----->exit 执行了 ")

with Open() as f:  # 触发 __enter__
    print(f)
print("----> 结束 ")  # 触发 __exit__
'''
----->enter 执行了
enter 的返回值
----->exit 执行了
-----> 结束
'''

2.exit 的三个参数

  • exc_type:异常类型
  • exc_val:异常信息
  • exc_tb:追溯信息

with语句中代码块出现异常,则 with 后的代码都无法执行

class Open:
    def __enter__(self):
        print("----->enter 执行了 ")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(" 异常类型 --->",exc_type)
        print(" 异常信息 --->",exc_val)
        print(" 追溯信息 --->",exc_tb)
        print("----->exit 执行了 ")

with Open() as f:
    print(f)
    raise AttributeError(" 这里设置一个异常, 则下面代码不会再运行 ")

print("----> 结束 ")  # 这一行无法运行

'''
----->enter 执行了
<__main__.Open object at 0x0000022E31429D08>
异常类型 ---> <class 'AttributeError'>
异常信息 ---> 这里设置一个异常, 则下面代码不会再运行
追溯信息 ---> <traceback object at 0x0000022E3142F0C8>
----->exit 执行了
(抛出异常) : AttributeError: 这里设置一个异常, 则下面代码不会再运行
'''

3. 为什么要使用上下文管理器

  • 可以自动的操作(创建 / 获取 / 释放)资源,如文件操作、数据库连接
  • 无需在使用 try...execept... 去处理异常

如果 __exit__() 返回值为 True, 那么异常会被清空,就好像啥都没发生一样,with 后的语句正常执行

class Open:
    def __enter__(self):
        print("----->enter 执行了 ")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(" 异常类型 --->",exc_type)
        print(" 异常信息 --->",exc_val)
        print(" 追溯信息 --->",exc_tb)
        print("----->exit 执行了 ")
        return True

with Open() as f:
    print(f)
    raise AttributeError(" 这里设置一个异常, 但上面 exit 的 return 是 True, 所以该异常被忽略 ")

print("----> 结束 ")  # 这一行正常运行

'''
----->enter 执行了
<__main__.Open object at 0x000001D6CC399D08>
异常类型 ---> <class 'AttributeError'>
异常信息 ---> 这里设置一个异常, 则下面代码不会再运行
追溯信息 ---> <traceback object at 0x000001D6CC39F0C8>
----->exit 执行了
----> 结束
Process finished with exit code 0
'''

4. 自定义一个 open , 可以进行文件操作

class Open:
    def __init__(self,path,mode="rt",encoding="utf-8"):
        # 拿到一个 open 对象, 那么我们就可以借用 open 对象的方法, 当然你也可以重写方法 read(),write()等
        self.f = open(path,mode,encoding=encoding)  

    def __enter__(self):
        return self.f  # 返回 open 对象 (作用就是为了可以使用它的方法)

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:             # 如果不为空, 说明 with 上下文内部出错误了
            print(exc_val)       # 打印一下错误信息
            print(" 错误已被忽略 ") # 提示一下不会结束程序
            return True          # return True 就是为了忽略错误
        self.f.close()           # 当 with 内部没有错误, 正常结束时进行资源清理工作(关闭文件等)

with Open("test.py","rt",encoding="utf-8") as f:
    res = f.read()
    print(res)

5. 总结

使用 with 语句的目的就是把代码块放入 with 中执行,with结束后,自动完成清理工作,无须手动干预
在需要管理一些资源比如文件,网络连接和锁的编程环境中,可以在 exit 中定制自动释放资源的机制,你无须再去关注这个问题

正文完
 
shawn
版权声明:本站原创文章,由 shawn 2023-06-16发表,共计21033字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)