# 内置方法

  • 用户定义一个 class 时,可以包含任意个 method ,每个 method 的名称由用户任意决定。但有些特殊的 method 名称,被 Python 解释器占用了,称为内置方法。
    • 内置方法的命名特点:以两个下划线 __ 开头,以两个下划线 __ 结尾。
    • 内置方法用于实现一些特殊的功能。
      • 例如定义了 __add__() 方法,就可以让当前类的实例,支持加法运算。
      • 由于内置方法的功能特殊,它们又被称为魔法方法(magic method)。
    • Python 总共设计了几十种内置方法。
      • 用户定义一个 class 时,不需要定义所有内置方法。
      • 用户定义一个 class 时,即使不主动定义内置方法,也会从元类、父类,继承一些基础的内置方法,例如 __new__()__init__()__repr__()__str__()

# 关于实例

# __new

def __new__(cls, ...)
  • 功能:创建当前类的一个实例,并返回该实例(的引用)。
    • 它要求输入当前类,作为第一个实参。之后还可传入任意个参数。
  • 例:
    >>> str.__new__()
    TypeError: str.__new__(): not enough arguments
    >>> str.__new__(str, 'hello')
    'hello'
    
  • 如果让 __new__() 总是返回同一个实例,则可以实现单例模式。
    >>> class Test:
    ...     def __new__(cls):
    ...         if not hasattr(cls, 'instance'):
    ...             cls.instance = super().__new__(cls)
    ...         return cls.instance
    ...
    >>> Test()
    <__main__.Test object at 0x00000140699F54C0>
    >>> Test()
    <__main__.Test object at 0x00000140699F54C0>
    >>> Test() is Test()
    True
    

# __init

def __init__(self, ...)
  • 功能:用于在新建实例之后,初始化该实例。类似于 C++ 中的构造函数。
    • 它要求输入当前实例,作为第一个实参。之后还可传入任意个参数。
  • 例:
    >>> class Test:
    ...     def __new__(cls):
    ...         print('__new__')
    ...         return super().__new__(cls)
    ...     def __init__(self):
    ...         print('__init__')
    ...
    >>> t = Test.__new__(Test)  # 创建一个实例,此时该实例尚未执行 __init__() 方法
    __new__
    >>> t.__init__()
    __init__
    

# __del

def __del__(self)
  • 功能:用于销毁实例。类似于 C++ 中的析构函数。
  • 何时生效?
    • 用关键字 del 删除一个对象时,并不会调用该对象的 __del__() 方法,只是使得其被引用数减 1 。
    • 当一个对象的被引用数,减少到 0 时,Python 解释器会自动调用该对象的 __del__() 方法,然后销毁该对象。
    • 一般情况下,用户不必定义 __del__() 方法。但有时,用户希望在每个对象即将被销毁时,执行某些操作,例如记录日志,则可以定义 __del__() 方法。

# __call

def __call_(self, ...)
  • 在 Python 的底层,函数也是基于 class 定义的。当用户执行 function_name() 调用一个函数时,实际上是调用该函数的 __call__() 方法。
  • 因此,用户可以给任何 class 定义 __call__() 方法,使得该 class 的实例,可以当作函数被调用。
  • 例:
    >>> class Test:
    ...     def __call__(self, *args, **kwargs):
    ...         print(*args, **kwargs)
    ...
    >>> t1 = Test()
    >>> t1('hello')
    hello
    

# 关于属性

# __getattribute

def __getattribute__(self, name)
  • 功能:当用户读取对象的一个属性(也就是实例变量、实例方法)时,会自动调用对象的该方法。
    • 如果未找到该属性,则抛出 AttributeError 异常。
  • 例:
    >>> class Test:
    ...     def __getattribute__(self, name):
    ...         if name == 'a':
    ...             return 0
    ...         else:
    ...             # return self.__getattribute__(name)  # 这行代码会调用当前 __getattribute__() 方法,导致无限循环
    ...             return super().__getattribute__(name) # 为了避免无限循环,应该调用父类的 __getattribute__() 方法
    ...
    >>> Test.a    # 读取类变量、类方法时,不会调用 __getattribute__() 方法
    AttributeError: type object 'Test' has no attribute 'a'
    >>> Test().a
    0
    >>> Test().b
    AttributeError: 'Test' object has no attribute 'b'
    

# __getattr

def __getattr__(self, name)
  • 功能:当 __getattribute__() 方法抛出 AttributeError 异常时,会自动捕捉异常,然后调用 obj.__getattr__() 方法,决定如何读取该属性。
  • 例:
    >>> class Test:
    ...     def __getattr__(self, name):
    ...         if name == 'a':
    ...             return 0
    ...         else:
    ...             raise AttributeError("this object has no attribute '{}'".format(name))
    ...
    >>> Test().a
    0
    >>> Test().b
    AttributeError: this object has no attribute 'b'
    

# __setattr

def __setattr__(self, name, value)
  • 功能:当用户给对象的一个属性赋值时,会自动调用对象的该方法。
  • 赋值时,不会调用 __getattribute__() 方法。
  • 例:
    >>> class Test:
    ...     def __setattr__(self, name, value):
    ...         # self.name = value   # 这行代码会调用当前 __setattr__() 方法,导致无限循环
    ...         super().__setattr__(name, value)  # 为了避免无限循环,应该调用父类的 __setattr__() 方法
    ...
    >>> t = Test()
    >>> t.a = 0
    >>> t.b
    0
    

# __delattr

def __delattr__(self, name)
  • 功能:当用户删除对象的一个属性时,会自动调用对象的该方法。

# 关于字符串

# __repr

# __str

def __repr__(self)  # 执行 repr(obj) 时,会自动调用该方法
def __str__(self)   # 执行 str(obj) 时,会自动调用该方法
  • 这两个方法的用途不同,返回值不一定相同。

    • __repr__() 用于将对象用一个 str 表示(representation),供用户查看这个对象的大致内容,甚至执行 eval(repr(obj)) == obj 可以重新创建该对象。
    • __str__() 用于将对象转换成 str 类型,要求准确保留该对象的所有内容。
  • 例:

    >>> a = list()
    >>> a               # 将对象直接输入 Python 终端,相当于执行 print(repr(a))
    []
    >>> repr(a)
    '[]'
    >>> print(repr(a))
    []
    >>> str(a)          # 将对象转换成 str 类型
    '[]'
    >>> print(a)        # 将对象 print 到 Python 终端,相当于执行 print(str(a))
    []
    >>> print(str(a))
    []
    
  • 例:

    >>> class Test:
    ...     def __repr__(self):
    ...         return 'TEST'
    ...     def __str__(self):
    ...         return 'test'
    ...
    >>> Test()
    TEST
    >>> str(Test())
    'test'
    >>> print(Test())   # 执行 print() 函数时,会先将对象转换成 str 类型,然后打印到终端
    test
    

# 关于运算

def __add__(self, other)      # 左加。执行 self + other 时,会自动调用该方法
def __radd__(self, other)     # 右加。执行 other + self 时,会自动调用该方法

def __sub__(self, other)      # 左减
def __rsub__(self, other)     # 右减

def __mul__(self, other)      # 左乘
def __rmul__(self, other)     # 右乘

def __truediv__(self, other)  # 左除
def __rtruediv__(self, other) # 右除

def __eq__(self, other)       # == other
def __ne__(self, other)       # != other
def __lt__(self, other)       # <  other
def __le__(self, other)       # <= other
def __gt__(self, other)       # >  other
def __ge__(self, other)       # >= other
  • 定义这些方法,可以改变运算符的工作逻辑,称为运算符重载。
  • 例:
    >>> class Test:
    ...     def __init__(self, num):
    ...         self.num = num
    ...     def __sub__(self, other):
    ...         return self.num - other
    ...     def __rsub__(self, other):
    ...         return other - self.num
    ...
    >>> Test(1) - 2
    -1
    >>> 2 - Test(1)
    1
    

# 关于容器

def __bool__(self)      # 执行 bool(obj) 时,会自动调用该方法
def __len__(self)       # 执行 len(obj) 时,会自动调用该方法
def __sizeof__(self)    # 用于计算对象占用的内存大小,单位为 bytes 。执行 sys.getsizeof(obj) 时,会自动调用该方法

def __getitem__(self, key)        # 执行 obj[key] 时,会自动调用该方法。如果 obj 属于序列类型,则返回 key 索引处的那个 value 。如果 obj 属于 mapping 类型,则返回 key 映射的那个 value
def __setitem__(self, key, value) # 执行 obj[key] = value 时,会自动调用该方法
def __delitem__(self, key)        # 执行 del obj[key] 时,会自动调用该方法

# 关于迭代

  • Python 中,存在 list、str、set、dict 等多种数据类型的对象。

    • 这些对象,可以存放一些元素,因此称为容器。
    • 这些容器的数据类型不同,访问方法也不同。
      • 例如 list 可通过索引获取元素,而 set 不支持索引。
      • 每使用一种容器,就需要考虑其独特的访问方法,比较麻烦。
      • 好消息是,这些容器都支持迭代,从而方便用户以同一方式获取这些容器中的元素。
  • 很多编程语言都存在迭代的概念:遍历一个容器中的所有元素,每次只获取一个元素。

    • Python 中的迭代器,属于单向只读迭代器:只能向前遍历,不能后退。
      • 由于是单向遍历,迭代器遍历完所有元素之后,就会停止迭代。
      • __next__() 循环返回元素,就可以创建无限循环的迭代器。

# 迭代器

  • 如果一个对象,实现了 __next__() 方法,则属于迭代器(iterator)。

    • 例:
      >>> class Number:
      ...     def __init__(self):
      ...         self.n = 0
      ...     def __next__(self):
      ...         if self.n < 5:
      ...             self.n += 1
      ...             return self.n
      ...         else:
      ...             raise StopIteration
      ...
      >>> n = Number()
      >>> n
      <__main__.Number object at 0x00000203210B9460>
      >>> next(n)
      1
      >>> next(n)
      2
      >>> list(n)
      [3, 4, 5]
      >>> list(n)   # 此时 n 已经迭代结束,list() 不能获取元素,因此返回一个空列表
      []
      >>> next(n)   # 此时 n 已经迭代结束,不能获取下一个元素,因此抛出异常
      StopIteration
      
  • 相关方法:

    def __next__(self)
    
    • 功能:调用该方法,会返回迭代器中的下一个元素,如果没有下一个元素,则抛出异常 StopIteration 。
    • 例如,执行 next(obj) 时,会自动调用该方法。
  • 相关函数:

    def next(iterator, default=...)
    
    • 功能:获取迭代器中的下一个元素。实际上是调用迭代器的 __next__() 方法,从而得到下一个元素。
      • 如果不存在下一个元素,且未输入 default 参数,则抛出异常 StopIteration 。
      • 如果不存在下一个元素,但输入了 default 参数,则返回 default 参数的值。
    • 例:
      >>> a = iter('Hello')
      >>> next(a)
      'H'
      >>> next(a)
      'e'
      

# 可迭代对象

  • 如果一个对象,实现了 __iter__() 方法,则属于可迭代对象(iterable)。

    • 例:
      >>> class Number:
      ...     def __iter__(self):
      ...         return iter(range(5))
      ...
      >>> n = Number()
      >>> [i for i in n]
      [0, 1, 2, 3, 4]
      >>> next(n)
      TypeError: 'Number' object is not an iterator
      
  • 相关方法:

    def __iter__(self)
    
    • 功能:调用该方法,会返回一个迭代器。
    • 例如,执行 iter(obj) 时,会自动调用该方法。
    • 例如,用 for 语句迭代一个对象时,会调用该对象的 __iter__() 方法,得到一个迭代器,然后开始迭代。
  • 相关函数:

    def iter(iterable)
    
    • 功能:将一个可迭代对象,转换成迭代器。
    • 实际上该函数并不懂如何转换,只是调用对象的 __iter__() 方法,从而得到一个迭代器。
    • 例:
      >>> iter('Hello')
      <iterator object at 0x7f19cd6e6810>
      >>> list(iter("Hello"))
      ['H', 'e', 'l', 'l', 'o']
      

# 生成器

  • 生成器(generator)是一种特殊的迭代器,它会控制函数的执行进度。

  • 如何创建?

    • 在函数(或方法)中,使用关键字 yield 指定返回值,取代关键字 return
      • 函数执行到 yield 语句时,函数会暂停执行,并返回一个生成器。
      • 当用户获取生成器的下一个元素时,会得到 yield 语句的返回值,然后使得函数继续执行。
      • 例:
        >>> def fun1():
        ...     print('A')
        ...     yield 1
        ...     print('B')
        ...     yield 2
        ...     print('C')
        ...     yield 3
        ...
        >>> g = fun1()
        >>> g
        <generator object fun1 at 0x0000015736089D60>
        >>> next(g)
        A
        1
        >>> next(g)
        B
        2
        >>> next(g)
        C
        3
        >>> next(g)
        StopIteration
        
    • 也可使用推导式的语法,创建生成器。
      • 例:
        >>> g = (i for i in range(5))
        >>> g
        <generator object <genexpr> at 0x0000015736089890>
        >>> list(g)
        [0, 1, 2, 3, 4]
        >>> next(g)
        StopIteration
        
  • 为什么使用生成器?通常是为了提高 Python 程序的性能。

    • 假设存在一个 iterable 对象,占用 100MB 内存空间。现在想将 iterable 中每个元素转换成 str 类型。

      • 如果使用列表推导式 result = [str(i) for i in iterable] ,此时 result 也会占用 100MB 内存,加上 iterable 就占用 200MB 内存。
      • 如果使用生成器推导式 result = (str(i) for i in iterable) ,此时 result 同时只获取一个元素,几乎不占用内存。
    • 除了内存开销,还需要考虑时间开销。

      • 假设遍历 iterable 对象的全部元素,耗时较久(比如几分钟)。等遍历完成之后,才开始处理这些元素,导致用户需要等一段时间才能看到处理结果。
        result = [str(i) for i in iterable] # 这一步可能耗时较久
        for i in result:
            print(i)
        
      • 而使用生成器,可以每获取一个元素,就立即处理。从而更早地让用户看到处理结果,虽然并不能减少整体耗时。
        result = (str(i) for i in iterable) # 这一步几乎没有耗时
        for i in result:
            print(i)
        
  • 综上,用户想创建一个可迭代对象时,有多种方式:

    • 只实现 __iter__() 方法。此时只能被 for 迭代,不能被 next() 迭代。例如:
      for i in obj:
          print(i)
      
    • 只实现 __next__() 方法。此时不能被 for 迭代,只能被 next() 迭代。例如:
      try:
          while True:
              print(next(obj))
      except StopIteration:
          pass
      
    • 实现 __iter__()__next__() 两个方法。此时可以被 for、next() 两种方式迭代。
    • 用生成器进行迭代。优点如下:
      • 代码更简单,不需要定义 __iter__()__next__() 两个方法,却可以被 for、next() 两种方式迭代。
      • 减少 Python 程序的开销。
  • yield from 是一种语法糖,用于迭代一个对象,对其中每个元素执行一次 yield 。例:

    >>> def fun1():
    ...     yield from range(3)
    ...
    >>> list(fun1())
    [0, 1, 2]
    

    这相当于:

    >>> def fun1():
    ...     for i in range(3):
    ...         yield i
    ...
    >>>
    >>> list(fun1())
    [0, 1, 2]
    

# 反向迭代

  • 如何对一个对象,进行反向迭代?
    • 建议让该对象实现 __reversed__() 方法,然后调用 reversed() 函数。例:
      >>> class Number:
      ...     def __iter__(self):     # 用于正向迭代
      ...         n = 0
      ...         while n < 5:
      ...             n += 1
      ...             yield n
      ...     def __reversed__(self): # 用于反向迭代
      ...         n = 5
      ...         while n > 0:
      ...             n -= 1
      ...             yield n
      ...
      >>> list(Number())
      [1, 2, 3, 4, 5]
      >>> list(reversed(Number()))
      [4, 3, 2, 1, 0]
      
    • 如果该对象没有实现 __reversed__() 方法,则只能转换成 list 对象,然后进行反向迭代:
      list(obj)[::-1]       # 转换成 list 对象之后,可通过切片,获取反向序列
      reversed(list(obj))   # 转换成 list 对象之后,存在 __reversed__() 方法
      
      • 这种做法的缺点:需要遍历全部元素,缓存在一个 list 对象中,占用一些内存、时间。

# 部分迭代

  • 一个属于迭代器的对象,不一定实现了 __getitem__() 方法,因此不一定支持索引。
  • 迭代器中的元素只能被逐个获取,如果用户只想获取部分元素,可采用以下几种方式:
    • 手动判断当前是第几个元素。
      >>> obj = range(5)
      >>> [item for index,item in enumerate(obj) if 3 <= index and index < 5]
      [3, 4]
      
    • 转换成 list 对象之后,可通过切片访问。缺点是,转换时会占用一些内存、时间。
      >>> obj = range(5)
      >>> list(obj)[3:5]
      [3, 4]
      
    • 调用 itertools.islice() 函数,获取切片:
      >>> import itertools
      >>> itertools.islice(range(5), 3, 5)
      <itertools.islice object at 0x0000014CF029BDB0>
      >>> list(_)
      [3, 4]
      

# 并行迭代

  • 如果目标对象有多个,用逗号分隔,则不会并行迭代。例如:

    >>> for i in 'hello','world':
    ...     print(i)
    ...
    hello
    world
    

    因为 for 语句会将这多个对象,先组成一个元组,然后当作一个对象迭代。相当于:

    for i in ('hello','world'):
        print(i)
    

    可以用 zip() 函数,同时取出每个对象中的第 n 项元素,实现并行迭代。例如:

    >>> for i in zip('hello','world'):
    ...     print(i)
    ...
    ('h', 'w')
    ('e', 'o')
    ('l', 'r')
    ('l', 'l')
    ('o', 'd')
    
  • 在列表推导式中,可以同时使用多个 for 语句,例如:

    >>> [x+str(y) for x in 'ABC' for y in range(3)]
    ['A0', 'A1', 'A2', 'B0', 'B1', 'B2', 'C0', 'C1', 'C2']
    

    但这并不是并行迭代,而是像多维数组一样逐层遍历,相当于:

    a = []
    for x in 'ABC':
        for y in range(3):
            list_item = x+str(y)
            a.append(list_item)
    

    可以用 zip() 函数,实现并行迭代:

    >>> iterables = 'ABC', range(3)
    >>> [i for i in zip(*iterables)]
    [('A', 0), ('B', 1), ('C', 2)]
    
  • 如果只迭代一个 iterable 对象,但希望每次获取多个元素,则可采用以下方式:

    >>> iterable = iter(range(6))
    >>> iterables = [iterable] * 4    # 将迭代器拷贝多份,这样并行迭代时,会累加迭代进度
    >>> [i for i in zip(*iterables)]  # 这里每次获取 4 个元素。如果 iterables 中任一对象为空,则会停止迭代。因此可能遗漏一些元素,没有迭代
    [(0, 1, 2, 3)]
    

# 描述器

  • 描述器用于描述一个实例变量,与 @property 类似。

    • 不过,描述器只能以类变量的形式创建,然后以实例变量的形式访问。
  • 如果一个对象,实现了以下三个方法之一,则属于描述器(descriptor)。

    def __get__(self, instance, instance_type)  # 读取该对象的值时,会自动调用该方法
    def __set__(self, instance, value)          # 赋值给该对象时,会自动调用该方法
    def __delete__(self, instance)              # 删除该对象时,会自动调用该方法
    
  • 例:读取描述器

    >>> class Number:
    ...     def __get__(self, instance, instance_type):
    ...         print(instance)
    ...         print(instance_type)
    ...         return 0
    ...
    >>> class Test:
    ...     n = Number()  # 创建类变量,指向一个描述器
    ...
    >>> Test().n  # 执行成员运算符 . 时,会发现类变量存在 __get__() 方法,于是自动调用它,并传入参数:当前对象 Test() 、当前类 Test
    <__main__.Test object at 0x000001C922BBF9D0>
    <class '__main__.Test'>
    0
    >>> Test.n    # 如果通过类名,访问类变量,则传入的 instance 参数为 None
    None
    <class '__main__.Test'>
    0
    
  • 例:读、写描述器

    >>> class Number:
    ...     def __init__(self):
    ...         self.n = 0
    ...     def __get__(self, instance, instance_type):
    ...         if instance is None:
    ...             return self
    ...         else:
    ...             return self.n
    ...     def __set__(self, instance, value):
    ...         if isinstance(value, int):
    ...             self.n = value
    ...         else:
    ...             raise TypeError('Expected value to be an int')
    ...
    >>> class Test:
    ...     n = Number()
    ...
    >>> Test().n
    0
    >>> Test().n = 1
    >>> Test().n = ''
    TypeError: Expected value to be an int
    >>> Test.n
    <__main__.Number object at 0x000001C922BC5730>
    
  • 例:用描述器实现一个装饰器,用于将某个实例方法转换成实例变量

    >>> class FixedProperty:
    ...     def __init__(self, method):
    ...         self.method = method    # 存储原本的实例方法
    ...     def __get__(self, instance, instance_type):
    ...         if instance is None:
    ...             return self
    ...         else:
    ...             value = self.method(instance)  # 调用一次实例方法,得到它的返回值
    ...             setattr(instance, self.method.__name__, value) # 给 instance 添加一个实例变量,名称与 method 相同,因此覆盖原本的 method
    ...             return value
    ...
    >>> import random
    >>> class Test:
    ...     @FixedProperty
    ...     def num(self):
    ...         return random.randint(1, 10)
    ...
    >>> t = Test()
    >>> t.num
    5
    >>> t.num   # 该实例变量的返回值是固定的。如果删除上面的 setattr() 一行代码,则返回值不固定,与 @property 装饰器的效果相同
    5
    
  • 例:用描述器实现一个装饰器,在赋值时进行类型检查

    >>> class TypeCheck:
    ...     expected_type = type(None)
    ...     def __init__(self, attr, **kwargs): # 这里使用 **kwargs ,因此子类调用该方法时,可以传入任意个关键字参数
    ...         self.attr = attr
    ...         for k, v in kwargs.items():
    ...             setattr(self, k, v)
    ...     def __set__(self, instance, value):
    ...         if not isinstance(value, self.expected_type):
    ...             raise TypeError('Expected {} to be of data type: {}'.format(self.attr, self.expected_type))
    ...         # setattr(instance, self.attr, value) # 这行代码会调用当前 __set__() 方法,导致无限循环
    ...         instance.__dict__[self.attr] = value  # 为了避免无限循环,应该采用这种方式赋值
    ...
    >>> class String(TypeCheck):
    ...     expected_type = str
    ...
    >>> class Price(TypeCheck):
    ...     expected_type = (int, float)
    ...
    >>> class Book:
    ...     name = String('name')
    ...     price = Price('price')
    ...     def __init__(self, name, price):
    ...         self.name = name
    ...         self.price = price
    ...
    >>> Book('test', '')
    TypeError: Expected price to be of data type: (<class 'int'>, <class 'float'>)
    >>> Book('test', 10)
    <__main__.Book object at 0x000001B091F5E520>
    >>> _.name = 10
    TypeError: Expected name to be of data type: <class 'str'>
    

    进一步地,在赋值时检查范围:

    >>> class RangeCheck(TypeCheck):
    ...     def __init__(self, attr, **kwargs):
    ...         if 'min_value' not in kwargs:
    ...             raise TypeError('Expected the argument: min_value')
    ...         if 'max_value' not in kwargs:
    ...             raise TypeError('Expected the argument: max_value')
    ...         if not kwargs['min_value'] <= kwargs['max_value']:
    ...             raise ValueError('Expected two arguments: min_value <= max_value')
    ...         super().__init__(attr, **kwargs)
    ...     def __set__(self, instance, value):
    ...         if value < self.min_value or value > self.max_value:
    ...             raise TypeError('Expected range: {} <= {} <= {}'.format(self.min_value, self.attr, self.max_value))
    ...         super().__set__(instance, value)
    ...
    >>> class LimitedPrice(Price, RangeCheck):
    ...     pass
    ...
    >>> class Book:
    ...     name = String('name', length=10)
    ...     price = LimitedPrice('price', min_value=1, max_value=100)
    ...     def __init__(self, name, price):
    ...         self.name = name
    ...         self.price = price
    ...
    >>> Book('test', 10)
    <__main__.Book object at 0x000001B091F33BE0>
    >>> _.price = 200
    TypeError: Expected range: 1 <= price <= 100
    

    什么时候进行类型检查?

    • Python 的初衷是一门动态语言,不对变量进行类型检查。
    • 进行类型检查,缺点是使得代码更繁琐、编程工作量更大。优点是保证变量的取值符合预期,减少代码的故障风险。
    • 折中的做法是,程序通过 API 与外部交互时,不信任外部输入的变量,因此进行类型检查。程序内部,各个函数相互调用时,不进行类型检查。