阅读须知

  • 阅读本材料需要一定的Python基础知识
  • 文中实例基于Python 3
  • 本文使用Markdown语言编写,使用Typora编辑器排版
  • 部分案例来自互联网,无法一一标注,向无偿贡献知识的Geeker们表示感谢

Python中,可以通过class关键字定义自己的类,然后通过自定义的类对象类创建实例对象。例如,下面创建了一个Student的类,并且实现了这个类的初始化函数”init“:

  1. class Student(object):
  2. count = 0
  3. books = []
  4. def __init__(self, name, age):
  5. self.name = name
  6. self.age = age
  7. pass

接下来就通过上面的Student类来看看Python中类的相关内容。

1. 数据属性

在上面的Student类中,”count”、”books”、”name”和”age”都被称为类的数据属性,但是它们又分为类数据属性和实例数据属性。

1.1 类中属性查找机制

Python中属性的获取存在一个向上查找机制,举个例子做说明:

  1. class Human(object):
  2. life = 80
  3. male = Human()
  4. female = Human()
  5. # 情形1
  6. print(male.life, female.life, Human.life)
  7. # 情形2
  8. female.life += 5
  9. print(male.life, female.life, Human.life)
  10. # 情形3
  11. Human.life += 10
  12. print(male.life, female.life, Human.life)

情形1的结果是:80 80 80
情形2的结果是:80 85 80
情形3的结果是:90 85 90

当调用Human.life时,直接从human获取其属性life。但是情形1中调用male.life时,Python按照从malehuman的顺序由下到上查找属性life的。值得注意的这时候male是没有属性life的,于是,Python到类human中去查找,成功找到,并显示出来。所以,从现象上来看,human的属性life确实是共享给其所有实例的。

第二个操作是属性设置,即female.life += 5。当发生属性设置的时候,female这个实例对象没有属性life,因此会为自身动态添加一个属性life。由于从对象的角度,类对象和实例对象属于两个独立的对象,所以,这个life属性只属于female,也就是说,这时候类对象Human和实例对象female各自有一个属性life

综上:

  • Python中属性的获取是按照从下到上的顺序来查找属性;
  • Python中的类和实例是两个完全独立的对象;
  • Python中的属性设置是针对对象本身进行的;

1.2 类中属性访问机制

  • 类数据属性属于类本身,可以通过类名进行访问/修改
  • 类数据属性也可以被类的所有实例访问/修改
  • 在类定义之后,通过类名动态添加类数据属性,新增的类属性也被类和所有实例共有
  • 实例数据属性只能通过实例访问
  • 在实例生成后,还可以动态添加实例数据属性,但是这些实例数据属性只属于该实例

1.3 特殊的类属性

特殊类属性 意义
name 类的名字(字符串)
doc 类的文档字符串
bases 类的所有父类组成的元组
dict 类的属性组成的字典
module 类所属的模块
class 类对象的类型
  1. class Student(object):
  2. '''This is a Student class'''
  3. count = 0
  4. books = []
  5. def __init__(self, name, age):
  6. self.name = name
  7. self.age = age
  8. pass
  9. print(Student.__name__)
  10. print(Student.__doc__)
  11. print(Student.__bases__)
  12. print(Student.__dict__)
  13. print(Student.__module__)
  14. print(Student.__class__)

2. 方法

有些数据(特性)和函数(方法)是类本身的一部分,还有一些是由类创建的实例的一部分。

2.1 实例方法

在类的定义中,以 self 作为第一个参数的方法都是实例方法(instance method),它们在创建自定义类时最常用。实例方法的首个参数是self,当它被调用时,Python 会把调用该方法的对象作为self参数传入。

  1. class Student(object):
  2. '''
  3. this is a Student class
  4. '''
  5. count = 0
  6. books = []
  7. def __init__(self, name, age):
  8. self.name = name
  9. self.age = age
  10. pass
  11. wei = Student('weimengxin', 28)
  12. wei.age
  13. Student.age
  14. AttributeError: type object 'Student' has no attribute 'age'

实例方法只能通过类实例进行调用,这时候self就代表这个类实例本身,通过self可以直接访问实例的属性。

  1. class Student(object):
  2. count = 0
  3. books = []
  4. def __init__(self, name, age):
  5. self.name = name
  6. self.age = age
  7. pass
  8. # 例1:
  9. >>> wei = Student('wei', 27) # 调用实例方法
  10. >>> wei.books = ['Python'] # 赋值调整实例属性
  11. >>> Student.books # 访问类属性
  12. []
  13. # 例2:
  14. >>> del wei.books
  15. >>> wei.books.append('Python') # 调用方法修改实例属性
  16. >>> Student.books
  17. ['Python']

通过上述例子,可以了解:

  • 当通过实例赋值修改books属性的时候,都将为实例新建一个实例属性,不影响类;
  • 当通过实例调用相应方法修改books的属性,将修改wei.books指向的内存地址(即Student.books),此时修改实例属性影响到类。

  • 总结:

    • 虽然通过实例可以访问类属性,但是不建议这么做,最好还是通过类名来访问类属性
    • 通过实例修改类的属性,可能带来属性隐藏的问题
    • 实例方法创造的对象,只能通过实例来调用,无法通过类调用

2.2 类方法

如果现在我们想写一些仅仅与类交互而不是和实例交互的方法会怎么样呢? 我们可以在类外面写一个简单的方法来做这些:

  1. # Kls类外部函数get_no_of_instances访问Kls类no_inst属性,记录了Kls类生成实例的个数
  2. # 显然get_no_of_instances函数,只与Kls有交互,与实例无关
  3. def get_no_of_instances(cls_obj):
  4. return cls_obj.no_inst
  5. class Kls(object):
  6. no_inst = 0
  7. def __init__(self):
  8. Kls.no_inst = Kls.no_inst + 1
  9. ik1 = Kls()
  10. ik2 = Kls()
  11. print(get_no_of_instances(Kls))

或者这样:

  1. # 功能与上面相同,不同之处在于通过实例访问Kls类,调取类属性
  2. def iget_no_of_instance(ins_obj):
  3. return ins_obj.__class__.no_inst
  4. class Kls(object):
  5. no_inst = 0
  6. def __init__(self):
  7. Kls.no_inst = Kls.no_inst + 1
  8. ik1 = Kls()
  9. ik2 = Kls()
  10. print(iget_no_of_instance(ik1))

上述做法,扩散了类代码的关系到类定义的外面,以后代码维护就变得异常困难。此时,类方法就应运而生。类方法以cls作为第一个参数,cls表示类本身,定义时使用@classmethod装饰器。通过cls可以访问类的相关属性:

  1. # 通过类方法,代码更加规范和优美
  2. class Kls(object):
  3. no_inst = 0
  4. def __init__(self):
  5. Kls.no_inst = Kls.no_inst + 1
  6. @classmethod
  7. def get_no_of_instance(cls_obj):
  8. return cls_obj.no_inst
  9. ik1 = Kls()
  10. ik2 = Kls()
  11. print(ik1.get_no_of_instance())
  12. print(Kls.get_no_of_instance())
  • 类方法可以简单的看成,在类中实现“自己调用自己”
  • 这样的好处是: 不管这个方式是从实例调用还是从类调用,它都用第一个参数把类传递过来
  • 同时说明,实例和类都可以访问类方法。

2.3 静态方法

经常有一些跟类有关系的功能但在运行时又不需要实例和类参与的情况下需要用到静态方法。比如更改环境变量或者修改其他类的属性等能用到静态方法。这种情况可以直接用函数解决, 但这样同样会扩散类内部的代码,造成维护困难。比如:

  1. # 下例中checkind函数可以看成一个常数,不需要实例和类参与,建议使用静态方法
  2. IND = 'ON'
  3. def checkind():
  4. return (IND == 'ON')
  5. class Kls(object):
  6. def __init__(self, data):
  7. self.data = data
  8. def do_reset(self):
  9. if checkind():
  10. print('Reset done for:', self.data)
  11. def set_db(self):
  12. if checkind():
  13. self.db = 'new db connection'
  14. print('DB connection made for:', self.data)
  15. ik1 = Kls(12)
  16. ik1.do_reset()
  17. ik1.set_db()

如果使用@staticmethod就能把相关的代码放到对应的位置了:

  1. IND = 'ON'
  2. class Kls(object):
  3. def __init__(self, data):
  4. self.data = data
  5. @staticmethod
  6. def checkind():
  7. return (IND == 'ON')
  8. def do_reset(self):
  9. if self.checkind():
  10. print('Reset done for:', self.data)
  11. def set_db(self):
  12. if self.checkind():
  13. self.db = 'New db connection'
  14. print('DB connection made for: ', self.data)
  15. ik1 = Kls(12)
  16. ik1.do_reset()
  17. ik1.set_db()

3. 访问控制

3.1 单下划线”_”

在Python中,通过单下划线“”来实现模块级别的私有化,一般约定以单下划线“”开头的变量、函数为模块私有的,也就是说from moduleName import将不会引入以单下划线“_”开头的变量、函数。

  1. numA = 10
  2. _numA = 100
  3. def printNum():
  4. print("numA is:", numA)
  5. print("_numA is:", _numA)
  6. def _printNum():
  7. print("numA is:", numA)
  8. print("_numA is:", _numA)
  9. # 当通过下面代码引入lib.py这个模块后,所有的以”_”开头的变量和函数都没有被引入,如果访问将会抛出异常
  10. from lib import *
  11. print(numA)
  12. printNum()
  13. print(_numA)

3.2 双下划线”__”

对于Python中的类属性,可以通过双下划线”__”来实现一定程度的私有化,因为双下划线开头的属性在运行时会被”混淆”(mangling)。

  1. # 在Student类中,加入了一个”__address”属性
  2. class Student(object):
  3. def __init__(self, name, age):
  4. self.name = name
  5. self.age = age
  6. self.__address = "Shanghai"
  7. pass
  8. wilber = Student("Wilber", 28)
  9. print(wilber.__address)# 当通过实例wilber访问这个属性的时候,就会得到一个异常,提示属性”__address”不存在

但是__address并没有被完全隐藏:

  1. >>> dir(wilber)
  2. ['_Student__address',
  3. '__class__',
  4. '__delattr__',
  5. '__dict__',
  6. '__dir__',
  7. .
  8. .
  9. # 可以发现_Student__address属性,通过调用它可以访问__address
  10. >>> wilber._Student__address
  11. 'Shanghai'

双下划线的另一个重要的目地是,避免子类对父类同名属性的冲突,父类中的属性被覆盖的问题:

  1. class A(object):
  2. def __init__(self):
  3. self.__private()
  4. self.public()
  5. def __private(self):
  6. print('A.__private()')
  7. def public(self):
  8. print('A.public()')
  9. class B(A):
  10. def __private(self):
  11. print('B.__private()')
  12. def public(self):
  13. print('B.public()')
  14. b = B()

输出结果为:

A.__private()
B.public()

当实例化B的时候,由于没有定义__init__函数,将调用父类的__init__,但是由于双下划线的”混淆”效果,self.__private()将变成 self._A__private()

总结:

  • _:以单下划线开头的表示的是protected类型的变量,即只能允许其本身与子类进行访问;同时表示弱内部变量标示,当使用from moduleNmae import时,不会将以一个下划线开头的对象引入
  • __:双下划线的表示的是私有类型的变量。只能是允许这个类本身进行访问了,连子类也不可以,这类属性在运行时属性名会加上单下划线和类名

4. 继承

4.1 基本用法

在Python中,同时支持单继承与多继承,一般语法如下:

  1. class SubClassName(ParentClass1 [, ParentClass2, ...]):
  2. class_suite

实现继承之后,子类将继承父类的属性,也可以使用内建函数insubclass()来判断一个类是不是另一个类的子孙类:

  1. class Parent(object):
  2. '''parent class'''
  3. numList = []
  4. def numAdd(self, a, b):
  5. return a+b
  6. class Child(Parent):
  7. pass
  8. c = Child() # subclass will inherit attributes from parent class
  9. Child.numList.extend(range(10))
  10. print(Child.numList)
  11. print("2 + 5 =", c.numAdd(2, 5))
  12. # built-in function issubclass()
  13. issubclass(Child, Parent)
  14. issubclass(Child, object)
  15. # __bases__ can show all the parent classes
  16. Child.__bases__
  17. # doc string will not be inherited
  18. Parent.__doc__
  19. Child.__doc__

代码的输出为,例子中唯一特别的地方是文档字符串。文档字符串对于类,函数/方法,以及模块来说是唯一的,也就是说__doc__属性是不能从父类中继承的。

4.2 继承中的init

当在Python中出现继承的情况时,一定要注意初始化函数__init__的行为。

  1. 如果子类没有定义自己的初始化函数,父类的初始化函数会被默认调用;但是如果要实例化子类的对象,则只能传入父类的初始化函数对应的参数,否则会出:
  1. class Parent(object):
  2. def __init__(self, data):
  3. self.data = data
  4. print("create an instance of:", self.__class__.__name__)
  5. print("data attribute is:", self.data)
  6. class Child(Parent):
  7. pass
  8. c = Child("init Child")
  9. c = Child() #报错
  1. 如果子类定义了自己的初始化函数,而没有显示调用父类的初始化函数,则父类的属性不会被初始化:
  1. class Parent(object):
  2. def __init__(self, data):
  3. self.data = data
  4. print "create an instance of:", self.__class__.__name__
  5. print "data attribute is:", self.data
  6. class Child(Parent):
  7. def __init__(self):
  8. print "call __init__ from Child class"
  9. c = Child()
  10. c.data #报错
  1. 如果子类定义了自己的初始化函数,显示调用父类,子类和父类的属性都会被初始化:
  1. class Parent(object):
  2. def __init__(self, data):
  3. self.data = data
  4. print("create an instance of:", self.__class__.__name__)
  5. print("data attribute is:", self.data)
  6. class Child(Parent):
  7. def __init__(self):
  8. print("call __init__ from Child class")
  9. super(Child, self).__init__("data from Child")

4.3 super()函数

前面一个例子中,已经看到了通过super()来调用父类__init__方法的例子,下面看看super()的使用。在子类中,一般会定义与父类相同的属性(数据属性,方法),从而来实现子类特有的行为。也就是说,子类会继承父类的所有的属性和方法,子类也可以覆盖父类同名的属性和方法。

  1. class Parent(object):
  2. fooValue = "Hi, Parent foo value"
  3. def foo(self):
  4. print "This is foo from Parent"
  5. class Child(Parent):
  6. fooValue = "Hi, Child foo value"
  7. def foo(self):
  8. print "This is foo from Child"
  9. c = Child()
  10. c.foo()
  11. print(Child.fooValue)

在这段代码中,子类的属性fooValuefoo覆盖了父类的属性,所以子类有了自己的行为。但是,有时候可能需要在子类中访问父类的一些属性:

  1. class Parent(object):
  2. fooValue = "Hi, Parent foo value"
  3. def foo(self):
  4. print("This is foo from Parent")
  5. class Child(Parent):
  6. fooValue = "Hi, Child foo value"
  7. def foo(self):
  8. print("This is foo from Child")
  9. print(Parent.fooValue)
  10. # use Parent class name and self as an argument
  11. Parent.foo(self)
  12. c = Child()
  13. c.foo()

这时候,可以通过父类名直接访问父类的属性,当调用父类的方法是,需要将self显示的传递进去的方式。这种方式有一个不好的地方就是,需要经父类名硬编码到子类中,为了解决这个问题,可以使用Python中的super()函数:

  1. class Parent(object):
  2. fooValue = "Hi, Parent foo value"
  3. def foo(self):
  4. print("This is foo from Parent")
  5. class Child(Parent):
  6. fooValue = "Hi, Child foo value"
  7. def foo(self):
  8. print("This is foo from Child")
  9. # use super to access Parent attribute
  10. print(super(Child, self).fooValue)
  11. super(Child, self).foo()
  12. c = Child()
  13. c.foo()

对于super(Child, self).foo()可以理解为,首先找到Child的父类Parent,然后调用父类的foo方法,同时将Child的实例self传递给foo方法。但是,如果当一个子类有多个父类的时候,super()会如何工作呢?这是就需要看看MRO的概念了。

4.4 MRO(Method Resolution Order)机制

假设现在有一个如下的继承结构,首先通过类名显示调用的方式来调用父类的初始化函数:

  1. class A(object):
  2. def __init__(self):
  3. print(" ->Enter A")
  4. print(" <-Leave A")
  5. class B(A):
  6. def __init__(self):
  7. print(" -->Enter B")
  8. A.__init__(self)
  9. print(" <--Leave B")
  10. class C(A):
  11. def __init__(self):
  12. print(" --->Enter C")
  13. A.__init__(self)
  14. print(" <---Leave C")
  15. class D(B, C):
  16. def __init__(self):
  17. print("---->Enter D")
  18. B.__init__(self)
  19. C.__init__(self)
  20. print("<----Leave D")
  21. d = D()
  22. # 运行结果为:
  23. ---->Enter D
  24. -->Enter B
  25. ->Enter A
  26. <-Leave A
  27. <--Leave B
  28. --->Enter C
  29. ->Enter A
  30. <-Leave A
  31. <---Leave C
  32. <----Leave D

从输出中可以看到,类A的初始化函数被调用了两次,这不是我们想要的结果。下面,我们通过super()方式来调用父类的初始化函数:

  1. class A(object):
  2. def __init__(self):
  3. print(" ->Enter A")
  4. print(" <-Leave A")
  5. class B(A):
  6. def __init__(self):
  7. print(" -->Enter B")
  8. super(B, self).__init__()
  9. print(" <--Leave B")
  10. class C(A):
  11. def __init__(self):
  12. print(" --->Enter C")
  13. super(C, self).__init__()
  14. print(" <---Leave C")
  15. class D(B, C):
  16. def __init__(self):
  17. print("---->Enter D")
  18. super(D, self).__init__()
  19. print("<----Leave D")
  20. d = D()
  21. # 运行结果为:
  22. ---->Enter D
  23. -->Enter B
  24. --->Enter C
  25. ->Enter A
  26. <-Leave A
  27. <---Leave C
  28. <--Leave B
  29. <----Leave D

通过输出可以看到,当使用super后,A的初始化函数只能调用了一次。

Python的类有一个__mro__属性,这个属性中就保存着方法解析顺序。结合上面的例子来看看类D的__mro__

  1. print("MRO:", [x.__name__ for x in D.__mro__])
  2. >>> MRO: ['D', 'B', 'C', 'A', 'object']

看到这里,对于上面使用super例子的输出就应该比较清楚了。

  • Python的多继承类是通过MRO的方式来保证各个父类的函数被逐一调用,而且保证每个父类函数只调用一次(如果每个类都使用super())
  • 混用super类和非绑定的函数是危险行为,这可能导致应该调用的父类函数没有调用或者一个父类函数被调用多次

5. slots属性

从前面的介绍可以看到,当我们通过一个类创建了实例之后,仍然可以给实例添加属性,但是这些属性只属于这个实例。有些时候,我们可能需要限制类实例对象的属性,这时就要用到类中的__slots__属性了。__slots__属性对赢于一个tuple,只有这个tuple中出现的属性可以被类实例使用。

  1. class Student(object):
  2. __slots__ = ("name", "age")
  3. def __init__(self, name, age):
  4. self.name = name
  5. self.age = age
  6. s = Student("Wilber", 28)
  7. print("%s is %d years old" %(s.name, s.age))
  8. s.score = 96 # 报错

5.1 子类没有slots属性

使用__slots__要注意,__slots__定义的属性仅对当前类的实例起作用,对继承的子类实例是不起作用的:

  1. class Person(object):
  2. __slots__ = ("name", "age")
  3. pass
  4. class Student(Person):
  5. pass
  6. s = Student()
  7. s.name, s.age = "Wilber", 28
  8. s.score = 100
  9. print("%s is %d years old, score is %d" %(s.name, s.age, s.score))
  10. # 运行结果:
  11. Wilber is 28 years old, score is 100

5.2 子类拥有slots属性

如果子类本身也有__slots__属性,子类的属性就是自身的__slots__加上父类的__slots__

  1. class Person(object):
  2. __slots__ = ("name", "age")
  3. pass
  4. class Student(Person):
  5. __slots__ = ("score", )
  6. pass
  7. s = Student()
  8. s.name, s.age = "Wilber", 28
  9. s.score = 100
  10. print("%s is %d years old, score is %d" %(s.name, s.age, s.score))
  11. prints.__slots__
  12. s.city = "Shanghai" # 报错

所以说,对于__slots__属性:

  • 如果父类包含对__slots__的定义,子类不包含对__slots__的定义,解释器忽略__slots__的作用
  • 如果父类包含对__slots__的定义,子类也包含对__slots__的定义,并且无论元组的的元素个数,解释器都会按照父类的__slots__子类的__slots__的并集来检查

6. 类构造与初始化

在前面的文章中,经常使用初始化函数__init__,下面看看__init____new__的联系和差别。