Python侧开28期-偕行-学习笔记-python魔法方法

一、 Python魔法方法介绍

1、什么是魔法函数

  • 所谓魔法函数(Magic Methods),是Python的一种高级语法,它是Python的内置函数,在相应的场景Python会自动去找到相应的魔法函数去处理相应的操作。可以重新Python内置的魔法函数,但是不可以自己定义Python中没有的魔法函数;
  • 特点:
    • Python中以双下划线(xx)开始和结束的函数,可以重写但不能自定义,如果不重写系统就会使用Python预先定义好的逻辑,如果重新就会实现我们自己需要的逻辑,就像构造方法__init__,如果不重写在实例化的时候也会去调用,但是是不接收任何参数的构造方法,如果想要接收参数那我们就需要去重写这个方法。
    • 调用类实例化的对象的方法时自动调用魔法函数。
    • 在自己定义的类中,可以实现之前的内置函数。

2、 魔法函数有什么作用?

魔法函数可以为我们写的类增加一些额外功能。举个简单的例子,我们定义一个“人”的类People,当中有属性姓名name、年龄age。当需要利用sorted函数对一个People的数组进行排序,排序规则是按照name和age同时排序,即name不同时比较name,相同时比较age。由于People类本身不具有比较功能,所以我们需要重新Python内置的比较函数去实现需要的比较逻辑,比如:

"""
我们定义一个“人”的类People,当中有属性姓名name、年龄age。当需要利用sorted函数对一个People的数组进行排序,排序规则是按照name和age同时排序,
即name不同时比较name,相同时比较age。由于People类本身不具有比较功能,所以我们需要重新Python内置的比较函数去实现需要的比较逻辑
"""
class People():
    # 构造魔法函数,当初始化对象的时候自动调用
    def __init__(self, name, age):
        self.name = name
        self.age = age
        return

    # 打印对象的时候会自动调用,使得打印的值具有一定的数据格式
    def __str__(self):
        return self.name + ":" + str(self.age)

    #进行小于比较的时候会自动调用,如果不重写这个方法,那么对象是不具备比较能力的
    # 这既让类具备了比较能力,也按照我们定义的比较规则进行比较
    def __lt__(self, other):
        return self.name < other.name if self.name != other.name else self.age < other.age


if __name__=="__main__":

    print("\t".join([str(item) for item in sorted([People("abc", 18),
        People("abe", 19), People("abe", 12), People("abc", 17)])]))
结果----------------------------------------------------------------------------------------------
abc:17	abc:18	abe:12	abe:19

二、Python常用魔法方法

1、 构造方法

我们最为熟知的基本的魔法方法就是 __init__ ,我们可以用它来指明一个对象初始化的行为。然而,当我们调用 x = SomeClass() 的时候, __init__ 并不是第一个被调用的方法。事实上,第一个被调用的是 __new__ ,这个 方法才真正地创建了实例。当这个对象的生命周期结束的时候, __del__ 会被调用。让我们近一步理解这三个方法:

  • __new__ (cls,[…])

    • __new__ 是对象实例化时第一个调用的方法,它只取下 cls 参数,并把其他参数传给 __init____new__ 很少使用,但是也有它适合的场景,尤其是当类继承自一个像元组或者字符串这样不经常改变的类型的时候。
  • __init__ (self,[…])

    • 类的初始化方法。它获取任何传给构造器的参数(比如我们调用 x = SomeClass(10, ‘foo’) , __init__ 就会接到参数 10 和 ‘foo’ 。__init__ 在Python的类定义中用的最多。
  • __del__ (self)

    • __new____init__ 是对象的构造器, __del__对象的销毁器。它并非实现了语句 del x (因此该语句不等同于 x.__del__ ())。而是定义了当对象被垃圾回收时的行为。当对象需要在销毁时做一些处理的时候这个方法很有用,比如 socket 对象、文件对象。但是需要注意的是,当Python解释器退出但对象仍然存活的时候, __del__ 并不会 执行。 所以养成一个手工清理的好习惯是很重要的,比如及时关闭数据库连接等。
from os.path import join

class FileObject:
    '''文件对象的装饰类,用来保证文件被删除时能够正确关闭。'''

    def __init__(self, filepath='~', filename='sample.txt'):
        # 使用读写模式打开filepath中的filename文件
        self.file = open(join(filepath, filename), 'r+')

    # 当程序运行结束进行垃圾回收的时候会自动调用这个方法
    def __del__(self):
        # 为了确保忘记关闭文件导致内存泄漏,在这里关闭文件并销毁对象
        print("__del__")
        self.file.close()
        del self.file

if __name__ == '__main__':
    file = FileObject(filepath="",filename='del.txt')
    """
    执行完之后会打印__del__
    """

2、 操作符

使用Python魔法方法的一个巨大优势就是可以构建一个拥有Python内置类型行为的对象。这意味着我们可以避免使用非标准的、丑陋的方式来表达简单的操作。在一些语言中,这样做很常见:

if instance.equals(other_instance):
    # do something

这样做让代码变得冗长而混乱。不同的类库可能对同一种比较操作采用不同的方法名称,这让使用者需要做很多没有必要的工作。运用魔法方法的魔力,我们可以定义方法 __eq__

if instance == other_instance:
    # do something  

这是魔法力量的一部分,这样我们就可以创建一个像内建类型那样的对象了!

(1) 比较操作符

Python包含了一系列的魔法方法,用于实现对象之间直接比较,而不需要采用方法调用。同样也可以重载Python默认的比较方法,改变它们的行为。

  • __cmp__(self, other)

    • __cmp__ 是所有比较魔法方法中最基础的一个,它实际上定义了所有比较操作符的行为(<,==,!=,等等),但是它可能不能按照你需要的方式工作(例如,判断一个实例和另一个实例是否相等采用一套标准,而与判断一个实例是否大于另一实例采用另一套)。__cmp__ 应该在 self < other 时返回一个负整数,在 self == other 时返回0,在 self > other 时返回正整数。最好只定义你所需要的比较形式,而不是一次定义全部。如果你需要实现所有的比较形式,而且它们的判断标准类似,那么 __cmp__ 是一个很好的方法,可以减少代码重复,让代码更简洁。
  • __eq__(self, other):定义等于操作符(==)的行为。

  • __ne__(self, other):定义不等于操作符(!=)的行为。

  • __lt__(self, other):定义小于操作符(<)的行为。

  • __gt__(self, other):定义大于操作符(>)的行为。

  • __le__(self, other):定义小于等于操作符(<)的行为。

  • __ge__(self, other):定义大于等于操作符(>)的行为。

class Word(str):
    '''单词类,按照单词长度来定义比较行为'''

    def __new__(cls, word):
        # 注意,我们只能使用 `__new__` ,因为str是不可变类型
        # 所以我们必须提前初始化它(在实例创建时)
        if ' ' in word:
            print("Value contains spaces. Truncating to first space.")
            word = word[:word.index(' ')]
            # Word现在包含第一个空格前的所有字母
        return str.__new__(cls, word)

    def __gt__(self, other):
        if len(self) > len(other):
            return f"{self}的长度>{other}"
    def __lt__(self, other):
        if len(self) < len(other):
            return f"{self}的长度<{other}"
    def __ge__(self, other):
        if len(self) >= len(other):
            return f"{self}的长度>={other}"
    def __le__(self, other):
        if len(self) <= len(other):
            return f"{self}的长度<={other}"
    def __eq__(self, other):
        if len(self) == len(other):
            return f"{self}的长度={other}"
    def __ne__(self, other):
        if len(self) != len(other):
            return f"{self}的长度!={other}"

if __name__ == '__main__':

    w1 = Word("hello")
    w2 = Word("world")
    w3 = Word("python")
    print(w3 > w1)
    print(w2 < w3)
    print(w1 >= w2)
    print(w1 <= w2)
    print(w1 == w2)
    print(w3 != w1)
    """
    输出:
    python的长度>hello
    world的长度<python
    hello的长度>=world
    hello的长度<=world
    hello的长度=world
    python的长度!=hello
    """

除了使用重写需要的比较方之外,还可以使用 @total_ordering装饰器来重写部分比较方法而实现所有的比较方法;

  • 被装饰的类必须重写__eq__以及其他方法(__ne__,__lt__,__ge__,__gt__,__le__)中的一个,@total_ordering会自动实现没有重写的其他比较功能;
from functools import total_ordering

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

@total_ordering
class StudentOrdering:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    # 重写相等比较方法
    def __eq__(self, other):
        return self.grade == other.grade

    # 重写小于比较方法
    def __lt__(self, other):
        return self.grade < other.grade

if __name__ == '__main__':
    # 非比较类进行比较直接报错
    student1 = Student("Alice", 85)
    student2 = Student("Bob", 75)
    print(student1 < student2)  # TypeError: '<' not supported between instances of 'Student' and 'Student'

    # 比较类
    student1 = StudentOrdering("Alice", 85)
    student2 = StudentOrdering("Bob", 75)
    student3 = StudentOrdering("Charlie", 85)
    print(student1 < student2)  # False
    print(student1 > student2)  # True
    print(student1 == student3)  # True
    print(student1 <= student3) # True
    print(student3 >= student2) # True
    print(student1 != student3) # False

(2) 数值操作符

就像我们可以使用比较操作符来比较类的实例,也可以定义数值操作符的行为,把数值操作符分为5类:

  • 一元操作符,
  • 常见算数操作符,
  • 反射算数操作符,
  • 增强赋值操作符,
  • 类型转换操作符,

一元操作符

一元操作符只有一个操作符。

  • __pos__(self):实现取正操作,例如 +some_object。

  • __neg__(self):实现取负操作,例如 -some_object。

  • __abs__(self):实现内建绝对值函数 abs() 操作。

  • __invert__(self):实现取反操作符 ~。

  • __round__(self, n):实现内建函数 round() ,n 是近似小数点的位数。

  • __floor__(self):实现 math.floor() 函数,即向下取整。

  • __ceil__(self):实现 math.ceil() 函数,即向上取整。

  • __trunc__(self):实现 math.trunc() 函数,即距离零最近的整数。

常见算数操作符

算术操作符就是算术运算符,比如+、-、*、/等;

  • __add__(self, other):实现加法操作。

  • __sub__(self, other):实现减法操作。

  • __mul__(self, other):实现乘法操作。

  • __floordiv__(self, other):实现使用 // 操作符的整数除法。

  • __div__(self, other):实现使用 / 操作符的除法。

  • __truediv__(self, other):实现 true 除法,这个函数只有使用 from **future** import division 时才有作用。

  • __mod__(self, other):实现 % 取余操作。

  • __divmod__(self, other):实现 divmod 内建函数。

  • __pow__:实现 ** 操作符。

  • __lshift__(self, other):实现左移位运算符 << 。

  • __rshift__(self, other):实现右移位运算符 >> 。

  • __and__(self, other):实现按位与运算符 & 。

  • __or__(self, other):实现按位或运算符 | 。

  • __xor__(self, other):实现按位异或运算符 ^ 。

反射算数运算符

反射算数运算的概念请自行百度;

  • __radd__(self, other):实现反射加法操作。

  • __rsub__(self, other):实现反射减法操作。

  • __rmul__(self, other):实现反射乘法操作。

  • __rfloordiv__(self, other):实现使用 // 操作符的整数反射除法。

  • __rdiv__(self, other):实现使用 / 操作符的反射除法。

  • __rtruediv__(self, other):实现 true 反射除法,这个函数只有使用 from __future__ import division时才有作用。

  • __rmod__(self, other):实现 % 反射取余操作符。

  • __rdivmod__(self, other):实现调用 divmod(other, self) 时 divmod 内建函数的操作。

  • __rpow__:实现 ** 反射操作符。

  • __rlshift__(self, other):实现反射左移位运算符 << 的作用。

  • __rshift__(self, other)
    实现反射右移位运算符 >> 的作用。

  • __rand__(self, other):实现反射按位与运算符 & 。

  • __ror__(self, other):实现反射按位或运算符 | 。

  • __rxor__(self, other):实现反射按位异或运算符 ^ 。

增强赋值运算符

所谓增强赋值运算就是同时使用了赋值符合运算符,比如:

x = 5
x += 1 # 也就是 x = x + 1

这些方法都应该返回左侧操作数应该被赋予的值(例如, a += b iadd 会返回 a + b ,这个结果会被赋给 a ),下面是方法列表:

  • __iadd__(self, other):实现加法赋值操作。

  • __isub__(self, other):实现减法赋值操作。

  • __imul__(self, other):实现乘法赋值操作。

  • __ifloordiv__(self, other):实现使用 //= 操作符的整数除法赋值操作。

  • __idiv__(self, other):实现使用 /= 操作符的除法赋值操作。

  • __itruediv__(self, other):实现 true 除法赋值操作,这个函数只有使用 from **future** import division 时才有作用。

  • __imod__(self, other):实现 %= 取余赋值操作。

  • __ipow__:实现 **= 操作。

  • __ilshift__(self, other):实现左移位赋值运算符 <<= 。

  • __irshift__(self, other):实现右移位赋值运算符 >>= 。

  • __iand__(self, other):实现按位与运算符 &= 。

  • __ior__(self, other):实现按位或赋值运算符 | 。

  • ixor(self, other):实现按位异或赋值运算符 ^= 。

类型转换操作符

也就是在定义数据类型以及数据类型转换时用到的魔法函数:

  • __int__(self):实现到int的类型转换。

  • __long__(self):实现到long的类型转换。

  • __float__(self):实现到float的类型转换。

  • __complex__(self):实现到复数的类型转换。

  • __oct__(self):实现到八进制数的类型转换。

  • __hex__(self):实现到十六进制数的类型转换。

  • __index__(self):实现当对象用于切片表达式时到一个整数的类型转换。如果你定义了一个可能会用于切片操作的数值类型,你应该定义 index

  • __trunc__(self):当调用 math.trunc(self) 时调用该方法, trunc 应该返回 self 截取到一个整数类型(通常是long类型)的值。

  • __coerce__(self):该方法用于实现混合模式算数运算,如果不能进行类型转换, coerce 应该返回 None 。反之,它应该返回一个二元组 self 和 other ,这两者均已被转换成相同的类型。

3、 类的表示

使用字符串来表示类是一个相当有用的特性。在Python中有一些内建方法可以返回类的表示,相对应的,也有一系列魔法方法可以用来自定义在使用这些内建函数时类的行为。

  • __str__(self):定义对类的实例调用 str() 时的行为,内部其实调用了__repr__

  • __repr__(self):定义对类的实例调用 repr() 时的行为。str() 和 repr() 最主要的差别在于“目标用户”。repr() 的作用是产生机器可读的输出(大部分情况下,其输出可以作为有效的Python代码),而 str() 则产生人类可读的输出。

  • __unicode__(self):定义对类的实例调用 unicode() 时的行为。unicode() 和 str() 很像,只是它返回unicode字符串。注意,如果调用者试图调用 str() 而你的类只实现了 __unicode__() ,那么类将不能正常工作。所有你应该总是定义 __str__() ,以防有些人没有闲情雅致来使用unicode。

  • __format__(self):定义当类的实例用于新式字符串格式化时的行为,例如, “Hello, 0:abc!”.format(a) 会导致调用 a.format(“abc”) 。当定义你自己的数值类型或字符串类型时,你可能想提供某些特殊的格式化选项,这种情况下这个魔法方法会非常有用。

  • __hash__(self):定义对类的实例调用 hash() 时的行为。它必须返回一个整数,其结果会被用于字典中键的快速比较。同时注意一点,实现这个魔法方法通常也需要实现 __eq__ ,并且遵守如下的规则:a == b 意味着 hash(a) == hash(b)。

  • __nonzero__(self):定义对类的实例调用 bool() 时的行为,根据你自己对类的设计,针对不同的实例,这个魔法方法应该相应地返回True或False。

  • __dir__(self):定义对类的实例调用 dir() 时的行为,这个方法应该向调用者返回一个属性列表。一般来说,没必要自己实现 __dir__ 。但是如果你重定义了 __getattr__ 或者 __getattribute__ (下个部分会介绍),乃至使用动态生成的属性,以实现类的交互式使用,那么这个魔法方法是必不可少的。

  • __dict__(self):类或对象的属性及方法都放在这里;

    • 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类__dict__ 里;
    • 对象的__dict__ 中存储了一些self.xxx的一些东西;
  • __name__:当前程序运行的模块名, 如果py文件作为模块被导入(import),那么__name__就是该py文件的文件名(也称 模块名),每一个py文件都有一个属于自己的__name__

  • __main__:实际上就是一个字符串,用来鉴别程序入口,没有太多花里胡哨的东西.

4、访问控制

很多从其他语言转向Python的人都抱怨Python的类缺少真正意义上的封装(即没办法定义私有属性然后使用公有的getter和setter)。然而事实并非如此。实际上Python不是通过显式定义的字段和方法修改器,而是通过魔法方法实现了一系列的封装。

  • __getattr__ (self, name):当我们访问一个不存在的属性的时候,会抛出异常,提示我们不存在这个属性。而这个异常就是__getattr__方法抛出的,其原因在于他是访问一个不存在的属性的最后落脚点。
class A(object):
    def __init__(self, value):
        self.value = value
 
    def __getattr__(self, item):
        print "into __getattr__"
        return  "can not find"
 
a = A(10)
print a.value
# 10
print a.name
# into __getattr__
# can not find
  • __setattr__(self, name, value) : 和 __getattr__ 不同, __setattr__ 可以用于真正意义上的封装。它允许你自定义某个属性的赋值行为,不管这个属性存在与否,也就是说你可以对任意属性的任何变化都定义自己的规则。然后,一定要小心使用 __setattr__ ,否则会出现递归调用。
class A(object):
    def __init__(self, value):
        self.value = value
 
    def __setattr__(self, name, value):
        self.name = value
    # 因为每次属性幅值都要调用 __setattr__(),所以这里的实现会导致递归
    # 这里的调用实际上是 self.__setattr('name', value)。因为这个方法一直
    # 在调用自己,因此递归将持续进行,直到程序崩溃


class A(object):
    def __init__(self, value):
        self.value = value
 
    def __setattr__(self, name, value):
        self.__dict__[name] = value # 使用 __dict__ 进行赋值
    # 定义自定义行为
  • __delattr__(self, name): 这个魔法方法和 __setattr__ 几乎相同,只不过它是用于处理删除属性时的行为。和 _setattr__ 一样,使用它时也需要多加小心,防止产生无限递归(在 __delattr__ 的实现中调用 del self.name 会导致无限递归)。

  • __getattribute__ (self, name): 看起来和上面那些方法很合得来,但是最好不要使用它。__getattribute__ 只能用于新式类。在最新版的Python中所有的类都是新式类,在老版Python中你可以通过继承 object 来创建新式类。__getattribute__ 允许你自定义属性被访问时的行为,它也同样可能遇到无限递归问题(通过调用基类的 __getattribute__ 来避免)。__getattribute__ 基本上可以替代 __getattr__ 。只有当它被实现,并且显式地被调用,或者产生 AttributeError 时它才被使用。这个魔法方法可以被使用(毕竟,选择权在你自己),我不推荐你使用它,因为它的使用范围相对有限(通常我们想要在赋值时进行特殊操作,而不是取值时),而且实现这个方法很容易出现Bug。

到这里,我们对Python中自定义属性存取控制有了什么样的印象?它并不适合轻度的使用。实际上,它有些过分强大,而且违反直觉。然而它之所以存在,是因为一个更大的原则:Python不指望让杜绝坏事发生,而是想办法让做坏事变得困难。自由是至高无上的权利,你真的可以随心所欲。下面的例子展示了实际应用中某些特殊的属性访问方法(注意我们之所以使用 super 是因为不是所有的类都有 __dict__ 属性,比如 int, list, dict等这些常用的数据类型是没有__dict__属性):

class AccessCounter(object):
    ''' 一个包含了一个值并且实现了访问计数器的类
    每次值的变化都会导致计数器自增'''

    def __init__(self, val):
            super(AccessCounter, self).__setattr__('counter', 0)
            super(AccessCounter, self).__setattr__('value', val)

    def __setattr__(self, name, value):
            if name == 'value':
                    super(AccessCounter, self).__setattr_('counter', self.counter + 1)
        # 使计数器自增变成不可避免
        # 如果你想阻止其他属性的赋值行为
        # 产生 AttributeError(name) 就可以了
        super(AccessCounter, self).__setattr__(name, value)

    def __delattr__(self, name):
            if name == 'value':
                    super(AccessCounter, self).__setattr('counter', self.counter + 1)
                    super(AccessCounter, self).__delattr(name)

5、 自定义序列

有许多办法可以让你的Python类表现得像是内建序列类型(字典,元组,列表,字符串等)。

既然讲到创建自己的序列类型,就不得不说一说协议了。协议类似某些语言中的接口,里面包含的是一些必须实现的方法。在Python中,协议完全是非正式的,也不需要显式的声明,事实上,它们更像是一种参考标准。

为什么我们要讲协议?因为在Python中实现自定义容器类型需要用到一些协议。首先,不可变容器类型有如下协议:想实现一个不可变容器,你需要定义 __len____getitem__ 。可变容器的协议除了上面提到的两个方法之外,还需要定义 __setitem____delitem__ 。最后,如果你想让你的对象可以迭代,你需要定义 __iter__ ,这个方法返回一个迭代器。迭代器必须遵守迭代器协议,需要定义 __iter__ (返回它自己)和 next 方法。

(1) 容器背后的魔法方法

  • __len__(self):返回容器的长度,可变和不可变类型都需要实现

  • __getitem__(self, key):定义对容器中某一项使用 self[key] 的方式进行读取操作时的行为。这也是可变和不可变容器类型都需要实现的一个方法。它应该在键的类型错误式产生 TypeError 异常,同时在没有与键值相匹配的内容时产生 KeyError 异常。

  • __setitem__(self, key):定义对容器中某一项使用 self[key] 的方式进行赋值操作时的行为。它是可变容器类型必须实现的一个方法,同样应该在合适的时候产生 KeyError 和 TypeError 异常。

  • __iter__(self, key):它应该返回当前容器的一个迭代器。迭代器以一连串内容的形式返回,最常见的是使用 iter() 函数调用,以及在类似 for x in container: 的循环中被调用。迭代器是他们自己的对象,需要定义 __iter__ 方法并在其中返回自己。

  • __reversed__(self):定义了对容器使用 reversed() 内建函数时的行为。它应该返回一个反转之后的序列。当你的序列类是有序时,类似列表和元组,再实现这个方法,

  • __contains__(self, item):__contains__ 定义了使用 in 和 not in 进行成员测试时类的行为。你可能好奇为什么这个方法不是序列协议的一部分,原因是,如果 contains 没有定义,Python就会迭代整个序列,如果找到了需要的一项就返回 True 。

  • __missing__(self ,key):__missing__ 在字典的子类中使用,它定义了当试图访问一个字典中不存在的键时的行为(目前为止是指字典的实例,例如我有一个字典 d , “george” 不是字典中的一个键,当试图访问 d["george’] 时就会调用 d.__missing__(“george”) )。

案例:实现了一些函数式结构的列表

class FunctionalList:
    '''一个列表的封装类,实现了一些额外的函数式
    方法,例如head, tail, init, last, drop和take。'''

    def __init__(self, values=None):
        if values is None:
            self.values = []
        else:
            self.values = values

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

    def __getitem__(self, key):
        # 如果键的类型或值不合法,列表会返回异常
        return self.values[key]

    def __setitem__(self, key, value):
        self.values[key] = value

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

    def __iter__(self):
        return iter(self.values)

    def __reversed__(self):
        return reversed(self.values)

    def append(self, value):
        self.values.append(value)

    def head(self):
        # 取得第一个元素
        return self.values[0]

    def tail(self):
        # 取得除第一个元素外的所有元素
        return self.valuse[1:]

    def init(self):
        # 取得除最后一个元素外的所有元素
        return self.values[:-1]

    def last(self):
        # 取得最后一个元素
        return self.values[-1]

    def drop(self, n):
        # 取得除前n个元素外的所有元素
        return self.values[n:]

    def take(self, n):
        # 取得前n个元素
        return self.values[:n]

当然啦,自定义序列有更大的用处,而且绝大部分都在标准库中实现了,比如Counter , OrderedDict 和 NamedTuple 。

6、反射

可以通过定义魔法方法来控制用于反射的内建函数 isinstance 和 issubclass 的行为。下面是对应的魔法方法:

  • __instancecheck__(self, instance):检查一个实例是否是你定义的类的一个实例(例如 isinstance(instance, class) )。
  • __subclasscheck__(self, subclass):检查一个类是否是你定义的类的子类(例如 issubclass(subclass, class) )。

7、抽象基类

8、 可调用的对象

在Python中,函数是一等的对象。这意味着它们可以像其他任何对象一样被传递到函数和方法中,这是一个十分强大的特性:函数的引用;

  • __call__ (self, [args…]):允许类的一个实例像函数那样被调用。本质上这代表了 x() 和 x.__call__ () 是相同的。注意 __call__ 可以有多个参数,这代表你可以像定义其他任何函数一样,定义 __call__ ,喜欢用多少参数就用多少。

__call__ 在某些需要经常改变状态的类的实例中显得特别有用。“调用”这个实例来改变它的状态,是一种更加符合直觉,也更加优雅的方法。一个表示平面上物体的类是一个不错的例子:

class Entity:
    '''表示一个实体的类,调用它的实例
    可以更新实体的位置'''

    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size

    def __call__(self, x, y):
        '''改变实体的位置'''
        self.x, self.y = x, y

if __name__ == '__main__':
    entity = Entity(160,8,20)
    print(f"x={entity.x},y={entity.y}")

    # 通过重写了__call__方法使得对象可以像方法一样被传参调用
    entity(16,10)
    print(f"x={entity.x},y={entity.y}")

9、上下文管理器

当对象使用 with 声明创建时,上下文管理器允许类做一些设置和清理工作。上下文管理器的行为由下面两个魔法方法所定义:

  • __enter__(self):定义使用 with 声明创建的语句块最开始上下文管理器应该做些什么。注意 __enter__ 的返回值会赋给 with 声明的目标,也就是 as 之后的东西。

  • __exit__(self, exception_type, exception_value, traceback):定义当 with 声明语句块执行完毕(或终止)时上下文管理器的行为。它可以用来处理异常,进行清理,或者做其他应该在语句块结束之后立刻执行的工作。如果语句块顺利执行, exception_type , exception_value 和 traceback 会是 None 。否则,你可以选择处理这个异常或者让用户来处理。如果你想处理异常,确保 __exit__ 在完成工作之后返回 True 。如果你不想处理异常,那就让它发生吧。

对一些具有良好定义的且通用的设置和清理行为的类,__enter____exit__ 会显得特别有用。你也可以使用这几个方法来创建通用的上下文管理器,用来包装其他对象。下面是一个例子:

class Closer:
    '''一个上下文管理器,可以在with语句中
    使用close()自动关闭对象'''

    def __init__(self, obj):
        self.obj = obj

    def __enter__(self):
        return self.obj # 绑定到目标

    def __exit__(self, exception_type, exception_value, traceback):
        try:
                self.obj.close()
        except AttributeError: # obj不是可关闭的
                print('Not closable.')
                return True # 成功地处理了异常

if __name__ == '__main__':
    with Closer(open('del.txt','r',encoding='utf8')) as f:
        print(f.read())

    # Not closable.----int(5)是无法关闭的对象
    with Closer(int(5)) as s:
        s += 1
        print(s)

Python标准库包含一个 contextlib 模块,里面有一个上下文管理器 contextlib.closing() 基本上和我们的包装器完成的是同样的事情(但是没有包含任何当对象没有close()方法时的处理)。源码如下:

class closing(AbstractContextManager):
    """Context to automatically close something at the end of a block.

    Code like this:

        with closing(<module>.open(<arguments>)) as f:
            <block>

    is equivalent to this:

        f = <module>.open(<arguments>)
        try:
            <block>
        finally:
            f.close()

    """
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

10、Python描述符(器)

什么是描述符:

  • 描述符是一个特殊的Python对象,该对象定义了__get__、set、__delete__三个方法中的一个或多个;
  • 描述符单独存在没有意义,一般描述符就是某个实例的属性,当访问该属性时,就会调用描述符的__get__方法,而修改会调用__set__方法,删除会调用__delete__方法;
  • 描述符是对多个属性运用相同存取逻辑的一种方式。

内置函数:

  • __get__(self, instance, owner):定义当试图取出描述符的值时的行为。instance 是拥有者类的实例, owner 是拥有者类本身。
  • __set__(self, instance, value ):定义当描述符的值改变时的行为。instance 是拥有者类的实例, value 是要赋给描述符的值。
  • __delete__(self, instance):定义当描述符的值被删除时的行为。instance 是拥有者类的实例。

描述符的种类

描述符分两种:覆盖型描述符非覆盖型描述符,实现了__set__方法的就叫覆盖型描述符,没有实现就叫非覆盖型描述符;

两种类型的区别

这两种描述符的区别就是访问的优先级不同,访问属性的优先级如下:

  • 首先会先调用实例的 __getattribute__方法,然后依次按如下顺序去获取属性,获取到就返回:

    • 如果访问的属性是覆盖型描述符,则调用覆盖型描述符的__get__方法获取属性(由 __getattribute__调用)
    • 从实例的__dict__字典获取属性
    • 从实例所属类的__dict__字典获取属性
    • 如果访问的属性是非覆盖型描述符,则调用非覆盖型描述符的__get__方法获取属性
    • 从实例父类的__dict__字典获取属性
    • 如果上述都未找到,则调用实例的 __getattr__方法
  • __get__(self, instance, owner):定义当试图取出描述符的值时的行为。instance 是拥有者类的实例, owner 是拥有者类本身。

  • __set__(self, instance, value ):定义当描述符的值改变时的行为。instance 是拥有者类的实例, value 是要赋给描述符的值。

  • __delete__(self, instance):定义当描述符的值被删除时的行为。instance 是拥有者类的实例。

property是Python自带的描述符

给类的方法加上property装饰器可以使方法变为一个描述符,该描述符可以实现对属性值的设置、获取、删除等功能,如下代码所示:

class Item:
    @property    # 等价于price = property(price),也就是实例化了一个描述符对象price
    def price(self):
        return self._price

    @price.setter    # 使用描述符对象的setter方法对price进行装饰(price = price.setter(price)),这里就是将设置属性值的方法传入描述符内,修改price属性时就会通过__set__方法调用传入的方法
    def price(self, value):
        if value > 0:    # 校验price
            self._price = value
        else:
            raise ValueError("Valid value..")

item = Item()
item.price = 100
print(item.price)
item.price = -100    # 会抛出ValueError异常

property的实现原理

class MyProperty(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        print("====>in __get__")
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError
        return self.fget(obj)

    def __set__(self, obj, value):
        print("====>in __set__")
        if self.fset is None:
            raise AttributeError
        self.fset(obj, value)

    def __delete__(self, obj):
        print("====>in __delete__")
        if self.fdel is None:
            raise AttributeError
        self.fdel(obj)

    def getter(self, fget):
        pass

    def setter(self, fset):
        print("====>in setter")
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        print("====>in deleter")
        return type(self)(self.fget, self.fset, fdel, self.__doc__)


class Item:
    @MyProperty
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value > 0:
            self._price = value
        else:
            raise ValueError("Valid value..")

测试:

>>> from property2 import Item
====>in setter       # 创建类的时候就开始装饰了

>>> item = Item()

>>> item.price = 100
====>in __set__

>>> item.price
====>in __get__
100

>>> item.price = -100
====>in __set__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\mycode\localtest\property2.py", line 20, in __set__
    self.fset(obj, value)
  File "D:\mycode\localtest\property2.py", line 50, in price
    raise ValueError("Valid value..")
ValueError: Valid value..

11、 Pickling

python的pickle模块实现了基本的数据序列化和反序列化,把 Python 对象直接保存到文件里,而不需要先把它们转化为字符串再保存,也不需要用底层的文件访问操作,直接把它们写入到一个二进制文件里。pickle 模块会创建一个 Python 语言专用的二进制格式,不需要使用者考虑任何文件细节,它会帮你完成读写对象操作。用pickle比你打开文件、转换数据格式并写入这样的操作要节省不少代码行。

优缺点

  • 优点:

    • pickle提供了一个简单的持久化功能。可以将对象以文件的形式存放在磁盘上。

    • 通过pickle模块的序列化操作我们能够将程序中运行的对象信息保存到文件中去,永久存储

    • python中几乎所有的数据类型(列表,字典,集合,类等)都可以用pickle来序列化。

    • 没有外部标准强加的限制,例如JSON或XDR(不能代表指针共享)。

    • 默认情况下,pickle数据格式使用相对紧凑的二进制表示。如果需要最佳尺寸特征,则可以有效地压缩数据。

    • pickle可以表示极其庞大的Python类型(其中许多是自动的,通过巧妙地使用Python的内省工具;复杂的案例可以通过实现特定的对象API来解决)。

    • 通过pickle模块的反序列化操作,我们能够从文件中创建上一次程序保存的对象。

  • 缺点:

    • pickle模块只能在python中使用,仅限于传输的两端都是python的情况,且需要尽量保持两端的版本一致。非Python程序可能无法重建pickled Python对象。

    • pickle序列化后的数据,可读性差,人一般无法识别。

pickle和JSON的区别

  • JSON是一种文本序列化格式(它输出unicode文本,虽然大部分时间它被编码utf-8),而pickle是二进制序列化格式。

  • JSON是人类可读的,而pickle则不是。

  • JSON跨语言(不同语言间的数据传递可用json交接)、体积小,但只能支持int\str\list\tuple\dict;而pickle专为python设计,支持python所有的数据类型;

  • pickle和JSON都提供四个功能:dumps ,dump ,loads ,load

主要方法

在pickle中dumps()和loads()操作的是bytes类型,而在使用dump() 和lload() 读写文件时,要使用rb或wb模式,也就是只接收bytes类型的数据。

(1) pickle.dump(obj, file)

将Python数据转换并保存到pickle格式的文件内,文件格式任意;

import pickle

data = {'foo': [1,2,3],
                'bar': ('Hello', 'world!'),
                'baz': True}
jar = open('data.pkl', 'wb')
pickle.dump(data, jar) # 将pickle后的数据写入jar文件
jar.close()

**可读性查:**文本编辑器打开上面保存的data文件,会发现其中全是不可认读的编码。

(2) pickle.dumps(obj)

将Python数据转换为pickle格式的bytes字串。

import pickle
dic = {"k1":"v1","k2":123}
s = pickle.dumps(dic)
print(s)

(3) pickle.load(file)

从pickle格式的文件中读取数据并转换为Python的类型。

pkl_file = open('data.pkl', 'rb') # 与pickle后的数据连接
data = pickle.load(pkl_file) # 把它加载进一个变量
print(data)
pkl_file.close()

(4) pickle.loads(bytes_object)

将pickle格式的bytes字串转换为Python的类型。

import pickle
dic = {"k1":"v1","k2":123}
s = pickle.dumps(dic)
dic2 = pickle.loads(s)
print(dic2)

pickle小案例: 将学生对象存入list,将list通过pickle写入文件;

import os
import pickle

class Utils:
    @classmethod
    def write(cls,data: list,filename: str) -> None:
        """
        将数据写入文件
        :param data: list格式数据,其中可以存放任何对象
        :param filename:写入文件名
        :return:None
        """
        path = os.path.dirname(os.path.abspath(__file__)) + filename
        # 写入
        try:
            with open(path, 'wb') as f:
               return pickle.dump(data, f)
        except Exception as e:
            print(e)

    @classmethod
    def read(cls,filename: str) ->list :
        """
        从文件中读取数据
        :param filename: 文件名
        :return: 返回list格式数据
        """
        path = os.path.dirname(os.path.abspath(__file__)) + filename
        # 如果文件不存在则返回空列表
        try:
            with open(path, 'rb') as f:
                data = pickle.load(f)
            return data
        except:
            return []

class Student:
    """学生类"""
    def __init__(self,sid,name,age,gender):
        self.sid = sid
        self.name = name
        self.age = age
        self.gender = gender

    def __repr__(self):
        return "{"+f"编号:{self.sid},姓名:{self.name},年龄:{self.age},性别:{self.gender}"+"}"

class StudentManager:
    """学生管理系统类"""
    def __init__(self, filename):
        self.filename = filename
        self.stu_list = Utils.read(filename)

    # def __setstate__(self, state):
    #     return state
    #
    # def __getstate__(self):
    #     return self.stu_list

    @property
    def sid_list(self):
        # 编号列表,用于判断列表是否重复
        temp_list = []
        # 将已有学生编号存入列表
        if len(self.stu_list) != 0:
            for stu in self.stu_list:
                temp_list.append(stu.sid)
        return temp_list

    # 添加学生编号输入处理:编号必须是3位纯数字
    def __add_sid_operator(self):
        while True:
            temp_sid = input("请输入学生编号,格式为3位数字,退出请输入bye:")
            if temp_sid == "bye":
                # 输入bye跳出学生添加操作
                break
            elif temp_sid.isdigit() and len(temp_sid) == 3:
                # 是3位纯数字则输入正确,判断之前是否已存在此编号
                if temp_sid in self.sid_list:
                    # 已存在则跳出循环重新输入
                    print(f"编号{temp_sid}已存在,请重新输入,退出请输入bye!")
                    continue
                else:
                    # 以前不存在,则输入完成,跳出循环
                    break
            else:
                print("学生编号输入有误,请重新输入,退出请输入bye!")
        return temp_sid

    # 姓名输入处理:姓名必须是字母或中文
    def __name_operator(self):
        while True:
            temp_name = input("请输入学生姓名,必须是字母或中文,退出请输入bye:")
            if temp_name == "bye":
                # 输入bye跳出循环
                break
            elif temp_name.isalpha():
                # 字母或中文又不是bye则输入正确,跳出循环
                break
            else:
                print("学生姓名输入有误,请重新输入,退出请输入bye!")
        return temp_name

    # 年龄输入处理:年龄必须2位纯数字,并且不能以0开头
    def __age_operator(self):
        while True:
            temp_age = input("请输入学生年龄,必须2位纯数字并且不能以0开头,退出请输入bye:")
            if temp_age == "bye":
                # 输入bye退出循环
                break
            elif len(temp_age) == 2 and not temp_age.startswith("0") and temp_age.isdigit():
                # 输入不以0开头的两位数字则输入正确,跳出循环
                #  temp_age = int(temp_age)
                break
            else:
                print("年龄输入有误,请重新输入,退出请输入bye!")
        return temp_age

    # 性别输入处理:必须是男或者女才行
    def __gender_operator(self):
        while True:
            temp_gender = input("请输入性别,必须是”男“或”女“:")
            if temp_gender == "bye":
                # 输入bye退出循环
                break
            elif temp_gender == "男" or temp_gender == "女":
                # 输入正确,跳出循环
                break
            else:
                print("性别输入有误,请重新输入!")
        return temp_gender

    # 查询学生编号输入处理:编号必须是3位纯数字
    def __query_sid_operator(self):
        while True:
            temp_sid = input("请输入学生编号,格式为3位数字,退出请输入bye:")
            if temp_sid == "bye":
                # 输入bye跳出学生查询操作
                break
            elif temp_sid.isdigit() and len(temp_sid) == 3:
                # 是3位纯数字则输入正确
                break
            else:
                print("学生编号输入有误,请重新输入,退出请输入bye!")
        return temp_sid

    # 1. 实现菜单函数,输出下列信息,返回用户输入的编号,并进行输入校验。
    def menu(self):
        # 启动菜单时,如果有学生信息则打印
        print("---------------start------------------")
        if len(self.stu_list) != 0:
            for stu in self.stu_list:
                print(stu)
            print(f"现一共有{len(self.stu_list)}个学生")
        # 打印菜单
        print("*" * 40)
        print('"*' + "学生管理系统".center(30) + '*"')
        print('"*' + "1.添加新学生信息".center(30) + '*"')
        print('"*' + "2.通过学号修改学生信息".center(28) + '*"')
        print('"*' + "3.通过学号删除学生信息".center(28) + '*"')
        print('"*' + "4.通过姓名删除学生信息".center(28) + '*"')
        print('"*' + "5.通过学号查询学生信息".center(28) + '*"')
        print('"*' + "6.通过姓名查询学生信息".center(28) + '*"')
        print('"*' + "7.显示所有学生信息".center(30) + '*"')
        print('"*' + "8.退出系统".center(32) + '*"')
        print("*" * 40)
        select_op = input("输入编号选择操作:")
        return select_op

    # 3. 实现添加学生函数,函数参数为编号,姓名,年龄,性别四个参数,返回是否添加成功的结果,要求编号不可重复。
    def add_student(self):
        print("欢迎进入添加学生信息操作!")
        # 循环输入学生信息进行添加,只要输入bye结束添加
        while True:
            # 输入编号
            print("*********************", self.sid_list)
            sid = self.__add_sid_operator()

            if sid == "bye":
                # 只要输入bye结束添加
                print("bye add_student")
                break

            # 输入姓名
            name = self.__name_operator()
            if name == "bye":
                # 只要输入bye结束添加
                print("bye add_stu")
                break

            # 输入年龄
            age = self.__age_operator()
            if age == "bye":
                # 只要输入bye结束添加
                print("bye add_stu")
                break

            # 输入性别
            gender = self.__gender_operator()
            if gender == "bye":
                # 只要输入bye结束添加
                print("bye add_stu")
                break

            # 四个输入项都正确输入之后才创建对象
            # 先把编号存入列表,方便后续去重判断
            self.sid_list.append(sid)
            # 创建学生对象
            stu = Student(sid, name, age, gender)
            # 将学生对象添加到列表
            self.stu_list.append(stu)

            # 是否继续添加学生信息
            again = input(
                f"学生信息{stu}已成功添加,是否继续添加,继续添加请按“1”,退出添加请任意键:")
            if again != "1":
                print(f"退出 add_stu,当前已有学生信息为:{self.stu_list}")
                break

    # 4. 实现修改函数,参数为学号,如果学生存在,则进行修改,不存在输出提示,并返回是否修改成功
    def update_sdudent_by_sid(self):
        print("欢迎进入学生信息修改!")
        while True:
            tem_sid = input("请输入需要修改的3位数字学生编号,退出修改请输入bye:")
            if tem_sid == "bye":
                print("bye update_sdudent_by_sid")
                break
            elif tem_sid.isdigit() and len(tem_sid) == 3:
                # 判断是否有学生
                if len(self.stu_list) == 0:
                    print("当前没有学生,请先添加学生!")
                    break
                # 输入正确才能进行修改操作
                for stu in self.stu_list:
                    # 如果学生存在,则进行修改
                    if stu.sid == tem_sid:
                        info = input(f"编号为{tem_sid}的学生信息为:{stu},请选择需要修改类型,1-姓名,2-年龄,3-性别,返回请输入bye:")
                        if info == "bye":
                            print("bye updata_stu")
                            break
                        elif info == "1":
                            # 修改姓名
                            name = self.__name_operator()
                            if name == "bye":
                                # 只要输入bye结束添加
                                print("bye update_stu")
                                break
                            stu.name = name
                            # 返回修改成功信息
                            print(f"编号为{tem_sid}学生信息修改成功:{stu}")
                            break
                        elif info == "2":
                            # 修改年龄
                            age = self.__age_operator()
                            if age == "bye":
                                # 只要输入bye结束添加
                                print("bye update_stu")
                                break
                            stu.age = age
                            # 返回修改成功信息
                            print(f"编号为{tem_sid}学生信息修改成功:{stu}")
                            break
                        elif info == "3":
                            # 修改性别
                            gender = self.__gender_operator()
                            if gender == "bye":
                                # 只要输入bye结束添加
                                print("bye update_stu")
                                break
                            stu.gender = gender
                            # 返回修改成功信息
                            print(f"编号为{tem_sid}学生信息修改成功:{stu}")
                            break
                        else:
                            print("修改类型输入有误!")
                else:
                    print(f"编号为{tem_sid}的学生不存在!")
            else:
                print("学生编号输入有误,请重新输入!")

    # 5. 实现删除函数,参数为学号,如果学生存在,则进行删除,不存在输出提示,并返回是否删除成功
    def delete_stu_by_sid(self):
        # 先确定是否有学生信息
        if len(self.stu_list) != 0:
            while True:
                temp_sid = input("请输入学生编号,格式为3位数字,退出删除请输入bye:")
                if temp_sid == "bye":
                    # 输入bye跳出学生删除操作
                    print(f"bye delete_stu_by_sid")
                    break
                elif temp_sid.isdigit() and len(temp_sid) == 3:
                    # 是3位纯数字则输入正确,判断之前是否已存在此编号
                    for stu in self.stu_list:
                        # 存在则删除并跳出循环
                        if stu.sid == temp_sid:
                            self.stu_list.remove(stu)
                            print(f"编号为{temp_sid}的学生已删除!")
                            break
                    else:
                        # 没有这个编号的学生则继续输入
                        print(f"编号为{temp_sid}的学生不存在,请重新输入学生编号,退出删除请输入bye!")
                        continue
                else:
                    print("学生编号输入有误,请重新输入!")
        else:
            print("当前没有学生信息!")

    # 6. 实现删除函数,参数为姓名,如果学生存在,则进行删除(同名学生全部删除),不存在输出提示,并返回是否删除成功
    def delete_stu_by_name(self):
        while True:
            temp_name = self.__name_operator()
            if temp_name == 'bye':
                print(f"bye delete_stu_by_name")
                break

            # 将重名的学生单独存放一个列表
            exist_name = []
            for stu in self.stu_list:
                if stu.name == temp_name:  # 说明重名
                    exist_name.append(stu)

            # 遍历重名学生列表去删除原始列表
            if len(exist_name) != 0:
                print(f"名字为{temp_name}的学生信息有:{exist_name}")
                for exist in exist_name:
                    self.stu_list.remove(exist)
                print(f"名字为{temp_name}的所有学生信息已成功删除!")
                # 干掉临时exist_name变量,清理内存
                del exist_name
                continue
            if len(exist_name) == 0:
                print(f"姓名为{temp_name}的学生不存在,请重新输入学生姓名,退出删除请输入bye!")
                continue

    # 7. 实现查询函数,参数为学号,如果学生存在,则输出学生信息,不存在输出提示,并返回是否查询成功
    def query_stu_by_sid(self):
        while True:
            temp_sid = self.__query_sid_operator()
            if temp_sid == "bye":
                print(f"bye query_stu_by_sid")
                break
            if len(self.stu_list) != 0:
                # 存在sid的学生信息则查询
                for stu in self.stu_list:
                    if stu.sid == temp_sid:
                        print(f"学生编号为{temp_sid}的学生信息为{stu}")
                        break
                else:
                    print(f"没有编号为{temp_sid}的学生信息!")
            else:
                print(f"当前没有任何学生,请先添加学生!")

    # 8. 实现查询函数,参数为姓名,如果学生存在,则输出学生信息(同名学生全部输出),不存在输出提示,并返回是否删除成功
    def query_stu_by_name(self):
        while True:
            temp_name = self.__name_operator()
            if temp_name == "bye":
                print(f"bye query_stu_by_name")
                break

            if len(self.stu_list) != 0:
                # 将该名字的学生重新保存
                temp_list = []
                for stu in self.stu_list:
                    if stu.name == temp_name:
                        temp_list.append(stu)
                # 输出查询结果
                if len(temp_list) != 0:
                    print(f"姓名为{temp_name}的学生有{temp_list}")
                    continue
                else:
                    print(f"姓名为{temp_name}的学生不存在,请重新输入学生姓名,退出查询请输入bye!")
            else:
                print(f"当前没有任何学生,请先添加学生!")

    # 9. 实现函数,输出所有学生信息
    def get_all_student(self):
        if len(self.stu_list) != 0:
            i = 1
            for stu in self.stu_list:
                print(f"第{i}个学生信息为:***{stu}***")
                i += 1
        else:
            print(f"当前没有任何学生,请先添加!")

    # 2. 实现控制函数,用来控制菜单的输出与功能的选择,直到用户选择8,结束程序运行。
    def start(self):
        while True:
            # 打印菜单
            op = self.menu()
            match op:
                case "1":
                    print("1.添加新学生信息")
                    # 添加学生
                    self.add_student()
                case "2":
                    print("2.通过学号修改学生信息 ")
                    self.update_sdudent_by_sid()
                case "3":
                    print("3.通过学号删除学生信息 ")
                    # 通过编号删除学生信息
                    self.delete_stu_by_sid()
                case "4":
                    print("4.通过姓名删除学生信息 ")
                    # 通过姓名删除学生信息
                    self.delete_stu_by_name()
                case "5":
                    print("5.通过学号查询学生信息 ")
                    # 通过学号查询学生信息
                    self.query_stu_by_sid()
                case "6":
                    print("6.通过姓名查询学生信息 ")
                    # 通过姓名查询学生信息
                    self.query_stu_by_name()
                case "7":
                    print("7.显示所有学生信息")
                    # 显示所有学生信息
                    self.get_all_student()
                case "8":
                    print("8.退出系统  ")
                    # 退出时将学生信息保存到文件
                    Utils.write(self.stu_list, self.filename)
                    break
                case _:
                    print("编号输入有误,请重新输入编号!")

if __name__ == '__main__':
    manager = StudentManager('student.pkl')
    manager.start()

  • file:表示显示文件当前的位置;

Pickle你的对象

任何遵守pickle协议的类都可以被pickle。Pickle协议有四个可选方法,可以让类自定义它们的行为;

  • __getinitargs__ (self):如果你想让你的类在反pickle时调用 __init__ ,你可以定义 __getinitargs__ (self) ,它会返回一个参数元组,这个元组会传递给 __init__ 。注意,这个方法只能用于旧式类。

  • __getnewargs__(self):对新式类来说,可以通过这个方法改变类在反pickle时传递给 __new__ 的参数。这个方法应该返回一个参数元组。

  • __getstate__(self):可以自定义对象被pickle时被存储的状态,也就是指定将那些信息记录下来,而不使用对象的 __dict__ 属性。这个状态在对象被反pickle时会被 __setstate__ 使用。

  • __setstate__(self):当一个对象被反pickle时,如果定义了 __setstate__ ,对象的状态会传递给这个魔法方法,也就是指明如何利用已记录的信息,而不是直接应用到对象的 __dict__ 属性。这个魔法方法和 __getstate__ 相互依存:当这两个方法都被定义时,你可以在Pickle时使用任何方法保存对象的任何状态。

  • __reduce__(self):当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。reduce 被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 __setstate__ 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选);

  • __reduce_ex__(self):__reduce_ex__ 的存在是为了兼容性。如果它被定义,在pickle时 __reduce_ex__ 会代替 __reduce__ 被调用。__reduce__ 也可以被定义,用于不支持 __reduce_ex__ 的旧版pickle的API调用。

案例:
1、在序列化时, getstate_ 可以指定将那些信息记录下来, 而 setstate 指明如何利用已记录的信息

import pickle
# 基类
class Demo:
    def __init__(self, name, age=0):
        self.name = name
        self.age = age

    def __str__(self):
        return f"name: {self.name}-age: {self.age}"

class DemoState1(Demo):
    # 反序列化时调用, state 是 __getstate__ 的返回对象
    def __setstate__(self, state):
        print(" __setstate__")
        # 将记录的信息("Python3") 赋给 name
        self.name = state
        self.age = 31

    # 序列化时调用
    def __getstate__(self):
        print(" __getstate__")
        # 记录信息 “Python3”
        return "Python3"

if __name__ == '__main__':
    d = DemoState1("臧三",18)
    print(d)

    # 系列化为字符串
    d_dumps = pickle.dumps(d)
    print(d_dumps)
    
    # 反序列化查看
    d_loads = pickle.loads(d_dumps)
    print(d_loads)

2、可以省略 setstate, 但 getstate 必须返回一个字典, 字典的内容被加载到当前的对象;

import pickle
# 基类
class Demo:
    def __init__(self, name, age=0):
        self.name = name
        self.age = age

    def __str__(self):
        return f"name: {self.name}-age: {self.age}"

class DemoState2(Demo):
    # 省略 __setstate__
    # 自动将 __getstate__ 的返回对象添加到当前对象的属性

    # 序列化时调用
    def __getstate__(self):
        # 必须返回字典
        state = {"name": "C", "age": 50, "address": "Bell Lab"}
        return state


if __name__ == '__main__':
    d = DemoState2("臧三",18)
    print(d)

    # 系列化为字符串
    d_dumps = pickle.dumps(d)
    print(d_dumps)

    # 反序列化查看
    d_loads = pickle.loads(d_dumps)
    print(d_loads)

3、如果 getstatesetstate 都省略, 那么就是默认情况, 自动保存和加载对象的属性字典 dict

import pickle
# 基类
class Demo:
    def __init__(self, name, age=0):
        self.name = name
        self.age = age

    def __str__(self):
        return f"name: {self.name}-age: {self.age}"

if __name__ == '__main__':
    d = Demo("张三",18)
    print(d)

    data = d.__dict__
    print(data)

    d_dumps_bytes = pickle.dumps(d)
    print(d_dumps_bytes)

    d_loads = pickle.loads(d_dumps_bytes)
    print(d_loads)

注意: 大多数时候保持默认情况即可, 但是遇到不可序列化的对象, 例如 self.file = open("filename") , 就需要忽略掉__dict__属性;

补充: JSON读写文件

import json

data = [ { 'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4, 'e' : 5 },{ 'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4, 'e' : 5 },{ 'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4, 'e' : 5 },{ 'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4, 'e' : 5 } ]

# 将数据写入文件
with open("data.json", 'w', encoding='utf8') as f:
    json.dump(data,f)

# 动文件读取数据
with open("data.json", 'r', encoding='utf8') as f:
    data = json.load(f)
    for s in data:
        print(s)

这个other该怎么理解呢?