#

# 创建

  • Python 既支持创建函数(function),也支持创建类(class),从而实现面向对象编程。

    • 使用类,可以创建一组相似的对象(object),作为该类的不同实例(instance)。
    • 例如:Python 中的 int 数据类型,定义为一个名为 int 的类。1、2、3 等整数,是 int 类的不同实例。
  • Python 中,使用关键字 class 可以定义一个类。

    • 一般语法为:
      def class_name:
          def method1(args):
              statement_block
          def method2(args):
              statement_block
      
      • 当 Python 解释器执行完 class 定义语句,就会创建一个类。
    • 例:
      >>> class Test:
      ...     def __init__(self, a=None, b=None):
      ...         self.a = a    # 这里将输入参数 a ,赋值给实例变量 self.a
      ...         self.b = b
      ...         print(self)
      ...         print('an instance has been initialized.')
      ...     def method1(self):
      ...         print(self.a, self.b)
      ...
      >>> t1 = Test(a=1)   # 创建一个 Test 类的实例,赋值给变量 t1
      <__main__.Test object at 0x0000029AA65C5760>
      an instance has been initialized.
      >>> t1              # 变量 t1 指向一个实例
      <__main__.Test object at 0x0000029AA65C5760>
      >>> t2 = Test()     # 再创建一个实例,可见它拥有不同的 object id
      <__main__.Test object at 0x0000029AA63B9430>
      an instance has been initialized.
      >>> t1 is t2        # t1 与 t2 是不同的对象
      False
      >>> t1.method1()    # 调用 t1 实例的一个方法
      1 None
      
      • 调用 class_name() 即可创建某个类的一个实例。
      • 调用 class_name(实参列表) 可以在创建实例的同时,输入一些参数。这些实参,会被传入 __init__() 方法。
      • 创建实例时,不管是否传入参数,都会执行一次 __init__() 方法,进行初始化。
  • 用户调用 class_name() 可以创建该类的一个实例。每个实例的创建过程如下:

    • 自动调用一次 class_name.__new__() 方法,创建实例。
    • 自动调用一次 class_name.__init__() 方法,初始化实例,比如创建一些实例变量。
    • 例:
      >>> class Test:
      ...     def __new__(cls):
      ...         return super().__new__(cls) # 通常调用父类的 __new__() 方法来创建对象,这样用户不必亲自创建对象、分配内存
      ...     def __init__(self, a, b):
      ...         self.a = a
      ...         self.b = b
      ...         # super().__init__()        # 可选调用父类的 __init__() 方法
      ...
      >>> Test()      # 这里创建实例失败,报错说 __init__() 方法需要传入两个参数
      TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'
      >>> Test(1, 2)  # 这里创建实例失败,报错说 __new__() 方法传入了多余的参数
      TypeError: __new__() takes 1 positional argument but 3 were given
      
      • 用户调用 class_name(实参列表) 创建一个实例时,实参列表会先传入 __new__() 方法,然后传入 __init__() 方法,因此这两个方法的形参列表需要一致,或者兼容。
      • 上例中,将 def __new__(cls): 改为 def __new__(cls, *args, **kwargs): ,即可接收实参列表,从而能创建一个对象。

# 成员

  • C 语言中,用关键字 struct 可以定义一个结构体,将多个变量封装在一起,作为一种高级的数据结构。
  • Python 中,用关键字 class 可以定义一个类,将多个变量、函数封装在一起,作为一种高级的数据结构。
    • 一个类中,可以定义任意个变量。分为几种:
      • 实例变量(instance variable)
      • 类变量(class variable)
    • 一个类中,可以定义多个函数,称为该类的方法(method)。分为几种:
      • 实例方法
      • 类方法
      • 静态方法
    • 变量、方法,都属于该类的成员(member),又称为属性(atrribute)。
      • 可通过 class_name.member_name 的形式,访问类中的某个成员。

# 访问控制

  • 一个类的成员,不一定允许被外部访问。根据访问权限的不同,可将成员分为几类:

    • public
      • :公有成员。名称不以下划线 _ 开头,可以被任何对象访问,可以通过 import 语句导入其它脚本。
    • protected
      • :受保护成员。名称以一个下划线 _ 开头,只能在当前类或子类的内部访问,或通过实例访问。
    • private
      • :私有成员。名称以两个下划线 __ 开头,只能在当前类的内部访问。
  • 例:

    >>> class Test:
    ...     a = 0
    ...     _b = 0
    ...     __c = 0
    ...
    >>> Test.a
    0
    >>> Test._b
    0
    >>> Test.__c
    AttributeError: 'Test' object has no attribute '__c'
    
  • Python 属于动态语言,允许给一个已经定义的类,添加属性。

    • 例:添加实例变量
      >>> class Test:
      ...     pass
      ...
      >>> t = Test()
      >>> t.a
      AttributeError: 'Test' object has no attribute 'a'
      >>> t.a = 0
      >>> t.a
      0
      
    • 例:添加类变量
      >>> Test.a
      AttributeError: type object 'Test' has no attribute 'a'
      >>> Test.a = 0
      >>> Test.__a = 0   # 私有属性不能在类外被访问,但这里还是赋值成功了
      
    • 例:添加方法
      >>> def method1(self, x):
      ...     return x
      ...
      >>> Test.method1 = method1
      >>> t.method1('hello')   # 该类的所有实例,都可以访问到新方法
      'hello'
      
    • 因此,给一个类或实例,的某个属性进行赋值时,即使该属性不存在,也可以赋值成功,这可能引发 bug 。
      • 可以声明内置变量 __slots__ ,从而限制所有实例变量的名称。

# 变量

# 实例变量

  • 用户根据 class 创建多个实例时,在 Python 底层,会自动为每个实例创建一个专用的命名空间,用于存储实例变量等标识符。

    • 每个名称的实例变量,会在每个实例的命名空间中,分别存储一份,互不影响。
  • 如何访问?

    • 定义类时,只能在方法内访问。一般通过 self.<variable> 的形式访问。
    • 定义类之后,一般通过 <instance>.<variable> 的形式访问。
  • 例:

    >>> class Test:
    ...     def __init__(self, num=0):
    ...         self.num = num
    ...     def get_num(self):
    ...         return self.num
    ...
    >>> t = Test(1)
    >>> t.num
    1
    >>> t.get_num()
    1
    

# 类变量

  • 用户定义一个 class 时,在 Python 底层,会自动为该 class 创建一个命名空间,用于存储类变量、方法等标识符。

    • 在该 class 的命名空间中,可以存储多个名称的类变量。
    • 每个名称的类变量,只存储一份值,被该 class 的所有实例共享。
  • 如何访问?

    • 定义类时,
      • 在方法外创建的变量,都会被视作类变量。
      • 在方法内,可以通过以下几种形式访问。
        cls.<variable>      # 推荐采用这种形式
        <class>.<variable>
        self.__class__.<variable>
        self.<variable>     # 此时不能存在同名的实例变量,才能读取该类变量。而且不能赋值,否则 Python 解释器会创建一个同名的实例变量
        
    • 定义类之后,
      • 可以通过 <class>.<variable> 的形式访问。
      • 也可以通过 <instance>.<variable> 访问,但不能与实例变量同名,否则优先访问实例变量。
  • 例:

    >>> class Test:
    ...     total = 0
    ...     def __init__(self, num=0):
    ...         self.num = num
    ...         Test.total += num
    ...     def get_total(cls):
    ...         return cls.total
    ...
    >>> t1 = Test(1)
    >>> t1.get_total()
    1
    >>> t1.total
    1
    >>> Test.total
    1
    >>> t2 = Test(2)
    >>> Test.total    # 不同实例,修改的是同一个类变量
    3
    

# 方法

# 实例方法

  • 假设一个名为 Test 的类中,定义了多个 method 。从该类创建了两个实例 t1、t2 。

    • t1、t2 都属于 Test 类,都可以调用 Test 类的所有 method 。
    • t1、t2 调用同一个 method 时,效果可能不同。因为有的 method ,会根据当前的实例的不同,执行不同的操作,这种 method 称为实例方法。
  • 如何定义?

    • 在类中,像函数一样定义,但第一个形参是特殊的,通常命名为 self
      • self 指向当前实例,像 C++ 中使用的 this 指针。
      • self 有什么用处?通过它,可以找到当前实例绑定的命名空间,从而访问该命名空间中存储的各个标识符,比如各个实例变量、实例方法。
      • self 取值从何而来?调用实例方法时,Python 解释器会自动传入当前实例的引用,作为第一个实参,赋值给 self 形参。
  • 如何调用?

    • 一般通过 <instance>.<method> 的形式调用。
    • 也可通过 <class>.<method> 的形式调用,此时需要手动传入第一个参数 self 。
  • 例:

    >>> class Test:
    ...     def __init__(self, num):
    ...         self.num = num
    ...     def get_num(self):
    ...         return self.num
    ...
    >>> t1 = Test(1)
    >>> t1.get_num()
    1
    >>> Test.get_num()
    TypeError: get_num() missing 1 required positional argument: 'self'
    >>> Test.get_num(t1)
    1
    

# 类方法

  • 如何定义?

    • 第一个形参是特殊的,通常命名为 cls
      • cls 指向当前类,相当于 self.__class__
      • 调用类方法时,Python 解释器会自动传入当前类的引用,作为第一个实参,赋值给 cls 形参。
    • 而且要使用 Python 的一个内置装饰器 @classmethod ,将当前方法声明为类方法。
  • 如何调用?

    • 一般通过 <class>.<method> 的形式调用。
    • 也可通过 <instance>.<method> 的形式调用。
  • 例:

    >>> class Test:
    ...     sum = 0
    ...     @classmethod
    ...     def get_sum(cls):
    ...         return cls.sum
    ...
    >>>
    >>> Test.get_sum()
    0
    >>> Test.sum = 1
    >>> Test.get_sum()
    1
    >>> Test().get_sum()
    1
    

    可改用以下方式使用装饰器,不过麻烦一些:

    class Test:
        sum = 0
        def get_sum(cls):
            return cls.sum
        get_sum = classmethod(get_sum)
    

# 静态方法

  • 如何定义?

    • 没有特殊的形参,不必传入 self、cls 参数。像一个普通的函数。
    • 而且要使用 Python 的一个内置装饰器 @staticmethod ,将当前方法声明为静态方法。
  • 如何调用?

    • 可以通过 <class>.<method><instance>.<method> 的形式调用。
  • 例:

    >>> class Test:
    ...     @staticmethod
    ...     def sum(a, b):
    ...         return a+b
    ...
    >>> Test.sum(1, 2)
    3
    >>> Test().sum(1, 2)
    3
    

# @property

  • @property 是 Python 的一个内置装饰器,用于将类的某个方法,转换成实例变量。

    • 优点:当用户读取该变量时,会自动调用该方法,从而执行某些代码,例如记录日志、给变量赋值时检查数据类型。
    • 例:
      >>> class Test:
      ...     def __init__(self):
      ...         self._sum = 0       # 这里实例变量名为 _sum ,因为不能与实例方法 sum 同名
      ...     @property               # 这个装饰器的作用是,将 sum 方法转换成一个实例变量,允许读取
      ...     def sum(self):
      ...         return self._sum
      ...     @sum.setter             # 如果未定义这个装饰器 @变量名.setter ,则变量 sum 不能被赋值
      ...     def sum(self, value):   # 两个方法都名为 sum ,才能映射到同一个 @property 变量
      ...         self._sum = value
      ...     @sum.deleter            # 如果未定义这个装饰器 @变量名.deleter ,则变量 sum 不能被删除
      ...     def sum(self):
      ...         raise AttributeError('Can not delete this attribute')
      ...
      >>> t1 = Test()
      >>> t1.sum
      0
      >>> t1.sum = 1
      >>> t1.sum
      1
      >>> t1.sum()      # 此时 sum 是一个变量,不能当作方法调用
      TypeError: 'int' object is not callable
      
  • 总之,如果用户希望在对实例变量进行读、写时,自动执行某些代码,则有多种方案:

    • 使用 @property 变量。
    • 使用 __getattribute__()__setattr__() 方法。
    • 使用描述器。

# 继承

# 单继承

  • 定义一个 class 时,可以继承另一个 class 。

    • 前者称为子类(subclass)、派生类(derived class)。
    • 后者称为父类(parent class)、超类(super class)、基类(base class)。
    • 例:
      >>> class Person:
      ...     num = 0
      ...
      >>> class Man(Person):  # 创建一个 Man 类,继承 Person 类
      ...     pass
      ...
      
    • 上例中,Man 是 Person 的子类。Man 也可以拥有自己的子类,子类还可以拥有更深层的子类。
      • 狭义上,提到子类时,是指直接子类。提到父类时,是指直接父类。
      • 广义上,提到子类时,包括直接子类、子孙类。提到父类时,包括直接父类、祖先类。
  • 子类会继承父类的成员(主要是类变量、方法),但内置变量、内置方法不一定会继承。

    • 如下,子类的类变量,与父类的类变量,指向同一个对象。
      >>> Person.num, Man.num
      (0, 0)
      >>> Person.num is Man.num
      True
      
    • 如果父类的类变量被赋值、改值,则子类的类变量也会变化,此时两个类变量依然指向同一个对象。
      >>> Person.num = []
      >>> Person.num, Man.num
      ([], [])
      >>> Person.num is Man.num
      True
      
      >>> Person.num.append(0)
      >>> Person.num, Man.num
      ([0], [0])
      >>> Person.num is Man.num
      True
      
    • 如果子类的类变量被改值,则父类的类变量也会变化,此时两个类变量依然指向同一个对象。
      >>> Man.num.append(1)
      >>> Person.num, Man.num
      ([0, 1], [0, 1])
      >>> Person.num is Man.num
      True
      
    • 如果子类的类变量被赋值,则会指向另一个对象,从此与父类的类变量取消联系。
      >>> Man.num = []
      >>> Person.num, Man.num
      ([0, 1], [])
      >>> Person.num is Man.num
      False
      
  • 执行完 class 定义语句之后,如果重新定义父类,则子类不受影响,依然继承旧的父类。

    >>> class Person:
    ...     name = ''
    ...
    >>> Man.name
    AttributeError: type object 'Man' has no attribute 'name'
    

# 多继承

  • 定义一个 class 时,可以同时继承多个父类。

    • 例:
      class User(Man, Woman):
          pass
      
  • 多继承时,如果多个父类,存在同名的成员,则应该继承哪个父类的?Python 会根据 MRO 列表,按顺序查找。

    • 例:
      >>> class Person:
      ...     name = 'Person'
      ...
      >>> class Man(Person):
      ...     name = 'Man'
      ...
      >>> class Woman(Person):
      ...     name = 'Woman'
      ...
      >>> class User(Man, Woman):
      ...     pass
      ...
      >>> User.name     # 这里继承的是 Man 类的成员,因为 Man 类在 MRO 列表中排序靠前
      'Man'
      >>> User.mro()    # 查看当前类的 MRO 列表
      [<class '__main__.User'>, <class '__main__.Man'>, <class '__main__.Woman'>, <class '__main__.Person'>, <class 'object'>]
      
    • 定义一个类时,Python 会自动确定其 MRO(Method Resolution Order,方法继承顺序):
      • 在一个列表中,记录当前类及其父类(包括祖先类)。
      • 子类排序在前,父类排序在后,每个类只出现一次。
    • 子类调用父类的一个成员时,会按 MRO 列表的顺序,从左往右,逐一检查各个父类。
      • 如果当前 class 找到了指定名称的成员,则立即访问它。并停止查找,不再检查其它 class 。
      • 如果当前 class 没找到,则到下一个 class 中查找。
  • 如何调用父类的一个方法?

    • 可以执行 父类名.方法名(self)
      • 缺点:
        • 需要手动输入父类的名称,比较麻烦。
        • 可能重复调用某个父类的方法。
      • 例:
        >>> class Person:
        ...     def __init__(self):
        ...         print('\t\t Person.__init__ start')
        ...         print('\t\t Person.__init__ end')
        ...
        >>> class Man(Person):
        ...     def __init__(self):
        ...         print('\t Man.__init__ start')
        ...         Person.__init__(self)
        ...         print('\t Man.__init__ end')
        ...
        >>> class Woman(Person):
        ...     def __init__(self):
        ...         print('\t Woman.__init__ start')
        ...         Person.__init__(self)
        ...         print('\t Woman.__init__ end')
        ...
        >>> class User(Man, Woman):
        ...     def __init__(self):
        ...         print('User.__init__ start')
        ...         Man.__init__(self)
        ...         Woman.__init__(self)
        ...         print('User.__init__ end')
        ...
        >>> user = User()
        User.__init__ start
                Man.__init__ start
                        Person.__init__ start
                        Person.__init__ end
                Man.__init__ end
                Woman.__init__ start
                        Person.__init__ start   # 这里重复调用了 Person.__init__
                        Person.__init__ end
                Woman.__init__ end
        User.__init__ end
        
    • 更推荐执行 super().方法名() ,即通过内置函数 super() 自动找到当前类的父类。
      • 优点:
        • 不必手动输入父类名称。例如在 User 类中执行 super().__init__() ,相当于执行 super(User, self).__init__() 。这会自动找到 User 的父类,调用其方法,传入当前的 self 参数。
        • 不会重复调用某个父类的方法。因为 super() 会根据 MRO 列表查找父类,每个父类最多被调用一次。
      • 例:
        >>> class Person:
        ...     def __init__(self):
        ...         print('\t\t Person.__init__ start')
        ...         print('\t\t Person.__init__ end')
        ...
        >>> class Man(Person):
        ...     def __init__(self):
        ...         print('\t Man.__init__ start')
        ...         super().__init__()
        ...         print('\t Man.__init__ end')
        ...
        >>> class Woman(Person):
        ...     def __init__(self):
        ...         print('\t Woman.__init__ start')
        ...         super().__init__()
        ...         print('\t Woman.__init__ end')
        ...
        >>> class User(Man, Woman):
        ...     def __init__(self):
        ...         print('User.__init__ start')
        ...         super().__init__()    #
        ...         print('User.__init__ end')
        ...
        >>> user = User()
        User.__init__ start
                Man.__init__ start      # 在 User 类中执行 super() ,会发现 MRO 列表中最靠前的是 Man 类,于是调用 Man.__init__
                Woman.__init__ start    # 在 Man 类中执行 super() ,指向的是 Woman 类,而不是 Person 类。因为此时 self 参数来自 User 类,在其 MRO 列表中,位于 Man 类之后的是 Woman 类
                        Person.__init__ start
                        Person.__init__ end
                Woman.__init__ end
                Man.__init__ end
        User.__init__ end
        

# 多态性

  • C++、Java 主要有两种方式实现多态性:

    • 方法重写(override)
      • :在一个类及其子类中,如果定义多个方法,它们的名称、形参列表都相同,则只会保留最后一个定义的方法,之前的同名方法会被重写、覆盖。
      • 例如父类的一个方法,执行了某个操作。子类可以重写该方法,执行不同的操作。
    • 方法重载(overload)
      • :在一个类及其子类中,如果定义多个方法,它们的名称相同、形参列表不同,则这些方法都会保留。
      • 用户调用该名称的方法时,实参列表与哪个方法的形参列表匹配,就会调用哪个方法。
  • Python 规定,在一个类及其子类中,如果定义多个方法,它们的名称相同(不考虑形参列表),则只会保留最后一个定义的方法。

    • 因此 Python 支持 override 。
      • 如果子类定义了一个方法,与父类的某个方法同名,则会覆盖父类的这个方法。
      • 此时,可以通过 super().方法名() 来调用父类的方法。
    • 因此 Python 不支持 overload 。如下:
      >>> class Test:
      ...     def method1(self, x):
      ...         pass
      ...     def method1(self, x, y):
      ...         pass
      ...
      >>> Test().method1()    # 从报错可以看出,此时调用的是第二个方法
      TypeError: method1() missing 2 required positional arguments: 'x' and 'y'
      

# 元类

  • Python3 中,大部分类,都是从 type 类创建的实例。

    • 用户可以调用函数 type(object) ,查看一个对象的类型,即该对象是从哪个 class 创建的实例。
      >>> type(1)       # 数字 1 ,是从 int 类创建的实例
      <class 'int'>
      >>> type(int)     # int 类,是从 type 类创建的实例
      <class 'type'>
      
    • 用户使用关键字 class 可以定义一个类。当 Python 解释器执行到 class 定义语句时,实际上会调用 type() 来创建该类。
      class type(name: str, bases: Tuple[type, ...], dict: Dict[str, Any]) -> a new type
      
      • 参数 name 表示要创建的类名。
      • 参数 bases 表示继承哪些父类(这里是指直接父类,不包括祖先类)。
      • 参数 dict 表示该类包含哪些方法,分别绑定哪些函数。
    • 用户也可以主动调用 type() 创建一个类:
      >>> Test = type('Test', (str,), {'method1': print})
      >>> Test
      <class '__main__.Test'>
      >>> Test().method1('hello')
      hello
      
      相当于按以下方式创建的 Test 类:
      class Test(str):
          def method1(self, *args, **kwargs):
              print(*args, **kwargs)
      
  • type 类的父类是 object 类。因此,从 type 类创建的所有类,都会继承 object 类。

    • 例:
      >>> type.__bases__
      (<class 'object'>,)
      >>> int.__bases__   # int 类,是从 type 类创建的实例。而 type 类的父类是 object 类,因此 int 类的父类是 object 类
      (<class 'object'>,)
      >>> object()        # 创建一个 object 类的对象
      <object object at 0x00000280798574E0>
      
    • 例:定义一个普通类
      >>> class Person:
      ...     pass
      ...
      >>> Person.__bases__  # 即使 Person 类没有声明父类,也会继承 object 类。因为 Person 类是从 type 类创建的实例
      (<class 'object'>,)
      >>> Person.__mro__
      (<class '__main__.Person'>, <class 'object'>)
      
  • Python 中,type 类被称为元类(metaclass),因为它负责创建普通类,是一种更抽象的类。

    • 除了使用 type 类,用户也可以创建一些自定义的元类。
      • 例如创建一个元类,用于初始化所有普通类,给它们添加打印日志的方法。
    • 存在多个元类时,Python 解释器如何知道,应该采用哪个元类?
      • 定义一个类时,如果指定了某个元类,则采用该元类。
      • 定义一个类时,如果没指定元类,则采用父类的元类。如果没声明父类,则调用 type() 来创建当前类。
    • 例:
      >>> class Mymetaclass(type):                    # 创建一个元类,继承 type 类
      ...     def  __new__(cls, name, bases, attrs):  # 形参列表模仿 type 类
      ...         attrs['method1'] = lambda self, x: print(x)
      ...         return super().__new__(cls, name, bases, attrs)
      ...
      >>> class Test(metaclass=Mymetaclass):  # 采用一个元类
      ...     pass
      ...
      >>> Test().method1('hello')
      hello
      
  • 综上,在 Python 中,想给一个类添加成员,有几种方法:

    • 在定义这个类时,添加成员。
    • 在定义这个类之后,添加成员。但可能被内置变量 __slots__ 拒绝。
    • 在定义这个类之前,在父类中添加成员,被子类继承。
    • 在定义这个类之前,通过元类添加成员。

# 抽象类

  • C++ 中可以将一个类,声明为抽象类(abstract class,简称为 abc)。Python 原生语法中,不存在抽象类。但可以调用标准库 abc 来实现这一需求。

    • 一个抽象类中,可以包含多个抽象方法,和非抽象方法。非抽象类中,不允许包含抽象方法。
    • 一个抽象类中,如果包含抽象方法,则该类不能直接被实例化,只能用作基类,被其它类继承。
    • 当前类继承一个抽象类时,需要 override 其所有抽象方法,除非当前类也是抽象类。
  • 例:创建一个抽象类

    >>> from abc import ABCMeta, abstractmethod
    >>> class Test(metaclass=ABCMeta):  # 设置元类为 ABCMeta ,从而将当前类定义为抽象类
    ...     @abstractmethod             # 声明抽象方法
    ...     def method1(self):
    ...         pass
    ...     @staticmethod
    ...     @abstractmethod             # 它可以与其它装饰器组合使用,但必须位于最下方,作为第一个生效的装饰器
    ...     def method2():
    ...         pass
    ...
    >>> Test()
    TypeError: Can't instantiate abstract class Test with abstract method method1
    
  • 例如,标准库 collections 提供了一些抽象类,比如 Iterator、Iterable 。