面向对象概念 ¶
找出对象是面向对象分析与编程中非常重要的任务。 记住,对象既有数据又有行为。如果我们只需要处理数据,那么通常更好的方法是将数据存储在列表、集合、字典或其他的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
方法。Dog
和 Cat
是派生类,它们覆盖了 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_list
和 another_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 中的大部分命名空间通过字典实现。命名空间的主要作用是避免命名冲突,确保名称的唯一性。
命名空间的类型:
- 内置命名空间(Built-in Namespace):
- 包含 Python 内置函数和异常(如
print
、len
、TypeError
等)。 - 在 Python 解释器启动时创建,程序结束时销毁。
- 包含 Python 内置函数和异常(如
- 全局命名空间(Global Namespace):
- 包含模块级别的变量、函数和类。
- 在模块被导入时创建,程序结束时销毁。
- 局部命名空间(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 顺序查找:
- 先在局部作用域(Local)中查找。
- 如果未找到,则在外层函数作用域(Enclosing)中查找。
- 如果仍未找到,则在全局作用域(Global)中查找。
- 如果仍未找到,则在内置作用域(Built-in)中查找。
- 如果仍未找到,则抛出
NameError
异常。
x = 10 # 全局作用域
def outer():
y = 20 # 外层函数作用域
def inner():
z = 30 # 局部作用域
print(x, y, z) # 访问全局、外层和局部变量
inner()
outer() # 10 20 30
2.3 global
和nonlocal
关键字 ¶
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 规则。
global
和nonlocal
:用于修改全局和外层函数的变量。- 动态与静态名称解析: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 类对象 ¶
类对象支持两种操作:
- 类对象属性引用:通过
obj.name
访问类属性和方法。如上例中MyClass.i
。 - 类对象实例化:通过调用类对象创建类的实例,如上例中
obj = MyClass("Example", None)
。 - 类变量:类变量是类的所有实例共享的属性,如上例中
i = 12345
。 - 实例变量:实例变量是每个实例独有的属性,如上例中
self.name
和self.data
。
3.3 实例对象 ¶
对实例对象(Instance Objects)唯一的操作是属性引用。有两种有效的属性名称:数据属性(data attributes)和方法(methods)。
在上面的示例中,obj
是 MyClass
的一个实例对象。通过调用 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.append
与MyClass.append
并不是一回事,obj.append
是一个 方法对象,而MyClass.append
是一个 函数对象。差别在于append()
是否与实例绑定,未绑定,就是函数,绑定,就是方法。
3.4 方法对象 ¶
方法对象是绑定到实例的函数对象。调用方法时,实例会自动作为第一个参数(self
)传递。如上例中append_method
是一个方法对象,它是 obj
实例的 append
方法。
小结:
- 函数(function)是Python中一个可调用对象(callable), 方法(method)是一种特殊的函数。
- 一个可调用对象是方法和函数,和这个对象无关,仅和这个对象是否与类或实例绑定有关(bound method)。
- 静态方法没有和任何类或实例绑定,所以 静态方法是个函数。
3.5 类和实例变量 ¶
- 类变量:类的所有实例共享的属性,如上例中:
i = 12345
。 - 实例变量:每个实例独有的属性,如上例中:
self.name
。obj.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) |
---|---|---|
核心功能 | 动态获取、设置、删除属性和方法。 | 获取对象的元数据(如属性、方法、注解等)。 |
主要用途 | 动态操作对象,实现灵活的程序逻辑。 | 分析和理解对象的结构和行为。 |
常用工具 | getattr 、setattr 、delattr 、hasattr 。 | 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...
上面这段代码的缺点是不容易理解。我们需要通读所有代码,才能理解这两个函数如何协作。
改进:我们通过类型提示来澄清函数的意图,修改后的代码如下:
- 定义了两个类型(
Point
和Polygon
)来声明函数的意图。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)']