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)

之所以称之为魔法方法,是因为这些方法会在进行特定的操作时会被自动调用

如果希望根据自己的程序定制自己特殊功能的类,那么就需要对这些方法进行重写

参考:Python 中类的魔术方法

# 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

抽象类相当于定义一个标准,包含了一些抽象的方法,要求子类必须实现

这种设计的意义在于:

  • 父类用来确定有哪些方法
  • 具体的方法实现,由子类自行决定

参考资料: