# 类
# 创建
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
定义语句,就会创建一个类。
- 当 Python 解释器执行完
- 例:
>>> 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
- :私有成员。名称以两个下划线
__
开头,只能在当前类的内部访问。
- :私有成员。名称以两个下划线
- public
例:
>>> 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 解释器会创建一个名为 self.<variable> 的实例变量
- 定义类之后,
- 可以通过
<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 列表查找父类,每个父类最多被调用一次。
- 不必手动输入父类名称。例如在 User 类中执行
- 例:
>>> 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++ 可通过以下方式,实现多态性,即:
- 方法重写(override):子类可以重写(又称为覆盖)从父类继承的方法的内容,但方法头不变,即返回类型、方法名、形参列表不变。
- 方法重载(overload):在一个类及其子类中,可以定义名称相同、形参列表不同的多个方法。调用该名称的方法时,实参列表与哪个形参列表匹配,就调用哪个方法。
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 类:>>> Test = type('Test', (str,), {'method1': print}) >>> Test <class '__main__.Test'> >>> Test().method1('hello') hello
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
- 除了使用 type 类,用户也可以创建一些自定义的元类。
综上,在 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 。