Python 是一种面向对象的编程语言
简单理解 面向对象 :基于模板(类)去创建实体(对象),使用对象完成功能开发
面向对象的三大特征:
- 封装
- 继承
- 多态
面向对象技术简介
- 类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例
- 对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法
- 实例化:创建一个类的实例(类的具体对象)
- 方法:类中定义的函数
- 类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用
- 数据成员:类变量或者实例变量用于处理类及其实例对象的相关的数据
- 方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写
- 局部变量:定义在方法中的变量,只作用于当前实例的类
- 实例变量:在类的声明中,属性是用变量来表示的,这种变量就称为实例变量,实例变量就是一个用 self 修饰的变量
- 继承:即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个 Dog 类型的对象派生自 Animal 类,这是模拟 "是一个(is-a)" 关系(即,Dog 是一个 Animal)
# 类和对象
类的组成:
- 成员变量:定义在类中的属性(变量)
- 成员方法:定义在类中的行为(函数)
# 类定义
使用关键字 class
定义类:
class Class_Name: | |
<statement-1> | |
... | |
<statement-N> |
其中, Class_Name
表示类的名称,通常需要首字母大写
# 类对象
创建对象(实例化):
obj = Class_Name() |
修改对象属性:
obj.var_name = xx |
删除对象属性:
# 使用 del 关键字删除对象的属性 | |
del obj.var_name |
删除对象:
# 使用 del 关键字删除对象 | |
del obj |
# 类的成员方法
- 函数:定义在类的外部
- 方法:定义在类的内部
在类的内部,使用 def
关键字来定义一个方法,与一般函数定义不同,类的成员方法必须必须有一个额外的第一个参数名称,按照惯例它的名称是 self
,即:
def func_name(self, parameter_1, ..., parameter_N): | |
... |
其中:
-
self
参数是对类的当前实例的引用,用于访问属于该类的变量。它不必被命名为self
,但它必须是类中任意函数的首个参数 -
在方法内部,想要访问类的成员变量,必须使用
self
。即,使用self.var_name
访问成员变量var_nam
-
调用类的成员方法时,
self
会被自动传入,即,不需要在实参列表中写出self
# 构造方法
Python 类中的 __init__
方法,称之为 构造方法
每次使用类创建新对象时,都会自动调用 __init__
方法,并且会将创建类对象时的传入参数自动传递给 __init__
方法
因此,使用 __init__
方法可以将值赋给对象属性,或者在创建对象时需要执行的其他操作
注意,构造方法 也是 成员方法,因此:
- 定义构造方法时需要在形参列表中写上
self
参数 - 在构造方法内部访问成员变量需要使用
self.var_name
例如:
# 定义类 | |
class Person: | |
name = None # 由于定义了 __init__ 方法,可以省略这里的初始化 | |
age = None # 由于定义了 __init__ 方法,可以省略这里的初始化 | |
def __init__(self, name, age): | |
self.name = name | |
self.age = age | |
# 创建类对象 | |
p = Person("John", 24) | |
# 打印类对象的属性 | |
print(p.name) | |
print(p.age) |
# 魔术方法
魔术方法(Magic Methods)是 Python 中的内置方法,一般以 __
(双下划线)为开头和结尾
例如:
__init__
:构造方法,当一个实例被创建的时候调用的初始化方法__del__
:析构方法,当一个实例被销毁的时候调用的方法__str__
:字符串方法,定义当被str()
调用或者打印对象时的行为__lt__
:定义小于号的行为:x < y
调用x.__lt__(y)
__le__
:定义小于等于号的行为:x <= y
调用x.__le__(y)
__eq__
:定义等于号的行为:x == y
调用x.__eq__(y)
之所以称之为魔法方法,是因为这些方法会在进行特定的操作时会被自动调用
如果希望根据自己的程序定制自己特殊功能的类,那么就需要对这些方法进行重写
# pass 语句
pass
是占位语句,用来保证函数(方法)或类定义的完整性,表示无内容
如果因为某种原因写了无内容的函数定义或类定义,需使用 pass
语句来避免错误
例如:
# 继承 | |
class DerivedClassName(BaseClassName): | |
pass |
参考资料:
# 封装
在程序设计中,封装(Encapsulation)是对具体对象的一种抽象,即将某些部分隐藏起来(即,定义成私有成员),在程序外部看不到(即,其他程序无法调用)
- 封装数据:保护隐私
- 封装方法:隔离复杂度(只保留部分接口对外使用)
# 私有成员
类对象无法直接访问私有成员,但类内其它成员可以直接使用私有成员
私有成员变量:
- 定义:变量名以
__
(2 个下划线)开头,例如:__private_attrs
- 声明该属性为私有属性,只能在类的内部使用,不能在类的外部使用或直接访问
- 在类内部的访问方式为:
self.__private_attrs
私有成员方法(封装的方法):
- 方法名以
__
(2 个下划线)开头,例如:__private_method
- 声明该方法为私有方法,只能在类的内部调用,不能在类的外部调用
- 在类内部的调用方式为:
self.__private_method()
与 java 等语言不同,Python 的封装并不是真正意义上的外部无法调用。在 Python 中,如果需要在类外调用封装的属性或方法,须采用
对象名._类名__方法名
和对象名._类名__变量名
的方式(不建议调用)
例如:
class Foo: | |
def __init__(self, height, weight): # 构造方法 | |
self.height = height | |
self.weight = weight | |
def __heightpow(self): # 私有方法 | |
return self.height * self.height | |
def tell_bmi(self): | |
return self.weight / self.__heightpow() # 私有成员可以被类内其它成员直接使用 | |
egon = Foo(1.7, 120) | |
print(egon.tell_bmi()) | |
print(egon._Foo__heightpow()) # 尽管是私有成员方法,但也可以在类外调用以供查看 |
# property
property 是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值(就是一个装饰器)
将一个类的方法 func_name
用 propery 装饰以后,类对象 obj
可直接通过 obj.func_name
去使用,以遵循统一访问的原则
被 property 装饰的属性会优先于对象的属性被使用
被 propery 装饰的属性分成三种:
- property
- 被装饰的函数名.setter
- 被装饰的函数名.deleter
例如, @property
把类中的方法伪装成属性:
from math import pi | |
class Circle: | |
def __init__(self, radius): | |
self.radius = radius | |
@property # 装饰器:将一个方法当成一个属性用 | |
def area(self): | |
return self.radius * self.radius * pi | |
@property | |
def peimeter(self): | |
return 2 * pi * self.radius | |
c = Circle(10) | |
print(c.area) # 当成一个属性来调用(即,不需要加括号) | |
print(c.peimeter) # 当成一个属性来调用(即,不需要加括号) |
注意:此时的特性 area 和 perimeter 不能被赋值
参考资料:
# 继承
继承允许我们定义继承(复制)另一个类的所有方法和属性的类
继承可以分为:
- 单继承:一个类继承另一个类
- 多继承:一个类继承多个类,按照顺序从左向右依次继承
父类是被继承的类,也称为基类
子类是从另一个类继承所得的类,也称为派生类
# 单继承
任何类都可以是父类,因此,创建父类的语法与创建任何其他类相同
要创建从其他类继承功能的类,只需在创建子类时将父类作为参数传递即可:
class DerivedClassName(BaseClassName): | |
<statement-1> | |
... | |
<statement-N> |
其中, DerivedClassName
为子类名, BaseClassName
为父类名
如果不希望向子类中添加任何其他属性或方法,可使用
pass
关键字
例如:
# 创建 Person 类 | |
class Person: | |
def __init__(self, fname, lname): | |
self.firstname = fname | |
self.lastname = lname | |
def print_name(self): | |
print(self.firstname, self.lastname) | |
# 创建 Person 类对象,并执行 print_name 方法 | |
x = Person("Bill", "Gates") | |
x.print_name() | |
# 创建继承自 Person 类的子类 Student | |
class Student(Person): | |
def __init__(self, fname, lname, gra): | |
self.firstname = fname | |
self.lastname = lname | |
self.grade = gra | |
def print_info(self): | |
print(self.firstname, self.lastname, self.grade) | |
# 使用 Student 类创建一个对象,然后执行 print_name 和 print_info 方法 | |
y = Student("Elon", "Musk", 6) | |
y.print_name() | |
y.print_info() |
# 多继承
创建子类的语法:
class DerivedClassName(Base1, Base2, Base3): | |
<statement-1> | |
... | |
<statement-N> |
如果多个父类中含有同名的成员,那么默认按照从左向右顺序依次继承,先继承的保留,后继承的被覆盖
# 方法重写
子类继承父类的成员属性和成员方法后,如果对其 “不满意”,可以在子类重写父类的方法
即:在子类中重新定义同名的属性或方法
例如:
class Parent: # 定义父类 | |
def myMethod(self): | |
print ('调用父类方法') | |
class Child(Parent): # 定义子类 | |
def myMethod(self): # 重写父类方法 | |
print ('调用子类方法') | |
c = Child() # 子类实例 | |
c.myMethod() # 子类调用重写方法 |
# 调用父类同名成员
一旦在子类中重写父类成员以后,子类的类对象调用成员时将是调用复写后的新成员
如果需要使用被复写的父类的成员,需要特殊的调用方式:
-
方式一:通过父类调用(单继承
- 调用父类成员变量:
父类名.成员变量
- 调用父类成员方法:
父类名.成员方法(self)
- 调用父类成员变量:
-
方式二:使用
super()
调用- 调用父类成员变量:
super().成员变量
- 调用父类成员方法:
super().成员方法()
- 调用父类成员变量:
如果是单继承的情况,可以直接用父类名调用父类方法
但是,如果是多继承的情况,可能会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题,此时,推荐采用 super()
函数调用,以解决多重继承问题。具体可参考:Python super () 函数
参考资料:
# 类型注解
类型注解:在代码中涉及数据交互的地方,提供数据类型的注解(显式的说明)
- 帮助第三方 IDE 工具(如 PyCharm)对代码进行类型推断,协助做代码提示
- 帮助开发者自身对变量进行类型注释
有两种注解:
- 变量的类型注解
- 函数(方法)的类型注解
需注意:类型注解仅仅是提示性的,不是决定性的,并不会真正地对类型做验证和判断(也就是说:即使注解的数据类型与实际数据类型不一致,程序也不会报错)
# 变量的类型注解
事实上,对于显式的变量定义,一般无需注解。即便不写类型注解,也可以明确知晓变量的类型
一般而言,在无法直接看出变量类型时(例如,将函数返回值赋给一个变量时),才会添加变量的类型注解
# 方式一(常用)
基础语法:
变量: 类型 |
例如:
基础数据类型注解:
var1: int = 3 | |
var2: float = 3.14 | |
var3: bool = True |
类对象类型注解:
class Person: | |
pass | |
p: Person = Person() |
容器类型简易注解:
mylist: list = [1, 2, 3] | |
mytuple: tuple = (1, 2, 3) | |
myset: set = {1, 2, 3} | |
mydict: dict = {0: "a", 1: "b"} | |
mystr: str = "Jiankychen" |
容器类型详细注解:
- 元组类型设置类型详细注解:需要将每一个元素都标记出来
- 字典类型设置类型详细注解:需要 2 个类型,第一个是 key ,第二个是 value
mylist: list[int] = [1, 2, 3] | |
mytuple: tuple[str, int, bool] = ("a", 0, True) | |
myset: set[int] = {1, 2, 3} | |
mydict: dict[int, str] = {0: "a", 1: "b"} |
# 方式二
除了使用 变量: 类型
这种语法做注解外,也可以 在注释中进行类型注解
基础语法:
# type: 类型 |
例如:
class Person: | |
pass | |
def func(): | |
return 3.14159 | |
var1 = random.randint(1, 10) # type: int | |
var2 = Person() # type: Person | |
var3 = func() # type: float |
# 函数(方法)的类型注解
# 形参注解
函数(方法)的形参类型注解语法:
def func_name(形参名: 类型, 形参名: 类型, ...) | |
statement |
例如:
def add(x: int, y: int): | |
return x + y |
# 返回值注解
函数(方法)的返回值也是可以添加类型注解的,语法如下:
def func_name() -> 返回值类型: | |
statement |
例如:
def func(data: list) -> list: | |
return data |
# Union 类型
通过 Union
可以定义联合类型
- 导包:
from typing import Union
- 使用:
Union[类型, ..., 类型]
在变量注解、函数(方法)形参和返回值注解中,均可使用 Union
联合类型
例如:
from typing import Union | |
mylist: list[Union[str, int]] = [1, 2, "3", "4"] # list 元素的数据类型可能是 str ,也可能是 int | |
mydict: dict[str, Union[str, int]] = {"name": "Jiankychen", "age": 24} # value 的数据类型可能是 str ,也可能是 int | |
def func(data: Union[int, str]) -> Union[int, str]: | |
return data |
参考资料:
# 多态
多态:多种状态,即:对于某个行为(函数),使用不同的对象将会得到不同的状态
例如: Cat
类和 Dog
类都有 speak
成员方法, Cat
类对象和 Dog
类对象都能被传入 command
函数;但是由于两个类定义的 speak
方法的功能不一致,传入不同类的对象就会得到不同的状态
class Cat: | |
def speak(self): | |
print "meow!" | |
class Dog: | |
def speak(self): | |
print "woof!" | |
def command(pet): | |
pet.speak() | |
cat = Cat() | |
command(cat) # 输出:meow! | |
dog = Dog() | |
command(dog) # 输出:woof! |
多态常作用在继承关系上,比如:定义函数(方法),通过类型注解声明需要父类对象,实际传入子类对象进行工作,从而获得不同的工作状态
例如,可以将上例中的代码修改为:
class Pet: | |
def speak(self) | |
pass | |
class Cat(Pet): | |
def speak(self): | |
print "meow!" | |
class Dog(Pet): | |
def speak(self): | |
print "woof!" | |
def command(pet: Pet): # 函数形参的类型注解 | |
pet.speak() | |
cat = Cat() | |
command(cat) # 输出:meow! | |
dog = Dog() | |
command(dog) # 输出:woof! |
# 抽象类(接口)
抽象方法:没有具体实现的方法(即:方法体是 pass
语句)
抽象类:包含抽象方法的类,也可以称之为 接口,比如上例中的 Pet
类
抽象类相当于定义一个标准,包含了一些抽象的方法,要求子类必须实现
这种设计的意义在于:
- 父类用来确定有哪些方法
- 具体的方法实现,由子类自行决定
参考资料: