Python基础-类
@(Python)[python, python基础]
写在前面
如非特别说明,下文均基于Python3
摘要
本文重点讲述如何创建和使用Python
类,绑定方法与非绑定方法的区别,以及Python
的多态与简单继承。
1. 面向对象编程
1.1 对象和类
面向对象这种思想其实只是人类思维在程序设计领域的一种自然延伸。程序设计领域将现实世界中事物自然延伸为“对象”,事物拥有其属性和作用,对象也一样,拥有属性以及方法;复杂的面向对象程序就是基于一个个基本的对象,相互交织,构造一个完整的对象生态。
现实世界中“类”这个概念其实并不显著,但是也存在。类在面向对象中是对一个对象集合的抽象,抽象集合中对象共有的属性,方法构成类。通过类可以构建具体的对象。
1.2 一切皆对象
Python
哲学是
一切皆对象
在使用Python
编程和学习时,需时刻秉持这一思想。
2. 定义Python类&对象
2.1 最简单的python类
Python
类定义的句法很简单,关键字class
后接合法类名即可;最简单的Python
类如下:
class Person:
pass
类对象
Python
一切皆对象,类也不例外。Python
解释器在解释完类定义这段代码时,在当前作用域创建了一个类对象用来表示该类,并使用名字Person
指向该类对象;
使用类对象,可以进行以下操作:
- 实例化
- 属性引用
实例化&实例对象
实例化就是用类对象创建一个实例对象过程,实例化的结果是实例对象;语法如下:
p = Person()
以上语句在当前作用域创建Person
类对象的实例对象,并使用名字p
指向该对象。
属性引用
引用对象的属性非常简单,使用obj.attr
的方式就可以引用对象obj
的属性attr
。
2.2 属性绑定
只有对象绑定过属性之后,才能引用该属性。Python
是动态语言,可以在可变对象创建之后为对象绑定属性。
类对象的绑定属性被称为类变量;实例对象的绑定属性被称为实例变量。通常来说,实例变量是对于每个实例都独有的数据,而类变量是该类所有实例共享的属性和方法。
由于Python
是动态语言,因此可以在对象创建之后修改对象的信息。
可以在Person
类对象创建之后绑定属性:
Person.school = 'Whu University'
也可以在Person
实例对象创建之后绑定属性:
p = Person()
p.name = 'Richard'
p.age = 20
def print_info(p):
print(p.name, p.age)
print_info(p)
更多关于类变量与实例变量的区别与联系,参考 Python基础-类变量和实例变量
3. 封装
如果在对象创建之后再根据需要绑定属性,并且在外部函数随意访问实例对象的属性,那么类机制的优点就无法体现出来了。在实践中,我们一般在定义类时就决定了类变量与实例变量。
3.1 隐藏细节
通常来说,对象的使用者只需要关心对象能“做什么”,而不需要也不应该关心“怎么做”,这就是封装了。按照封装的思想,一个Python
类应该向外提供接口,并隐藏接口实现细节:
class Person:
school = 'Whu University' # 类属性绑定
def __init__(self, name, age):
self.name = name # 实例属性绑定
self.age = age # 实例属性绑定
def print_info(self): # 暴露公共接口
print(self.name, self.age)
p = Person('Richard', 20)
p.print_info()
如上,将属性的绑定放到类定义中,并向外提供了接口。然而不幸的是,在类外部还是可以引用到类实例的属性。
3.2 绑定方法与非绑定方法
通过直接将print_info
打印出来,可以直观看到绑定方法与非绑定方法的区别:
print(Person.print_info)
print(p.print_info)
output:
<function Person.print_info at 0x006E2AE0>
<bound method Person.print_info of <__main__.Person object at 0x006E0E10>>
可以看到类对象Person
的函数print_info
是个function
;而实例对象p
的方法print_info
是个bound method
绑定方法。
也可以查看它们的类型:
print(type(Person.print_info)) # <class 'function'>
print(type(p.print_info)) # <class 'method'>
一般来说,非绑定方法也叫函数,绑定方法简称方法。绑定方法的绑定,在于函数与特定的对象绑定在了一起。
引用非数据属性的实例属性时,会搜索它对应的类。如果名字是一个有效的函数对象,Python
会将实例对象连同函数对象打包到一个抽象的对象中并且依据这个对象创建方法对象:这就是被调用的方法对象(绑定方法)。当使用参数列表调用方法对象时,会使用实例对象以及原有参数列表构建新的参数列表,并且使用新的参数列表调用函数对象。
那么,以绑定形式调用方法时,第一个参数不是显式传递的,而是解释器隐式传递的:
p.print_info()
同样,也可以通过调用绑定方法的函数版本达到相同的目的:
Person.print_info(p) # 函数必须显式传递参数
3.3 魔法方法__init__
方法__init__
是一个充满魔力的方法,一般以双下划线开头并且结尾的方法对于Python
都有特殊的意义,在满足条件的时候被调用。
__init__
就是一个构造方法,在类对象的实例化过程中被调用,即在p = Person()
这条语句中,Python
解释器自动调用了__init__
方法。
一般来说,使用__init__
方法来初始化实例对象,即为实例对象绑定属性。
3.4 绑定方法参数self
注意到类Person
定义的两个方法中都有一个占据第一个参数位置,名为self
的参数。其实这是一个特殊参数:当调用绑定方法时,调用实例对象会被Python
解释器作为第一个参数来调用绑定方法。
因此,重要的是参数位置,在绑定方法中,第一个参数总是代表当前调用实例对象,与参数名字无关。但是,self
的字面意思是自身的意思,这个名字能很好的体现第一个参数的实际意义。
在类中,类的局部作用域与函数的局部作用域是不能相互访问的,详见:Python进阶 - 命名空间与作用域。因此,函数直接只能以对象引用的方式访问其他属性,这也是对外提供的接口都有self
参数的原因。
class Person:
school = 'Whu University'
def __init__(self, name, age):
self.name = name
self.age = age
# print(school) 类属性school是不能直接访问的,需要用Person.school的形式
def print_info(self): # 如果没有self,将访问不到name和age
print(self.name, self.age)
3.5 私有化
Python
并没有语法支持属性的私有化,但是Python
可以使用“名称变化术”改变私有属性的名字:
class Person:
__school = 'Whu University'
def __init__(self, name, age):
self.__name = name
self.__age = age
def print_info(self):
print(self.__name, self.__age)
p = Person('Richard', 20)
p.print_info()
print(p.__dict__) # {'_Person__name': 'Richard', '_Person__age': 20}
通常来说,Python
解释器会将类定义中所有以__
开头的属性变名,具体变名的规则由解释器决定,目前的CPython
解释器是在前面加_类名
的方式。
诚然,仍然可以使用变名后的属性名,如此处的_Person__name, _Person__age
来访问实例对象的属性,但是通常不建议这么做。因为变名的规则是解释器决定的,如果变更解释器,变名可能就不一样了;再则,变名是类作者给类使用者的一个强力信号,不建议使用者直接访问属性。
一般地,还可以在属性名前加前缀_
,如_name, _age
,来表达私有属性。这种方式不会引起解释器的变名,但是发出了不应该直接访问这些属性的信号。同时,这种规则的属性不会被from module import *
的方式导入。
4. 多态
多态意味着就算不知道变量所引用的对象的类型,还是能够对它进行操作,而它会根据对象的实际类型的不同表现出不同的行为。
4.1 其他语言的多态
在一些高级语言中,为实现多态。首先会定义一个接口,然后实现若干继承接口的的具体类,以顶层的接口来引用具体的接口实现,在运行时根据具体实现的不同表现出不同的行为:
interface Top {
void bar();
}
class A implements Top {
public void bar(){
// implementing of A
}
}
class B implements Top {
public void bar(){
// implementing of B
}
}
void foo(Top t) {
t.bar()
}
这里foo
方法根据参数t
实际类型的不同会表现出不同的行为。
4.2 Python的多态
Python
的多态思想与上面是相同的,但是在实现上有所不同。Python中的多态是基于对象的行为,而不像其他语言是基于父类或者接口。Python
虽然是强类型语言,但是Python
的变量是没有类型的,因此Python
多态不需要一个顶层的接口。只需要实际的对象拥有需要的属性即可:
class FooLike1:
def wow(self):
print('foo in FooLike1')
class FooLike2:
def wow(self):
print('foo in FooLike2')
def bar(foo):
foo.wow()
bar(FooLike1()) # foo in FooLike1
bar(FooLike2()) # foo in FooLike2
当然参与多态的对象有共同父类也是可以的,重点是都有参与多态需要的函数。Python
中的多态要灵活得多;只要对象拥有指定方法,就可以参与多态,这种对象成为“like”对象,如和file
对象拥有read
方法的对象是file like
对象,可以参与到关于read
的多态中。
5. 继承
继承是一个懒惰的行为,子类可以不劳而获从父类获取必要信息。如现在有一个Person
类,拥有名字和年龄属性,现在要创建一个Student
类,也有名字和年龄,并且增加和学号信息;通过继承,可以从Person
不劳而获一些信息。
5.1 如何继承
Python
的继承语法也很简单,只需要在类名后跟括号,括号中写入要继承的类即可:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
当然,如果是多继承,在继承列表里添加即可:
class DerivedClassName(BaseClassName1, BaseClassName2, ..., BaseClassNamen):
<statement-1>
.
.
.
<statement-N>
5.1 继承到了什么
那么,子类到底从父类继承到了什么呢?一个直观的例子可以看出:
class Person:
__school = 'Whu University'
def __init__(self, name, age):
self.__name = name
self.__age = age
def print_info(self):
print(self.__name, self.__age)
def test():
pass
class Student(Person):
def __init__(self):
pass
print(dir(Student))
s = Student()
print(dir(s))
output:
['_Person__school', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'print_info', 'test']
=======分割线=======
['_Person__school', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'print_info', 'test']
可以看出,子类类对象继承到了父类类对象的所有东西,包括类属性,函数。但是子类实例对象没有继承到父类实力对象的实例属性,即 __name
和__age
没有继承到。
我们说过,属性只有绑定之后才能引用,显然,在子类中没有调用父类的__init__
方法,父类的实例对象的属性自然没有初始化;因此只要在子类中调用父类的构造方法,就可以继承到实例属性了:
class Student(Person):
def __init__(self, name, age, stu_id):
self.stu_id = stu_id
super().__init__(name, age)
子类如何初始化父类是一个大问题,单继承比较简单,涉及到多继承的初始化就比较复杂了。
5.2 获取继承关系
Python有两个可以判断继承关系的内建函数:
- 使用isinstance()检查实例的类型:isinstance(obj, int),当且仅当obj.class是int或者派生与int的类时,返回True
- 使用issubclass()检查类的继承关系:issubclass(bool, int)返回True,因为bool是int的子类。然而issubclass(float, int)返回False,因为float不是int的子类。