10. Python 面向对象编程#


Python 是一种支持面向对象编程(Object Oriented Programming, OOP)的语言。面向对象编程是一种编程范式,它将数据和操作数据的函数封装在对象中,通过类(Class)和对象(Object)来组织代码,使得代码更易于理解、维护、扩展和重用, 特别适合开发大型、复杂的系统。

10.1. 类, 对象, 属性, 方法#


面向对象编程中,类是对象的模板或蓝图,描述一类事物的属性(变量)和行为(方法);而对象是类的实例,具体体现类的属性和行为。例如,定义企业员工为一个类,该类中,可以包含姓名,工资,工龄,职务等属性;而具体的一个企业员工张三或李四就是这个类的一个实例。打个比喻,类就像是“蛋糕模具”,对象是用这个模具做出的“蛋糕”。

Python 通过 class 来定义一个类,并在类中进一步定义类的属性(数据)和方法(函数)。语法格式如下:

class ClassName:
      <statement_block_1>
      <statement_block_2>
       ...
      <statement_block_N>

下面的例子中,我们定义一个员工类:

class Employee:
    # 类的属性
    department = "business school"  # 普通属性
    __age = 20  # 前缀双下划线命名的变量为类的私有属性

    # 类的方法
    def get_age(self):
        return self.__age

    def say_hello(self):
        print("Hello, everyone!")


john = Employee()  # 实例化一个类
john.say_hello()  # 访问对象里的方法
print("My department is %s." % john.department)  # 访问对象里的属性
Hello, everyone!
My department is business school.

上面的例子中,我们定义了一个 Employee 类,有两个属性:一个普通属性和一个私有属性。通过实例化一个类,我们就可以通过实例访问类里面的一些属性和方法。

前缀双下划线命名的变量为类的私有属性,不能在类外部直接访问,但可以在类里面定义一个访问私有属性的方法,然后调用方法进行访问。

print("age is", john.get_age())
age is 20

如果直接在对象 john 访问私有属性 __age (例如 print(john.__age)),程序会报错。

print(john.__age)

Note

  • 前缀双下划线命名的变量或函数为类的私有属性或方法,不能在类外部直接访问。

  • 前缀单下划线命名的变量或函数为类的守保护属性或方法,可以在类外部直接访问,但不推荐访问。

10.2. 类的初始化 __init__#


类有一个名为 __init__() 的特殊方法,该方法在类实例化时会自动调用,进行对象的初始化。例如:

class Employee:
    # 类的属性
    department = "business school"  # 普通属性
    __age = 20  # 前缀双下划线命名的变量为类的私有属性

    # 类的初始化方法
    def __init__(self, name, age, department):
        self.name = name
        self.__age = age
        self.department = department

    # 类的方法
    def get_age(self):
        return self.__age

    def say_hello(self):
        print("Hello, everyone! My name is %s." % self.name)


john = Employee("John", 25, "engineering school")  # 实例化一个类,自动调用  __init__()初始化
john.say_hello()  # 访问类里的方法
print("My department is %s." % john.department)  # 访问类里的属性
Hello, everyone! My name is John.
My department is engineering school.

上面的例子中,__init__ 方法初始化了类的多个属性,可以包括__init__ 方法前面未被类声明的属性。而已经声明的属性值被__init__ 方法中的参数值覆盖。

Note

类的方法与普通函数有一个显著的区别,它们的第一个参数总是 self,self 代表的是类的实例。

10.3. 类的继承与方法重写#


Python 支持类的继承,即一个类可以继承另一个类的所有属性和方法,还可以添加新的属性,新的方法或者重写另一个类的方法。

例如,我们可以添加一个类 NewEmployee,继承 Employ 类。

class NewEmployee(Employee):  # 小括号内为所继承的父类名
    under_probation = True  # 子类自己的属性

    def __init__(self, under_probation, *args, **kwargs):
        super().__init__(*args, **kwargs)  # 调用父类的初始化函数
        self.under_probation = under_probation

    # 重写父类的方法
    def say_hello(self):
        print("Hi! My name is %s." % self.name)

    # 调用父类的方法
    def age_range(self):
        if super().get_age() >= 30:  # 调用父类的方法
            print("I am over 30s")
        else:
            print("I am under 30s")

    # 自己的方法
    def probabtion_info(self):
        if self.under_probation:
            print("I am under probation.")
        else:
            print("I have passed probation.")


tom = NewEmployee(
    under_probation=False, name="John", age=25, department="engineering school"
)
tom.say_hello()
tom.probabtion_info()
Hi! My name is John.
I have passed probation.

上面的例子中,使用了super()调用了父类的 __init__ 与 get_age 方法,并重写父类的 say_hello 方法。

Python 同样有支持多继承形式,即一个子类可以继承多个父类,将多个类的名称放入子类的小括号里面即可。例如,我们定义个 NewAcademicEmpolyee 类,同时继承了类 Employee 与 NewEmployee。

class NewAcademicEmployee(NewEmployee, Employee):
    def say_hello(self):
        print("Hello, I am a new academic empolyee.")


# 实例化子类时,初始化方法中的参数包括所有父类初始化方法中需要的参数
lily = NewAcademicEmployee(True, "Lily", 22, "art school")
lily.say_hello()
Hello, I am a new academic empolyee.

上面的例子中,子类在初始化时,需要传递所有父类初始化方法中需要的参数。

Note

如果子类和父类有同名的方法,并且你使用子类实例调用该方法,Python 会优先调用子类的方法,而不会调用父类的方法。

class Parent:
    def show(self):
        print("Parent method")


class Child(Parent):
    def show(self):  # 重写父类方法
        print("Child method")


obj = Child()
obj.show()  # 输出: Child method
Child method

10.4. 类的专有/特殊方法 __str__、__getitem__等#


以前后缀双下划线命名的方法为类的专有/特殊方法,又称作魔术方法,用以实现一些特定功能。

下面表格中总结了一些常用的专有方法。

专有方法

含义

__init__

创建对象时自动调用,用于初始化类的实例

__str__

给用户看的,打印对象时显示的内容

__repr__

给开发者看的,用于调试,打印对象时显示的内容

__getitem__

获取对象中元素,使得对象可以像数组一样访问类中元素

__setitem__

设置对象中元素,使得对象可以像数组一样修改类中元素

__add__

实现对象的加法运算

__sub__

实现对象的减法运算

__mul__

实现对象的乘法运算

__truediv__

实现对象的除法运算

__eq__

自定义对象比较规则

__hash__

定义对象的哈希值,让对象支持 set 或 dict 存储

__class__

返回对象所属类的名字

__call__

让对象可以像函数一样被调用

下面的一个例子展示了 __repr____str__的用法。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Person('{self.name}', {self.age})"

    def __str__(self):
        return f"{self.name}, {self.age} years old"


p = Person("Bob", 30)
print(repr(p))  # 调用 __repr__
print(p)  # 默认调用 __str__, 如果 __str__ 未定义,会默认调用 __repr__
Person('Bob', 30)
Bob, 30 years old

如果没有定义 __repr__ 或 __str__ 方法,则打印对象时会显示该对象的内存地址。

class Dog:
    pass


d = Dog()
print(d)  # 类中没有 __str__ 或 __repr__ 方法,打印显示地址
print(d.__class__)  # 返回对象所属类的名字
<__main__.Dog object at 0x104ffbec0>
<class '__main__.Dog'>

Note

__main__ 代表的是当前运行的 Python 文件(以 .py 后缀命名的文件)

下面的例子,展示了如何通过 __getitem__ 方法实现像访问数组一样访问对象。

class MyList:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, index):
        return self.data[index]  # 支持整数索引和切片


my_list = MyList([10, 20, 30, 40, 50])
print(my_list[1:3])  # 像数组切片一样访问类中元素
[20, 30]

下面的例子,实现了两个对象的相加。

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)  # 输出: Vector(6, 8)
Vector(6, 8)

下面的例子,展示了如何比较两个对象。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 若姓名与年龄都相同,表示两个实例相同
    def __eq__(self, other):
        if isinstance(other, Person):
            return self.name == other.name and self.age == other.age
        return False  # 不是 Person 对象,直接返回 False


p1 = Person("Alice", 25)
p2 = Person("Alice", 25)
p3 = Person("Bob", 30)

print(p1 == p2)  # True
print(p1 == p3)  # False
True
False

下面的例子,实现了对象类似函数的调用。

class MyClass:
    def __call__(self, *args, **kwargs):
        print("Instance called!")
        print("Arguments:", args, kwargs)


obj = MyClass()
obj(1, 2, a=3)  # 像函数一样调用实例
Instance called!
Arguments: (1, 2) {'a': 3}

10.5. 类的特殊属性 __doc__、__name__ 等#


在 Python 中,类的特殊属性也称作“魔术属性”(magic attribute),或“dunder”属性/方法,通常指以双下划线开头和结尾的特殊属性或方法。这些属性和方法内置于 Python 语言中,用于实现一些特殊行为和功能。

除了专有方法,Python 还提供一些魔术属性,用于存储元数据或对象状态信息,如:

  • __doc__:存储模块、类或函数的文档字符串。

  • __name__:记录模块或类的名称。

  • __dict__:存放对象的属性字典,记录对象的所有可写属性。

class Example:
    """这是一个演示魔术属性的示例类。"""

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return f"示例对象,值为: {self.value}"

    def __repr__(self):
        return f"Example({self.value!r})"

# 创建一个实例
ex = Example(42)

# 使用魔术属性
print(ex.__doc__)   # 输出:这是一个演示魔术属性的示例类。
print(str(ex))      # 输出:示例对象,值为: 42
print(repr(ex))     # 输出:Example(42)
print(ex.__dict__)  # 以字典形式输出实例的属性值
这是一个演示魔术属性的示例类。
示例对象,值为: 42
Example(42)
{'value': 42}

__name__ 是 Python 内置的特殊变量,可以表示当前类的名称,也可以表示当前模块的名称。

MyClass.__name__  # 返回类的名称
'MyClass'
import numpy as np

np.__name__  # 返回模块的名称
'numpy'

当直接运行一个 Python 文件时,文件的名称为 ‘__main__’。因此,在编程调试某个程序文件时,可以用一个判断语句,根据文件的名字判断文件是否为当前直接运行的文件。例如:

# myscript.py
def hello():
    print("Hello, world!")


if __name__ == "__main__":
    print("This script is running directly")
    hello()
else:
    print("This script is imported as a module")
This script is running directly
Hello, world!

10.6. 类属性的相关方法setattr(), getattr()#


对于类中属性值的修改和访问,Python 内置有两个方法setattr(), getattr()比较有用。

  • setattr(object, name, value) 设置一个对象 object 的属性 atribte 的值为 value,若没有找到属性,则新建属性并赋值 value

  • getattr(object, name) 返回对象 object的属性 name 的值,若没找到则触发 AttributeError

  • getattr(object, name, default) 返回对象 object 的属性 name的值,若没找到,则返回 default值

其中,小括号的属性参数以字符串形式表示。

class ExampleClass:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

setattr(ExampleClass, 'name', 10)
print(getattr(ExampleClass, 'name'))
setattr(ExampleClass, 'gender', 'male')  # 没有找到属性 gender,则新建该属性并赋值
print(getattr(ExampleClass, 'gender'))

p = ExampleClass('tom', '40')
print(getattr(p, 'grade', 75)) # 没有找到属性 grade,返回给定的值 75
print(p.gender)
10
male
75
male

10.7. 类方法与静态方法#


上面的例子中,类中方法的第一个参数总是 self,这类方法为普通方法,只能被对象调用。

除了普通方法外,还有类方法静态方法

  • 静态方法: 用 @staticmethod 装饰的不带 self 参数的方法叫做静态方法,类的静态方法可以没有参数,可以直接使用类名调用

  • 类方法: 默认有个 cls 参数,可以被类和对象调用,需要加上 @classmethod 装饰器

class MyClass:
    @staticmethod  # 以@开头的名称为装饰器,用来说明特殊的函数
    def fun():
        print("静态方法")

    @classmethod
    def a(cls):  # 第一个参数为 cls
        print("类方法")

    # 普通方法
    def b(self):  # 第一个参数为 self
        print("普通方法")


MyClass.fun()  # 静态方法可以被类名直接调用
MyClass.a()  # 类方法可以被类名直接调用

case = MyClass()
case.fun()  # 静态方法也可以被对象调用
case.a()  # 类方法也可以被对象调用
case.b()  # 普通方法只能被对象调用
静态方法
类方法
静态方法
类方法
普通方法

10.8. dateclass#


dataclass 是 Python 3.7 引入的装饰器,自动生成 __init__、__repr__、__eq__ 等方法,让数据类的编写更加简洁。

一个例子:

from dataclasses import dataclass


@dataclass
class Person:
    name: str  # 声明 name 为字符串类型
    age: int  # 声明 age 为整数类型

上面的代码等同于:

class Person:
    def __init__(self, name: str, age: int):  # 冒号跟数据类型,可以声明变量的数据类型
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Person(name={self.name}, age={self.age})"

可以看出,dataclass 与普通类的一个区别是,必须在定义中声明属性的数据类型,但是显著减少了代码量。