Skip to content

面向对象概念

找出对象是面向对象分析与编程中非常重要的任务。 记住,对象既有数据又有行为。如果我们只需要处理数据,那么通常更好的方法是将数据存储在列表、集合、字典或其他的Python数据结构中​。 如果我们只需要用到行为,不需要存储数据,那么简单的函数就足够了。

在Python中,类是面向对象编程(OOP)的核心概念之一,它提供了一种将数据和功能封装在一起的方式。 类(class)把数据与功能绑定在一起。创建新类就是创建新的对象类型,从而创建该类型的新实例。 类实例具有多种保持自身状态的属性。 类实例还支持(由类定义的)修改自身状态的方法。

类(Class):

  • 定义:类是一个蓝图或模板,用于创建具有相同属性和方法的对象。它定义了对象的结构和行为。
  • 创建新类:通过定义一个类,你创建了一个新的对象类型(type of object)。这意味着你可以创建该类的多个实例,每个实例都是类的一个具体化,拥有类定义的属性(attributes)和方法(methods)。
  • 实例化:创建类的实例的过程称为实例化(instances)。每个实例都拥有自己的属性值,但共享类定义的方法。

类实例(Instance):

  • 属性(Attributes):在Python中,我们可以把属性称为实例变量。属性是属于每个类实例的变量,用于保持对象的状态。属性可以是基本数据类型、其他对象或任何Python支持的数据类型。
  • 方法(Methods):实例的方法是定义在类中的函数,用于修改实例的状态或执行与实例相关的操作。方法的第一个参数通常是 self,它代表实例本身。

Python的类支持所有面向对象编程(OOP)的标准特性:

  • 类继承(Class Inheritance):
    • 多继承:Python支持多继承,即一个类可以继承多个基类。这允许派生类继承多个基类的属性和方法。
    • 基类(Base Classes):基类是被其他类继承的类。基类可以提供通用的属性和方法,供派生类(derived class)使用和扩展。
  • 方法覆盖(Method Overriding):
    • 派生类可以覆盖基类的方法,以提供特定的实现。当调用派生类实例的方法时,会优先执行派生类中的方法。
  • 方法调用:
    • 派生类的方法可以使用 super() 函数调用基类中相同名称的方法。这允许在派生类中扩展或修改基类的行为。
  • 对象数据的灵活性:
    • 对象可以包含任意数量和类型的数据。Python的动态特性(dynamic nature)允许在运行时为对象添加新的属性和方法。

类和模块的动态性:

  • 类和模块都是在运行时创建的,我们可以在程序运行过程中定义新的类和模块。
  • 创建后,类和模块也可以被修改。例如,可以在运行时为类添加新的方法或属性,或者修改现有的方法。

在下面示例中,Animal 是一个基类,定义了一个通用的 speak 方法。DogCat 是派生类,它们覆盖了 speak 方法以提供特定的实现。通过继承和方法覆盖,派生类可以扩展基类的功能,同时保持代码的可重用性和组织性

在Python的面向对象编程中,self 是一个特殊的参数,用于指向类的实例本身。它在类的方法定义中必须作为第一个参数出现,但在调用方法时不需要显式传递。

当创建一个类的实例并调用其方法时,Python会自动将实例本身作为 self 参数传递给方法。如下历我们建了一个 Dog 实例 dog = Dog("Buddy"),然后调用 dog.speak(),Python会将 dog 作为 self 参数传递给 Dog 类的 speak 方法。

class Animal:
    # Animal类的构造方法(也称为初始化方法),用于在创建类的实例时初始化对象的状态。
    # self代表新创建的Animal实例,name是传递给构造方法的参数。
    def __init__(self, name):
        self.name = name

    # 抽象方法,用于定义动物的发声行为。
    # 它在基类中没有具体实现,而是要求派生类提供具体的实现。
    # self表示调用speak方法的Animal实例。
    def speak(self):
        raise NotImplementedError("Subclasses must implement this method")


class Dog(Animal):
    # Dog类对speak方法的实现,用于返回狗的叫声。
    # self表示调用speak方法的Dog实例。
    def speak(self):
        return "Woof!"


class Cat(Animal):
    def speak(self):
        return "Meow!"


# 创建类的实例
dog = Dog("Buddy")
cat = Cat("Whiskers")

# 调用实例的方法
print(dog.speak())  # Woof!
print(cat.speak())  # Meow!

1. 名称Names和对象Objects

在Python中,名称(Names)和对象(Objects)是两个基本概念,对象之间相互独立。

名称(Names):

  • 定义:名称是变量、函数、类等的标识符,用于在代码中引用对象。它们是存储在命名空间中的字符串,指向内存中的对象。
  • 作用域:名称存在于特定的作用域内,例如全局作用域、局部作用域或嵌套作用域。作用域决定了名称的可见性和生命周期。
  • 绑定:名称通过赋值操作与对象绑定。一个名称可以绑定到不同的对象,也可以有多个名称绑定到同一个对象。

对象(Objects):

  • 定义:对象是Python中一切事物的实例,包括数字、字符串、列表、函数、类等。每个对象都有一个唯一的身份(内存地址)、一组类型信息和一些数据。
  • 独立性:对象在内存中独立存在,它们可以被多个名称引用,但对象本身不会因为名称的变化而改变。

名称和对象的关系:

  • 别名(Alias):当多个名称绑定到同一个对象时,这些名称被称为该对象的别名。别名在Python中非常常见,因为Python是动态类型语言,变量的类型可以在运行时改变。
  • 内存管理:Python使用引用计数和垃圾回收机制来管理内存。当一个对象的引用计数变为0时,对象会被自动销毁。别名会影响对象的引用计数,因为每个别名都增加了一个引用。
  • 传递对象:在Python中,函数参数传递是通过引用传递的。这意味着当你将一个对象作为参数传递给函数时,实际上是传递了对象的引用(类似于指针),而不是对象本身。因此,函数内部对对象的修改会反映到所有引用该对象的地方。

在下面这个示例中,my_listanother_list 是同一个列表对象的两个名称(别名)。当我们通过 another_list 修改列表时,my_list 也反映了这一更改,因为它们都指向同一个对象。

import sys

# 创建一个列表对象
my_list = [1, 2, 3]

# 创建别名
another_list = my_list

# 修改对象
another_list.append(4)

# 打印两个名称引用的对象
print(my_list)       # [1, 2, 3, 4]
print(another_list)  # [1, 2, 3, 4]

# 查看对象的引用计数
ref_count = sys.getrefcount(my_list)
print(f"Reference count of my_list: {ref_count}")
# 3

2. 作用域和命名空间

2.1 命名空间

命名空间(Namespace)是一个从名称到对象的映射。Python 中的大部分命名空间通过字典实现。命名空间的主要作用是避免命名冲突,确保名称的唯一性。

命名空间的类型:

  1. 内置命名空间(Built-in Namespace):
    • 包含 Python 内置函数和异常(如 printlenTypeError 等)。
    • 在 Python 解释器启动时创建,程序结束时销毁。
  2. 全局命名空间(Global Namespace):
    • 包含模块级别的变量、函数和类。
    • 在模块被导入时创建,程序结束时销毁。
  3. 局部命名空间(Local Namespace):
    • 包含函数或方法内部的变量、函数和类。
    • 在函数或方法调用时创建,函数或方法返回时销毁。
# 全局命名空间
x = 10  # 全局变量

def foo():
    # 局部命名空间
    y = 20  # 局部变量
    print(x, y)  # 访问全局变量和局部变量

foo()  # 10 20

命名空间的特点:

  • 独立性:不同命名空间中的名称互不干扰。例如,两个模块中可以定义同名的函数而不会冲突。
  • 属性引用:通过点号(.)访问对象的属性。例如,z.real 中的 real 是对象 z 的属性。
  • 模块命名空间:模块中的全局名称和模块属性共享相同的命名空间。例如,modname.funcname 中的 funcname 是模块 modname 的属性。

2.2 作用域

作用域(Scope)是命名空间在程序中的可见范围。Python 中的作用域遵循 LEGB 规则

  • L(Local):局部作用域,包含函数内部的变量。
  • E(Enclosing):嵌套函数的外层函数作用域。
  • G(Global):全局作用域,包含模块级别的变量。
  • B(Built-in):内置作用域,包含 Python 内置名称。

作用域的查找顺序:当访问一个变量时,Python 会按照 LEGB 顺序查找:

  1. 先在局部作用域(Local)中查找。
  2. 如果未找到,则在外层函数作用域(Enclosing)中查找。
  3. 如果仍未找到,则在全局作用域(Global)中查找。
  4. 如果仍未找到,则在内置作用域(Built-in)中查找。
  5. 如果仍未找到,则抛出 NameError 异常。
x = 10  # 全局作用域

def outer():
    y = 20  # 外层函数作用域

    def inner():
        z = 30  # 局部作用域
        print(x, y, z)  # 访问全局、外层和局部变量

    inner()

outer()  # 10 20 30

2.3 globalnonlocal关键字

  • global:用于在函数内部修改全局变量。
  • nonlocal:用于在嵌套函数中修改外层函数的变量。
x = 10  # 全局变量

def outer():
    y = 20  # 外层函数变量

    def inner():
        nonlocal y  # 修改外层函数变量
        global x    # 修改全局变量
        y += 1
        x += 1
        print(x, y)

    inner()

outer()  # 11 21

2.4 命名空间与作用域的关系

  • 命名空间是名称到对象的映射。
  • 作用域是命名空间在程序中的可见范围。
  • 每个作用域对应一个命名空间。
x = 10  # 全局命名空间

def foo():
    y = 20  # 局部命名空间
    print(x, y)  # 访问全局和局部命名空间

foo()  # 10 20

2.5 动态名称解析与静态名称解析

  • 动态名称解析:Python在运行时动态查找名称。
  • 静态名称解析:Python正在向编译时静态名称解析发展,局部变量已经静态确定。
def foo():
    x = 10  # 局部变量
    print(x)

foo()  # 10

2.6 作用域与命名空间的示例

下面例子展示了局部、非局部和全局变量的行为:

spam = "spam out of func"  # 全局变量

def scope_test():
    spam = "test spam"  # 外层函数变量

    def do_local():
        spam = "local spam"  # 局部变量

    def do_nonlocal():
        nonlocal spam  # 修改外层函数变量
        spam = "nonlocal spam"

    def do_global():
        global spam  # 修改全局变量
        spam = "global spam"

    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)


scope_test()
print("In global scope:", spam)
# After local assignment: test spam
# After nonlocal assignment: nonlocal spam
# After global assignment: nonlocal spam
# In global scope: global spam

总结:

  • 命名空间:名称到对象的映射,分为内置、全局和局部命名空间。
  • 作用域:命名空间的可见范围,遵循 LEGB 规则。
  • globalnonlocal:用于修改全局和外层函数的变量。
  • 动态与静态名称解析:Python 在运行时动态查找名称,但局部变量已经静态确定。

3. 类(Class)

3.1 类定义

类定义通过 class 关键字实现。类定义中的语句通常是函数定义(方法),但也可以包含其他语句。 类定义(Class Definition)与函数定义 (def 语句) 一样必须被执行才会起作用。

class ClassName:
    <statement-1>
    ...
    <statement-N>

类的例子:

class MyClass:
    i = 12345 # 类变量(类属性)

    # 构造方法,用于初始化类的实例
    def __init__(self, name, data):
        self.name = name  # 实例属性
        self.data = []  # 实例属性

    # 实例方法
    def append(self, value):
        self.data.append(value)

    # 实例方法
    def get_name(self):
        return self.name


# 类对象属性引用
print(MyClass.i)  # 12345
# 类对象属性被赋值
MyClass.i = 10
print(MyClass.i)
# 10

# 实例方法的引用
print(MyClass.append)  # <function MyClass.f at 0x73904bbe79a0>

# 类MyClass实例化
obj = MyClass("Example", None)
print(obj.data) # []

# 修改类变量
obj.i = 321
print(obj.i)  
# 100

# 使用实例方法 append 向 data 属性中添加数据
obj.append(10)
obj.append(20)

# 访问实例属性
print(obj.name)  # Eobjample
print(obj.data)  # [10, 20]

# 动态添加实例变量
obj.counter = 1
print(obj.counter)  # 1
obj.counter += 1
print(obj.counter)  # 2

实例方法引用:

  • 定义:实例方法是定义在类中的方法,它们的第一个参数通常是 self,表示类的实例。实例方法需要通过类的实例来调用。
  • 引用方式:当你通过类名访问一个实例方法时(如 MyClass.append),你得到的是该方法的函数对象引用。这个引用指向类定义中的方法函数,但它还不是绑定到任何特定实例的方法。
  • 调用方式:要调用实例方法,你需要先创建类的实例,然后通过实例来调用该方法。例如:
obj = MyClass("Example", [])
obj.append(10)  # 正确的实例方法调用

类的实例化(instantiation)使用函数表示法。 可以把类对象(class object)看作是一个不带参数的函数,这个函数返回了该类的一个新实例。

在上面的例子中,obj = MyClass()创建了MyClass()这个类的一个实例,并赋值给局部变量obj。实例化操作(调用类对象)会创建一个空对象。许多类会创建带有特定初始状态的自定义实例。为此类定义中需要包含一个名为__init__()的特殊方法。

小结:

  • 命名空间:类定义会创建一个新的命名空间,用于存储类变量和方法。
  • 类对象:类定义执行后,会创建一个类对象,封装了类命名空间中的内容。
  • 类属性引用:通过ClassName.attribute访问类属性,如:MyClass.i。类属性也可以被赋值,因此可以通过赋值来更改MyClass.i的值。
  • 通过类名MyClass访问一个实例方法append时(MyClass.append),得到的是append方法的函数对象引用。这个引用指向类定义中的方法函数,但它还不是绑定到任何特定实例的方法。
  • print(MyClass.append) 是对类中定义的实例方法的引用,而不是类方法引用。类方法是使用 @classmethod 装饰器定义的方法,它们的第一个参数通常是 cls,表示类本身。类方法可以通过类名直接调用,也可以通过实例调用。

3.2 类对象

类对象支持两种操作:

  1. 类对象属性引用:通过obj.name访问类属性和方法。如上例中MyClass.i
  2. 类对象实例化:通过调用类对象创建类的实例,如上例中obj = MyClass("Example", None)
  3. 类变量:类变量是类的所有实例共享的属性,如上例中i = 12345
  4. 实例变量:实例变量是每个实例独有的属性,如上例中self.nameself.data

3.3 实例对象

对实例对象(Instance Objects)唯一的操作是属性引用。有两种有效的属性名称:数据属性(data attributes)和方法(methods)。

在上面的示例中,objMyClass 的一个实例对象。通过调用 obj.append(10)obj.append(20),我们修改了实例的 data 属性。同时,我们也可以直接访问实例的 name 属性。

这些属性和方法都是与特定实例相关联的,不同的实例将具有不同的属性值。

实例属性(Instance Attributes):

  • self.name:这是一个实例属性,用于存储对象的名称。它在 __init__ 方法中被初始化,并且每个 MyClass 的实例都会有自己的 name 属性值。
  • self.data:这是一个实例属性,用于存储与对象相关的数据。它被初始化为一个空列表,并且可以在对象的生命周期内被修改和扩展。

方法(Methods)

  • 实例属性引用称为方法(methods)。 方法是隶属于对象的 函数。在以下讨论中,我们使用方法一词将专指 类实例对象的方法
  • 实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类定义中所包含的所有函数对象(function objects)都称为属性。
  • __init__(self, name, data):这是一个特殊的方法,称为构造方法或初始化方法。它在创建类的新实例时自动调用,用于初始化实例的属性。虽然它看起来像一个普通方法,但它实际上用于设置实例的初始状态。
  • append(self, value):这是一个普通的实例方法,用于向 self.data 列表中添加一个新值。这个方法可以被类的实例调用,以修改实例的状态。

区分几个概念:

  • 在上面的示例中,obj.append是有效的方法引用,因为MyClass.append是一个函数,而obj.i不是方法,因为MyClass.i不是函数。
  • 但是obj.appendMyClass.append并不是一回事,obj.append是一个 方法对象,而MyClass.append是一个 函数对象。差别在于append()是否与实例绑定,未绑定,就是函数,绑定,就是方法。

3.4 方法对象

方法对象是绑定到实例的函数对象。调用方法时,实例会自动作为第一个参数(self)传递。如上例中append_method是一个方法对象,它是 obj 实例的 append 方法。

小结:

  • 函数(function)是Python中一个可调用对象(callable), 方法(method)是一种特殊的函数。
  • 一个可调用对象是方法和函数,和这个对象无关,仅和这个对象是否与类或实例绑定有关(bound method)。
  • 静态方法没有和任何类或实例绑定,所以 静态方法是个函数

3.5 类和实例变量

  • 类变量:类的所有实例共享的属性,如上例中:i = 12345
  • 实例变量:每个实例独有的属性,如上例中:self.nameobj.counter是动态添加实例变量。如果实例变量和类变量同名,实例变量优先。

在Python中,类变量(Class Variable)和类属性(Class Attribute)通常指的是同一个概念。它们都用于描述定义在类中但不在任何方法内的变量,这些变量属于类的命名空间,而不是实例的命名空间。类变量在所有实例之间共享。

当一个实例的非数据属性被引用时,将搜索实例所属的类。

如果同样的属性名称同时出现在实例和类中,则属性查找会 优先选择实例,上例中obj.i = 321就体现了这个优先顺序。

如果被引用的属性名称是类中一个有效的函数对象,则会创建一个抽象的对象,通过打包(parking,即指向)匹配到的实例对象和函数对象,这个抽象对象就是 方法对象

当带参数调用方法对象时,将基于实例对象和参数列表构建一个新的参数列表,并使用这个新参数列表调用相应的函数对象。

数据属性(Data attributes)可以被方法(method)以及一个对象的普通用户(ordinary users)(客户端Client)所引用。 换句话说,类不能用于实现纯抽象数据类型。

任何一个作为类属性(class attribute)的函数对象(function object)都为该类的实例定义了一个相应方法。

方法的第一个参数常常被命名为self,这只是一个约定。方法(methods)可以通过使用self参数的方法属性(method attributes)调用其他方法(method)。方法可以通过与普通函数相同的方式引用全局名称。

类成员操作(不推荐):

成员属性:

  • 访问:ClassName.AttributeName
  • 修改:ClassName.AttributeName = NewValue,等于给这个类对象创建了一个自己的属性,通过这个类创建的对象都具有这个属性。
  • 添加:ClassName.NewAttributeName = Value,等于给这个类对象创建了一个自己的属性,通过这个类创建的对象都具有这个属性。
  • 删除:del ClassName.AttributeName,注意,只能删除类对象自己的属性,通过这个类创建的对象都不再具有这个属性。

成员方法:

  • 访问:ClassName.MethodName()
  • 修改:ClassName.MethodName = NewFunction,等于给这个类对象创建了一个自己的方法,通过这个类创建的对象都具有这个方法。
  • 添加:ClassName.MethodName = Function,等于给这个类对象创建了一个自己的方法,通过这个类创建的对象都具有这个方法。
  • 删除:del ClassName.MethodName,注意,只能删除类对象自己的方法,通过这个类创建的对象都不再具有这个方法。

3.6 魔术方法

魔术方法(Magic Method),也称为特殊方法或双下方法,是Python中以双下划线__开头和结尾的特殊方法,它们通常用于实现特定的内置功能或操作符重载。

以下是一些常见的魔术方法示例,以及如何在类中定义它们:

  • __init__:构造方法,用于初始化类的实例。
def __init__(self, name, data):
    self.name = name
    self.data = []
  • __str__:返回对象的字符串表示,通常用于打印对象时提供更友好的输出。
def __str__(self):
    return f"MyClass(name={self.name}, data={self.data})"
  • __repr__:返回对象的官方字符串表示,通常用于调试。
def __repr__(self):
    return f"MyClass(name={self.name!r}, data={self.data!r})"
  • __len__:返回对象的长度,通常用于支持 len() 函数。
def __len__(self):
    return len(self.data)
  • __getitem__:支持索引操作,通常用于访问序列或映射中的元素。
def __getitem__(self, index):
    return self.data[index]
  • __setitem__:支持索引赋值操作,通常用于修改序列或映射中的元素。
def __setitem__(self, index, value):
    self.data[index] = value

修改上面的MyClass的例子。

class MyClass:
    i = 12345  # 类变量

    def __init__(self, name, data):
        self.name = name  # 实例变量
        self.data = []  # 实例变量

    def append(self, value):
        self.data.append(value)

    def get_name(self):
        return self.name

    def __str__(self):
        return f"MyClass(name={self.name}, data={self.data})"

    def __repr__(self):
        return f"MyClass(name={self.name!r}, data={self.data!r})"

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

调用上例MyClass中的魔术方法。

# 创建 MyClass 的一个实例
obj = MyClass("Example", None)

# 使用实例方法 append 向 data 属性中添加数据
obj.append(10)
obj.append(20)

# 访问实例属性
print(obj.name)  # Example
print(obj.data)  # [10, 20]

# 使用魔术方法 __str__ 和 __repr__
print(obj)  # MyClass(name=Example, data=[10, 20])
print(repr(obj))  # MyClass(name='Example', data=[10, 20])

# 使用魔术方法 __len__
print(len(obj))  # 2

# 使用魔术方法 __getitem__ 和 __setitem__
print(obj[0])  # 10
obj[1] = 30
print(obj.data)  # [10, 30]

比较魔术方法

比较魔术方法允许你定义对象之间的比较行为,如等于、不等于、大于、小于等。

  • __eq__(self, other):定义等于(==)操作符的行为。
  • __ne__(self, other):定义不等于(!=)操作符的行为。
  • __lt__(self, other):定义小于(<)操作符的行为。
  • __le__(self, other):定义小于等于(<=)操作符的行为。
  • __gt__(self, other):定义大于(>)操作符的行为。
  • __ge__(self, other):定义大于等于(>=)操作符的行为。
class MyClass:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if isinstance(other, MyClass):
            return self.value == other.value
        return NotImplemented

    def __ne__(self, other):
        return not self.__eq__(other)

    def __lt__(self, other):
        if isinstance(other, MyClass):
            return self.value < other.value
        return NotImplemented

    def __le__(self, other):
        return self.__lt__(other) or self.__eq__(other)

    def __gt__(self, other):
        return not self.__le__(other)

    def __ge__(self, other):
        return not self.__lt__(other)

算术运算符魔术方法

算术运算符魔术方法允许你定义对象之间的算术运算行为,如加、减、乘、除等。

  • __add__(self, other):定义加(+)操作符的行为。
  • __sub__(self, other):定义减(-)操作符的行为。
  • __mul__(self, other):定义乘(*)操作符的行为。
  • __truediv__(self, other):定义除(/)操作符的行为。
  • __floordiv__(self, other):定义整除(//)操作符的行为。
  • __mod__(self, other):定义取模(%)操作符的行为。
class MyClass:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        if isinstance(other, MyClass):
            return MyClass(self.value + other.value)
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, MyClass):
            return MyClass(self.value - other.value)
        return NotImplemented

    def __mul__(self, other):
        if isinstance(other, MyClass):
            return MyClass(self.value * other.value)
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, MyClass):
            if other.value == 0:
                raise ZeroDivisionError("division by zero")
            return MyClass(self.value / other.value)
        return NotImplemented

    def __floordiv__(self, other):
        if isinstance(other, MyClass):
            if other.value == 0:
                raise ZeroDivisionError("division by zero")
            return MyClass(self.value // other.value)
        return NotImplemented

    def __mod__(self, other):
        if isinstance(other, MyClass):
            if other.value == 0:
                raise ZeroDivisionError("division by zero")
            return MyClass(self.value % other.value)
        return NotImplemented

反向算术运算符魔术方法

反向算术运算符魔术方法允许你定义对象在右侧时的算术运算行为,如 other + self。

  • __radd__(self, other):定义反向加(+)操作符的行为。
  • __rsub__(self, other):定义反向减(-)操作符的行为。
  • __rmul__(self, other):定义反向乘(*)操作符的行为。
  • __rtruediv__(self, other):定义反向除(/)操作符的行为。
  • __rfloordiv__(self, other):定义反向整除(//)操作符的行为。
  • __rmod__(self, other):定义反向取模(%)操作符的行为。
class MyClass:
    def __init__(self, value):
        self.value = value

    def __radd__(self, other):
        if isinstance(other, (int, float)):
            return MyClass(other + self.value)
        return NotImplemented

    def __rsub__(self, other):
        if isinstance(other, (int, float)):
            return MyClass(other - self.value)
        return NotImplemented

    def __rmul__(self, other):
        if isinstance(other, (int, float)):
            return MyClass(other * self.value)
        return NotImplemented

    def __rtruediv__(self, other):
        if isinstance(other, (int, float)):
            if self.value == 0:
                raise ZeroDivisionError("division by zero")
            return MyClass(other / self.value)
        return NotImplemented

    def __rfloordiv__(self, other):
        if isinstance(other, (int, float)):
            if self.value == 0:
                raise ZeroDivisionError("division by zero")
            return MyClass(other // self.value)
        return NotImplemented

    def __rmod__(self, other):
        if isinstance(other, (int, float)):
            if self.value == 0:
                raise ZeroDivisionError("division by zero")
            return MyClass(other % self.value)
        return NotImplemented

增量赋值魔术方法

增量赋值魔术方法允许你定义对象在增量赋值操作(如 +=、-=、*= 等)时的行为。

  • __iadd__(self, other):定义增量加(+=)操作符的行为。
  • __isub__(self, other):定义增量减(-=)操作符的行为。
  • __imul__(self, other):定义增量乘(*=)操作符的行为。
  • __itruediv__(self, other):定义增量除(/=)操作符的行为。
  • __ifloordiv__(self, other):定义增量整除(//=)操作符的行为。
  • __imod__(self, other):定义增量取模(%=)操作符的行为。
class MyClass:
    def __init__(self, value):
        self.value = value

    def __iadd__(self, other):
        if isinstance(other, MyClass):
            self.value += other.value
            return self
        return NotImplemented

    def __isub__(self, other):
        if isinstance(other, MyClass):
            self.value -= other.value
            return self
        return NotImplemented

    def __imul__(self, other):
        if isinstance(other, MyClass):
            self.value *= other.value
            return self
        return NotImplemented

    def __itruediv__(self, other):
        if isinstance(other, MyClass):
            if other.value == 0:
                raise ZeroDivisionError("division by zero")
            self.value /= other.value
            return self
        return NotImplemented

    def __ifloordiv__(self, other):
        if isinstance(other, MyClass):
            if other.value == 0:
                raise ZeroDivisionError("division by zero")
            self.value //= other.value
            return self
        return NotImplemented

    def __imod__(self, other):
        if isinstance(other, MyClass):
            if other.value == 0:
                raise ZeroDivisionError("division by zero")
            self.value %= other.value
            return self
        return NotImplemented

布尔值魔术方法

布尔值魔术方法允许你定义对象在布尔上下文中的行为,如 if 语句、while 循环等。

  • __bool__(self):定义对象在布尔上下文中的行为。
  • __len__(self):定义对象的长度,如果长度为0,则对象在布尔上下文中被视为False
class MyClass:
    def __init__(self, value):
        self.value = value

    def __bool__(self):
        return self.value != 0

    def __len__(self):
        return len(self.value) if isinstance(self.value, (list, tuple, str)) else 0

容器魔术方法

容器魔术方法允许你定义对象作为容器的行为,如支持迭代、索引、切片等。

  • __iter__(self):定义对象的迭代器。
  • __next__(self):定义迭代器的下一个元素。
  • __getitem__(self, key):定义对象的索引操作。
  • __setitem__(self, key, value):定义对象的索引赋值操作。
  • __delitem__(self, key):定义对象的索引删除操作。
class MyClass:
    def __init__(self, data):
        self.data = data

    def __iter__(self):
        self.index = 0
        return self

    def __next__(self):
        if self.index < len(self.data):
            result = self.data[self.index]
            self.index += 1
            return result
        else:
            raise StopIteration

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __delitem__(self, key):
        del self.data[key]

3.7 函数内省

函数内省(Function Introspection)是指在运行时检查函数的属性和行为的能力。

假设我们有以下 MyClass 类定义:

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

    def __str__(self):
        return f"MyClass(value={self.value})"

    def __add__(self, other):
        if isinstance(other, MyClass):
            return MyClass(self.value + other.value)
        return NotImplemented

    def __len__(self):
        return len(str(self.value))

    def my_method(self):
        """This is a method that does something."""
        return self.value * 2

函数内省技术:

__name__ 属性

每个函数都有一个 __name__ 属性,返回函数的名称。对于类方法,__name__ 属性返回方法的名称。

print(MyClass.my_method.__name__)  # 输出: my_method

__doc__ 属性

每个函数都有一个 __doc__ 属性,返回函数的文档字符串。文档字符串通常用于描述函数的功能和用法。

print(MyClass.my_method.__doc__)  # 输出: This is a method that does something.

inspect 模块

  • inspect 模块提供了许多有用的函数,用于检查对象的属性和行为。
  • inspect.getmembers() 可以列出对象的所有成员。
  • inspect.signature() 可以获取函数的签名,包括参数和返回值类型。
import inspect

# 列出 MyClass 的所有成员
members = inspect.getmembers(MyClass)
for name, member in members:
    print(f"{name}: {member}")

# 获取 my_method 的签名
signature = inspect.signature(MyClass.my_method)
print(signature)  # 输出: (self)

dir() 函数

dir() 函数返回对象的属性和方法列表。对于类,dir() 返回类的属性和方法名称。

print(dir(MyClass))  # 输出: ['__add__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'my_method']

type() 函数

type() 函数返回对象的类型。对于类方法,type() 返回方法的类型,通常是 function 或 method。

print(type(MyClass.my_method))  # 输出: <class 'function'>

callable() 函数

callable() 函数检查对象是否可调用。对于类方法,callable() 返回 True

print(callable(MyClass.my_method))  # 输出: True

3.8 反射

反射(reflection)是动态语言的一个特性。

反射机制 指的是在程序的运行状态中,对于任意一个类,都可以知道这个类的所有属性和方法;对于任意一个对象,都能够调用他的任意方法和属性。这种动态获取程序信息以及动态调用对象的功能称为反射机制。

反射的核心功能包括:

  • 动态获取属性或方法(getattr)。
  • 动态设置属性(setattr)。
  • 动态删除属性(delattr)。
  • 动态检查属性或方法是否存在(hasattr)。
class MyClass:
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return f"MyClass(value={self.value})"

    def __add__(self, other):
        if isinstance(other, MyClass):
            return MyClass(self.value + other.value)
        return NotImplemented

    def __len__(self):
        return len(str(self.value))

    def my_method(self):
        """This is a method that does something."""
        return self.value * 2

# 创建对象
obj = MyClass(10)

# 反射:动态获取属性
value = getattr(obj, 'value')
print(value)  # 输出: 10

# 反射:动态调用方法
method = getattr(obj, 'my_method')
print(method())  # 输出: 20

# 反射:动态设置属性
setattr(obj, 'value', 20)
print(obj.value)  # 输出: 20

# 反射:动态检查属性是否存在
print(hasattr(obj, 'value'))  # 输出: True
print(hasattr(obj, 'non_existent'))  # 输出: False

对比反射和内省

在 Python 中,反射(Reflection)内省(Introspection) 是两个相关但不同的概念。

内省是指在运行时获取对象的元数据(如属性、方法、注解等),用于分析和理解对象的结构和行为。内省的核心功能包括:

  • 获取对象的属性和方法列表(dir)。
  • 获取函数的参数信息(inspect.signature)。
  • 获取函数的注解(__annotations__)。
  • 获取函数的文档字符串(__doc__)。
# 内省:获取对象的属性和方法列表
print(dir(obj))
# 输出: ['__add__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'my_method', 'value']

# 内省:获取方法的文档字符串
print(obj.my_method.__doc__)
# 输出: This is a method that does something.

# 内省:获取方法的参数信息(使用 inspect 模块)
from inspect import signature
sig = signature(obj.my_method)
print(sig)
# 输出: (self)

反射和内省的不同点:

特性 反射(Reflection) 内省(Introspection)
核心功能 动态获取、设置、删除属性和方法。 获取对象的元数据(如属性、方法、注解等)。
主要用途 动态操作对象,实现灵活的程序逻辑。 分析和理解对象的结构和行为。
常用工具 getattrsetattrdelattrhasattr dir__annotations____doc__inspect 模块。
示例场景 动态调用方法、动态设置属性。 获取函数的参数信息、注解、文档字符串。

3.9 元类

所有的对象都是实例化或者说调用类而得到的(调用类的过程称为类的实例化)。

class StandfordProfessor(object):
    university = 'Standford'

    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def display(self):
        print(f'Professor {self.name} says welcome to {self.university}!')

professor = StandfordProfessor('Tom', 'Male')

print(type(StandfordProfessor))
# <class 'type'>

上例中,对象professor是调用类StandfordProfessor得到的。类StandfordProfessor本质也是一个对象,通过type(StandfordProfessor)可以验证,StandfordProfessor是调用了内置的类type得到的。这个type称为 元类(metaclass)

如果一个类没有声明自己的元类,默认它的元类就是type,除了使用内置元类type,我们也可以通过继承type来自定义元类,然后使用metaclass关键字参数为一个类的指定元类。

只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类。

下面通过自定义元类来改写上面的代码。

定义Mymeta类的作用是作为一个元类(metaclass),它定义了当 StandfordProfessor 类被实例化时的行为。 元类是类的类,即它们用于创建类,而不是实例。 通过自定义元类,可以控制类的创建过程,包括实例化时的行为。

要想让professor这个对象变成一个可调用的对象,需要在该对象的类中定义一个方法__call__,该方法会在调用对象时自动触发。 调用professor的返回值就是__call__方法的返回值。

class Mymeta(type):

    def __call__(self, *args, **kwargs):
        print(self)  # 类名
        print(args)  # 输入参数
        print(kwargs)  # 输入参数
        return 10086


class StandfordProfessor(object, metaclass=Mymeta):
    university = "Standford"

    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def display(self):
        print(f"Professor {self.name} says welcome to {self.university}!")


professor = StandfordProfessor("Tom", "Male")
# <class '__main__.StandfordProfessor'>
# ('Tom', 'Male')
# {}

professor = StandfordProfessor("Tom", "Male", age=30, title="Professor")
# <class '__main__.StandfordProfessor'>
# ('Tom', 'Male')
# {'age': 30, 'title': 'Professor'}

输出结果解释:

  • print(self):输出类名 <class '__main__.StandfordProfessor'>
  • print(args):输出位置参数 ('Tom', 'Male')
  • print(kwargs):输出关键字参数 {}(在这个例子中没有关键字参数)。
  • return 10086:返回值 10086,而不是 StandfordProfessor 的实例。

类的产生过程其实就是元类的调用过程,即StandfordProfessor = Mymeta('StandfordProfessor', (object), {...}),调用Mymeta会先产生一个空对象StandfordProfessor,然后连同调用Mymeta括号内的参数一同传给Mymeta下的__init__方法,完成初始化。

我们可以基于上例做如下改写。

class Mymeta(type):

    def __init__(self, class_name, class_bases, class_dic):
        super(Mymeta, self).__init__(class_name, class_bases, class_dic)

        if class_name.islower():
            raise TypeError(f'Please follow Camel-Case to change class name {class_name}')
        if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
            raise TypeError('Please add documentation in class {class_name}, which is mandatory.')


class StandfordProfessor(object, metaclass=Mymeta):
    """
    Documentation of class StanfordTeacher
    """

    university = 'Standford'

    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def display(self):
        print(f'Professor {self.name} says welcome to {self.university}!')


professor = StandfordProfessor('Tom', 'Male')

professor.display()
# Professor Tom says welcome to Standford!

print(professor.__dict__)
# {'name': 'Tom', 'gender': 'Male'}

StandfordProfessor.mro()
# [<class '__main__.StandfordProfessor'>, <class 'object'>]

4.何时使用面向对象

这段代码实现了一个计算多边形周长的功能。以下是逐行解释:

  • hypot 函数:计算直角三角形的斜边长度(即欧几里得距离,即计算两点之间的直线距离。)。
  • 公式:hypot(x, y) 等价于 sqrt(x**2 + y**2)
  • polygon 是一个列表,包含多边形的顶点坐标,格式为 [(x1, y1), (x2, y2), ...]
  • 生成顶点对:
    • polygon[1:] + polygon[:1]:将列表的第一个元素移到末尾,形成闭合环。
    • 例如,polygon = [(1, 1), (1, 2), (2, 2), (2, 1)],则 polygon[1:] + polygon[:1][(1, 2), (2, 2), (2, 1), (1, 1)]
    • zip(polygon, polygon[1:] + polygon[:1]):将原列表和移动后的列表配对,生成顶点对。
    • 例如,pairs = [((1, 1), (1, 2)), ((1, 2), (2, 2)), ((2, 2), (2, 1)), ((2, 1), (1, 1))]
  • 计算周长:
    • 使用生成器表达式 distance(p1, p2) for p1, p2 in pairs 计算每对顶点之间的距离。
    • 使用 sum 函数将所有距离相加,得到多边形的周长。
from math import hypot

# 计算两点 `p1` 和 `p2` 之间的欧几里得距离
def distance(p1, p2):
    return hypot(p2[0] - p1[0], p2[1] - p1[1])

# 计算多边形的周长
def perimeter(polygon):
    # 生成顶点对(确保多边形闭合)
    pairs = zip(polygon, polygon[1:] + polygon[:1])
    # 计算每对顶点之间的距离并求和
    return sum(distance(p1, p2) for p1, p2 in pairs)


if __name__ == "__main__":
    # 计算多边形周长
    square = [(1, 1), (1, 2), (2, 2), (2, 1)]
    print(perimeter(square))  # 输出: 4.0
    # 计算三角形的周长
    triangle = [(0, 0), (1, 0), (0.5, 1)]
    print(perimeter(triangle))  # # 输出: 3.236...

上面这段代码的缺点是不容易理解。我们需要通读所有代码,才能理解这两个函数如何协作。

改进:我们通过类型提示来澄清函数的意图,修改后的代码如下:

  • 定义了两个类型(PointPolygon)来声明函数的意图。Point就是内置的tuple类。Polygon是由Point类组成的内置list类。
from math import hypot

# 定义类型别名,增强代码可读性
Point = tuple[float, float]  # 点的坐标类型
Polygon = list[Point]        # 多边形的顶点列表类型

# 计算两点 `p1` 和 `p2` 之间的欧几里得距离
def distance(p1: Point, p2: Point) -> float:
    return hypot(p2[0] - p1[0], p2[1] - p1[1])

# 计算多边形的周长
def perimeter(polygon: Polygon) -> float:
    # 生成顶点对(确保多边形闭合)
    pairs = zip(polygon, polygon[1:] + polygon[:1])
    # 计算每对顶点之间的距离并求和
    return sum(distance(p1, p2) for p1, p2 in pairs)

if __name__ == "__main__":
    # 定义正方形的顶点
    square: Polygon = [(1, 1), (1, 2), (2, 2), (2, 1)]
    print(perimeter(square))  # 输出: 4.0

    # 定义三角形的顶点
    triangle: Polygon = [(0, 0), (1, 0), (0.5, 1)]
    print(perimeter(triangle))  # 输出: 3.236...

从面向对象角度,创建Polygon类,用来封装“点”的列表(数据)和“计算周长”的函数(行为)​。

  • self.vertices[1:]:从第二个顶点开始到列表末尾的所有顶点。
  • self.vertices[:1]:第一个顶点(闭合多边形,使最后一个顶点连接到第一个顶点)。
  • zip(self.vertices, self.vertices[1:] + self.vertices[:1]):将顶点列表与其自身偏移一个位置的列表配对,形成相邻顶点的对。
from math import hypot
from typing import List


class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # 计算当前点与另一个点之间的欧几里得距离
    def distance(self, other: "Point") -> float:
        return hypot(self.x - other.x, self.y - other.y)

    # 重定义__repr__,用以输出点的坐标
    def __repr__(self):
        return f"({self.x}, {self.y})"


class Polygon:
    def __init__(self) -> None:
        self.vertices: List[Point] = []  # 初始化顶点列表为空列表

    # 向多边形中添加一个顶点
    def add_point(self, point: Point) -> None:
        self.vertices.append(point)  # 将点添加到顶点列表中

    # 计算多边形的周长
    def perimeter(self) -> float:
        # 创建一个迭代器,将顶点列表与其自身偏移一个位置的列表配对
        pairs = zip(self.vertices, self.vertices[1:] + self.vertices[:1])
        # 计算每对顶点之间的距离并求和
        return sum(p1.distance(p2) for p1, p2 in pairs)


if __name__ == "__main__":
    # 定义一个正方形的顶点
    square = Polygon()
    square.add_point(Point(1, 1))  # 添加第一个顶点
    square.add_point(Point(1, 2))  # 添加第二个顶点
    square.add_point(Point(2, 2))  # 添加第三个顶点
    square.add_point(Point(2, 1))  # 添加第四个顶点
    print(square.perimeter())  # 输出正方形的周长,结果是 4.0
    print(square.vertices)  # 输出正方形的顶点列表,结果是 [(1, 1), (1, 2), (2, 2), (2, 1)]

如果要在perimeter()中输出pairs的值,则需要修改下面2句。

  • pairs = list(zip(self.vertices, self.vertices[1:] + self.vertices[:1])) # 转换为列表
  • print(f"Pairs: {pairs}") # 打印 pairs 的内容

如果使用原来的pairs语句,则会导致sum(p1.distance(p2) for p1, p2 in pairs)返回结果是0。

  • pairs = zip(self.vertices, self.vertices[1:] + self.vertices[:1])
  • print(f"Pairs: {pairs}") # 打印 pairs 的内容

原因是 zip 函数返回的是一个迭代器,只能被迭代一次。一旦迭代器的内容被提取出来,后续的迭代操作将不会产生任何结果。如果需要多次使用迭代器的内容时,将其转换为列表或其他可重复访问的集合类型。所以需要注意迭代器的耗尽问题,避免在调试代码中耗尽迭代器,导致后续逻辑无法正常工作。

下面对Polygon类继续进行修改,让它能够接收Point对象的列表作为参数进行初始化:

from math import hypot
from typing import List, Optional, Iterable


class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # 计算当前点与另一个点之间的欧几里得距离
    def distance(self, other: "Point") -> float:
        return hypot(self.x - other.x, self.y - other.y)

    # 重定义__repr__,用以输出点的坐标
    def __repr__(self):
        return f"({self.x}, {self.y})"


class Polygon_2:
    def __init__(self, vertices: Optional[Iterable[Point]]) -> None:
        self.vertices = list(vertices) if vertices else []

    def perimeter(self) -> float:
        # 创建一个迭代器,将顶点列表与其自身偏移一个位置的列表配对
        pairs = zip(self.vertices, self.vertices[1:] + self.vertices[:1])
        # 计算每对顶点之间的距离并求和
        return sum(p1.distance(p2) for p1, p2 in pairs)


if __name__ == "__main__":
    # 定义一个正方形的顶点
    square = Polygon_2([Point(1, 1), Point(1, 2), Point(2, 2), Point(2, 1)])
    print(square.perimeter())  # 输出正方形的周长,结果是 4.0
    print(square.vertices)  # 输出正方形的顶点列表,结果是 [(1, 1), (1, 2), (2, 2), (2, 1)]

Polygon类继续进行修改,让它可以接收元组,我们可以根据元组自己创建Point对象。 这个构造方法遍历由Point或者元组组成的列表。如果传入的是元组(Tuple[float,float]),就把它转换成Point对象。

from math import hypot
from typing import List, Optional, Iterable, Union, Tuple

Pair = Tuple[float, float]
Point_or_Tuple = Union["Point", Pair]


class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # 计算当前点与另一个点之间的欧几里得距离
    def distance(self, other: "Point") -> float:
        return hypot(self.x - other.x, self.y - other.y)

    # 重定义__repr__,用以输出点的坐标
    def __repr__(self):
        return f"({self.x}, {self.y})"


class Polygon_3:
    def __init__(self, vertices: Optional[Iterable[Point_or_Tuple]]) -> None:
        self.vertices: list[Point] = []
        if vertices:
            for Point_or_Tuple in vertices:
                self.vertices.append(self.make_point(Point_or_Tuple))

    @staticmethod
    def make_point(item: Point_or_Tuple) -> Point:
        return item if isinstance(item, Point) else Point(*item)

    def perimeter(self) -> float:
        pairs = zip(self.vertices, self.vertices[1:] + self.vertices[:1])
        return sum(p1.distance(p2) for p1, p2 in pairs)


if __name__ == "__main__":
    # 定义一个正方形的顶点,使用元组格式
    square = Polygon_3([(1, 1), (1, 2), (2, 2), (2, 1)])
    print(square.perimeter())  # 输出正方形的周长,结果是 4.0
    print(square.vertices)  # 输出正方形的顶点列表,结果是 ['(1, 1)', '(1, 2)', '(2, 2)', '(2, 1)']