前言
本文是关于面向对象程序设计的习题整理和讲解
1. 面向对象程序设计的三要素分别为封装继承和多态
2. 双星号(**)在Python中的用法
2.1. 解包操作(可迭代对象(如列表或元组)或字典与函数参数之间转换的过程)
2.1.1双星号(**):提供了字典与关键字参数的相互转换的功能
2.1.1.1字典解包为关键字参数例子
def my_func(a, b):
return a + b
my_dict = {'a': 1, 'b': 2}
result = my_func(**my_dict)
print(result) # Output will be 3
解释:
这里就变成了函数传参为a = 1 ,b = 2;
使用my_dict进行解包操作后,字典中的键值对就变成了函数的关键字参数,这样就可以方便地将字典中的数据传递给函数作为参数。
2.1.1.2 关键字参数解包为字典
示例 1:传递关键字参数
def print_kwargs(**kwargs):
for key, value in kwargs.items():
print(f"{key} = {value}")
# 使用 ** 将字典解包为关键字参数
print_kwargs(a=1, b=2, c=3)
在这个示例中,print_kwargs
函数接收一个**kwargs
参数,它是一个字典。调用print_kwargs(a=1, b=2, c=3)
时,这些关键字参数被解包并传递给函数,相当于:
kwargs = {'a': 1, 'b': 2, 'c': 3}
print_kwargs(**kwargs)
示例 2:动态参数传递
def add(**kwargs):
result = 0
for key in kwargs:
result += kwargs[key]
return result
# 动态地传递任意数量的参数
result = add(a=1, b=2, c=3)
在这个示例中,add
函数可以接收任意数量的参数,因为它们被封装在**kwargs
中。这意味着可以传递任意数量的关键字参数,而不需要知道它们的具体数量或顺序。
示例 3:参数的默认值(有点默认形参的意思了)
def greet(**kwargs):
name = kwargs.get('name', 'Guest')
greeting = kwargs.get('greeting', 'Hello')
print(f"{greeting}, {name}!")
# 传递部分参数
greet(name='Alice')
# 传递全部参数
greet(greeting='Good morning', name='Bob')
在这个示例中,greet
函数使用**kwargs
来接收任意数量的参数。通过使用kwargs.get(key, default)
,我们可以为参数提供默认值。
示例 4:合并多个字典(也适用于大小都不确定的情况)
def combine(**kwargs):
return {key: value for key, value in kwargs.items()}
# 合并多个字典
dict1 = {'a': 1, 'b': 2}
dict2 = {'c': 3, 'd': 4}
combined_dict = combine(**dict1, **dict2)
在这个示例中,我们使用**
将两个字典解包并合并为一个新字典。
####2.1.2 单星号(*):序列和函数参数的转换
-
将序列(如列表或元组)解包为位置参数:
在函数调用中使用*
前缀一个序列时,序列中的每个元素都会被解包为独立的参数。这意味着序列中的元素按照它们在序列中的顺序,变成函数调用中的位置参数。def func(a, b, c): print(a, b, c) args = [1, 2, 3] func(*args) # 等同于 func(1, 2, 3)
在这个例子中,列表
args
中的元素被解包为func
函数的位置参数。 -
解包参数列表:
*
还可以用于在函数定义中收集任意数量的位置参数到一个元组中。def func(*args): for arg in args: print(arg) func(1, 2, 3) # 输出:1 2 3
在这个例子中,
*args
收集了传递给func
的所有额外位置参数,并将它们存储在一个名为args
的元组中。
2.2 与**对应的特殊方法名
特殊方法 __pow__
**自动调用__pow__这个特殊方法
用于实现幂运算的特殊方法。当使用双星号进行幂运算时,会调用对象的__pow__
方法。
class MyClass:
def __init__(self, x):
self.x = x
def __pow__(self, other):
return self.x ** other
obj = MyClass(2)
result = obj ** 3
print(result) # Output will be 8
2. // 对应的特殊方法
概述:在Python中,//
是整除运算符,也称为地板除(floor division)。当使用地板除运算符 //
对两个数进行除法运算时,结果将是一个整数,该整数是商向下取整的结果。
特殊方法:对于Python中的对象,//
运算符对应的特殊方法是 __floordiv__
。当使用 //
对某个对象进行操作时,Python 解释器会在该对象上调用 __floordiv__
方法。
记忆: “floor” (地板) “division”(除法,这个英文单词释义不常见)
3. python中强行改变一个类的私有成员
例如,考虑以下类:
class MyClass:
def __init__(self):
self.__private_attr = 0
在这个类中,__private_attr
是一个私有成员。在类的外部,可以通过名称改写后的名称来直接访问或修改它:
obj = MyClass()
obj._ MyClass__private_attr = 10 # 直接设置私有成员的值
4. python中类的构造方法都是__init__
构造方法在Python中被称为 __init__
方法。每当创建一个新的类实例时,Python 解释器会自动调用这个方法。
以下是 __init__
方法的一些关键点:
- 名称:构造方法的名称固定为
__init__
。 - 调用:创建类的新实例时自动调用。
- 参数:
__init__
方法的第一个参数始终是self
,代表当前的实例对象。之后可以定义其他参数,用于传递初始化所需的值。 - 继承:如果类继承自其他类,则
__init__
方法可以调用父类的__init__
方法,使用super().__init__()
或者ParentClass.__init__(self)
,以确保父类也被正确初始化。 - 返回值:构造方法
__init__
不需要返回值,即使返回了,也会被忽略。
下面是一个简单的类定义,包括 __init__
方法的例子:
class MyClass:
def __init__(self, param1, param2):
self.var1 = param1
self.var2 = param2
# 初始化代码
# 创建实例
my_instance = MyClass('value1', 'value2')
解释:在这个例子中,MyClass
的构造方法 __init__
接受两个参数 param1
和 param2
,并将它们分别赋值给实例变量 var1
和 var2
。
注意:如果一个类没有显式定义 __init__
方法,那么一个默认的 __init__
方法会被隐式提供,这个默认的构造方法什么也不做(即:它是一个空函数)。
5.如果在设计一个类的时候实现了特殊方法__contains__,那么这个类的对象会自动支持什么运算符?
这个类的对象将会自动支持 in
运算符。in
运算符用于检查一个对象是否包含另一个对象。
__contains__
方法通常被实现为接受一个参数,并返回一个布尔值(True
或 False
),表示传入的参数是否存在于该对象中。这个方法被用于实现容器类型的行为,比如列表、集合或字典。
下面是一个简单的例子,展示如何为自定义类实现 __contains__
方法:
class Container:
def __init__(self, items):
self.items = items
def __contains__(self, item):
return item in self.items
# 创建 Container 类的实例
my_container = Container([1, 2, 3, 4, 5])
# 使用 in 运算符
print(2 in my_container) # 输出:True
print(6 in my_container) # 输出:False
6. 如果在设计一个类的时候实现了特殊方法__mul__,那么这个类的对象会自动支持什么运算符?
这个类的对象将会自动支持乘法运算符 *
具体来说,使用 *
运算符时,Python 会在类的实例上调用 __mul__
方法。这通常用于执行两个操作数的乘法运算,比如:
class MyNumber:
def __init__(self, value):
self.value = value
def __mul__(self, other):
return MyNumber(self.value * other.value)
# 创建对象
num1 = MyNumber(2)
num2 = MyNumber(3)
# 使用 * 运算符
result = num1 * num2 # 这是对象*对象,这将调用 num1 的 __mul__ 方法,实际上是对象的value*对象的value
print(result.value) # 输出结果应该是 6
7.如果在设计一个类的时候实现了特殊方法__eq__,那么这个类的对象会自动支持什么运算符?
这个类的对象会自动支持等价比较运算符 ==
。eq
对应英文单词 “equal”,表示相等或等价。
8.如果在设计一个类的时候实现了特殊方法__gt__,那么这个类的对象会自动支持什么运算符?
如果在设计一个类的时候实现了特殊方法 __gt__
,那么这个类的对象会自动支持大于运算符 >
。gt
对应英文单词 “greater than”。
9.如果在设计一个类的时候实现了特殊方法__ne__,那么这个类的对象会自动支持什么运算符?
这个类的对象会自动支持不等于运算符!=
。
ne 对应英文 “not equal”,用于比较两个对象是否不相等。
10.如果在设计一个类的时候实现了特殊方法__ge__,那么这个类的对象会自动支持什么运算符?
这个类的对象会自动支持大于等于运算符 >=
。
__ge__
对应英文 “greater than or equal to”,用于比较两个对象,以确定调用对象是否大于或等于传入的对象。
5~10 特殊方法和运算符之间的整理
特殊方法 | 运算符 | 英文含义 |
---|---|---|
__add__ | + | add |
__sub__ | - | subtract |
__mul__ | * | multiply |
__matmul__ | @ | matrix multiply |
__truediv__ | / | divide |
__floordiv__ | // | floor divide |
__mod__ | % | modulo |
__pow__ | ** | power |
__lshift__ | << | left shift |
__rshift__ | >> | right shift |
__and__ | & | bitwise and |
__or__ | 竖着的斜杠这里因为语法展示不了 | bitwise or |
__xor__ | ^ | bitwise xor |
__contains__ | in | contains |
__eq__ | == | equal |
__ne__ | != | not equal |
__lt__ | < | less than |
__gt__ | > | greater than |
__le__ | <= | less than or equal |
__ge__ | >= | greater than or equal |
__call__ | () | call |
__getitem__ | [] | get item |
__setitem__ | [] | set item |
__delitem__ | [] | delete item |
__iter__ | iterate | |
__next__ | next item | |
__len__ | len() | length |
__str__ | str() | string representation |
__repr__ | repr() | official representation |
解释最后一个:在Python中,repr 是一个特殊方法,它用于返回对象的官方字符串表示形式。
13 .代码结果解释
class A(object):
def __init__(self):
self.__private()
self.public()
def __private(self):
print("__private() method in A")
def public(self):
print("public() method in A")
class B(A):
def __private(self):
print("__private() method in B")
def public(self):
print("public() method in B")
if __name__ == "__main__":
B()
结果是
__private() method in A
public() method in B
解释:
函数调用的顺序
-
创建
B
类的实例:B()
调用了B
类的构造函数。由于B
类没有定义自己的构造函数,所以它隐式地调用了基类A
的构造函数。 -
调用
A
的__init__
构造函数:在A
的__init__
方法中,有两个函数调用:a.
self.__private()
:由于__private
是一个以双下划线开头的方法,Python 会进行名称改写(name mangling),以避免子类中同名方法的冲突。改写后的名称为__A__private
。在B
类中定义的__private
方法实际上对应的是__B__private
,因此,self.__private()
调用的是A
类中定义的__A__private
方法。b.
self.public()
:public
是一个公有方法,没有名称改写。在B
类中也定义了一个同名的public
方法。根据Python的继承规则,如果子类中存在与父类同名的方法,则调用子类中的方法。因此,self.public()
调用的是B
类中定义的public
方法。 -
执行
__A__private
方法:输出__private() method in A
。这是因为A
类的__private
方法被调用。 -
执行
public
方法:输出public() method in B
。这是因为B
类覆盖了A
类的public
方法,并且B
类的实例在调用public
方法时,会调用B
类中定义的版本。
综上所述,代码的执行流程是:
- 创建
B
类的实例,隐式调用A
的__init__
。 - 在
A
的__init__
中,调用A
的__private
方法(名称改写后的__A__private
)。 - 继续在
A
的__init__
中,调用B
的public
方法(因为B
覆盖了A
的public
方法)。
最终结果如下:
__private() method in A
public() method in B
这里体现了Python的两个重要的特性:
-
继承和方法解析顺序(MRO - Method Resolution Order):
Python 使用一种称为 C3 线性化的算法来确定方法解析顺序。当调用一个方法时,Python 首先在当前类中查找方法,如果当前类中不存在该方法,则沿着继承链向上查找基类。如果子类中存在同名的方法,子类中的方法将被优先调用,这是方法覆盖(或称方法重写)的基本概念。 -
私有方法的名称改写(Name Mangling):
Python 中,以双下划线开头的方法或属性名称会经过名称改写,这是为了防止子类中可能的命名冲突。名称改写通过在方法名前添加_ClassName
(其中ClassName
是类名)来实现。这意味着即使方法名看起来相同,不同类中的同名私有方法实际上是不同的,因为它们的全名是不同的。
结合这两个特性,当创建 B
类的实例并调用其构造函数时:
- 由于
B
没有自己的构造函数,它隐式地调用了基类A
的__init__
方法。 - 在
A
的__init__
方法中,调用self.__private()
。由于这是私有方法,它会被名称改写,因此B
类中的__private
方法不会覆盖A
中的__private
方法。所以,调用的是A
中的__private
方法。 - 然后调用
self.public()
。这是一个公有方法,Python 会在B
类中查找是否有同名方法。由于B
类中定义了public
方法,它覆盖了A
类中的public
方法,因此调用的是B
类中的public
方法。
这个机制确保了私有方法的封装性,同时允许公有方法被继承和覆盖,以实现多态性。
15~17.重写特殊方法使得python自动支持内置函数
在Python中,特殊方法(也称为魔术方法)允许定义类的行为,使其能够模拟内置类型的行为。例如,如果为一个类实现了__abs__方法,那么这个类的对象就可以使用内置的abs()函数。以下是一些常见的特殊方法及其对应的内置函数或行为
特殊方法 | 内置函数或行为 |
---|---|
__init__(self, ...) | 初始化实例 |
__del__(self) | 实例销毁时调用 |
__str__(self) | 使用print() 或str() 时的行为 |
__repr__(self) | 使用repr() 时的行为 |
__len__(self) | 使用len() 时的行为 |
__getitem__(self, key) | 使用索引或切片时的行为 |
__setitem__(self, key, value) | 使用索引赋值时的行为 |
__delitem__(self, key) | 使用del 进行删除时的行为 |
__iter__(self) | 实现迭代器协议 |
__next__(self) | 实现迭代器协议的下一个元素 |
__call__(self, ...) | 实例作为函数调用时的行为 |
__add__(self, other) | 使用+ 运算符时的行为 |
__sub__(self, other) | 使用- 运算符时的行为 |
__mul__(self, other) | 使用* 运算符时的行为 |
__truediv__(self, other) | 使用/ 运算符时的行为 |
__mod__(self, other) | 使用% 运算符时的行为 |
__lt__(self, other) | 使用< 运算符时的行为 |
__le__(self, other) | 使用<= 运算符时的行为 |
__eq__(self, other) | 使用== 运算符时的行为 |
__ne__(self, other) | 使用!= 运算符时的行为 |
__abs__(self) | 使用abs() 函数时的行为 |
__bool__(self) | 使用bool() 时的行为,或在条件表达式中使用 |
所以15~17:
abs,getitem,setitem都是支持的
18~20 实例成员方法、类方法和静态方法
实例成员方法、类方法和静态方法的定义、区别以及它们对实例数据成员的访问权限的整理:
实例成员方法
- 定义: 直接定义在类中的方法,默认接收
self
参数,表示当前实例对象的引用。 - 区别: 主要用于操作实例的状态,依赖于类的特定实例。
- 访问实例数据成员: 可以访问和修改实例的数据成员。
class Person:
def __init__(self, name, age):
self.name = name # 实例数据成员
self.age = age
def introduce(self):
return f"Hello, my name is {self.name} and I am {self.age} years old."
# 创建Person类的实例
person1 = Person("Alice", 30)
# 使用实例成员方法
print(person1.introduce()) # 输出: Hello, my name is Alice and I am 30 years old.
在这个例子中,introduce
是一个实例成员方法,它通过self
参数访问实例的数据成员name
和age
。
类方法
- 定义: 使用
@classmethod
装饰器的方法,接收cls
参数,表示当前类对象的引用。 - 区别: 用于操作类的状态,不依赖于类的特定实例,适合定义与类相关但不需要实例化即可使用的方法。
- 访问实例数据成员: 不能直接访问实例的数据成员,但可以访问类的数据成员。
class Person:
species = "Homo sapiens" # 类数据成员
'''
这个变量就是一个类变量(也称为静态变量),被所有对象共享
'''
def __init__(self, name, age):
self.name = name
self.age = age
'''它是 Python 中的一个正式特性,用于将一个普通函数定义为类方法。类方法是与类相关联的方法'''
'''如果想要定义一个类方法,必须使用 @classmethod 装饰器。这个装饰器告诉Python解释器,接下来的函数应该被识别为类方法,而不是普通的实例方法。'''
@classmethod
def get_species(cls):
return cls.species
# 使用类方法
print(Person.get_species()) # 输出: Homo sapiens
在这个例子中,get_species
是一个类方法,它通过cls
参数访问类的数据成员species
,并且不需要类的实例。
静态方法
- 定义: 使用
@staticmethod
装饰器的方法,不需要接收self
或cls
参数。 - 区别: 完全独立于类和实例,可以看作是类内部的普通函数,不涉及类的属性和方法。
- 访问实例数据成员: 不能访问实例的数据成员,也不需要访问。
- 就是静态方法相当于给他加了一个限制让他不去访问类里面的东西。
class Math:
@staticmethod
def add(a, b):
return a + b
# 使用静态方法
print(Math.add(5, 3)) # 输出: 8
在这个例子中,add
是一个静态方法,它不接收self
或cls
参数,也不依赖于Math类的任何实例或类的状态,仅仅是执行了一个简单的加法操作。
总结实例方法、类方法和静态方法的区别:
特性 | 实例方法 | 类方法 | 静态方法 |
---|---|---|---|
装饰器 | 无 | @classmethod | @staticmethod |
参数 | self | cls | 无 |
作用域 | 实例相关 | 类相关 | 完全独立 |
访问实例变量 | 可以 | 不直接 | 不能 |
访问类变量 | 可以(通过实例) | 可以 | 不能 |
修改实例变量 | 可以 | 不直接 | 不能 |
修改类变量 | 不直接 | 可以 | 不能 |
调用方式 | 实例.方法() | 类.方法() 或 实例.方法() | 类.方法() 或 实例.方法() |
典型用途 | 实例特定的操作 | 与类相关的操作 | 与类相关但独立于实例和类的操作 |
不直接的意思是:可以用但是他设计出来的功能不是做这个的,所以叫不直接
权限整理
方法类型 | 访问/修改实例变量 | 访问/修改类变量 |
---|---|---|
实例方法 | 能 | 能 |
类方法 | 不能 | 能 |
静态方法 | 不能 | 不能 |
21
题目:如果在自定义类中实现了特殊方法 __call__
,那么这个类的对象都是可调用对象,可以像调用函数一样使用该类的对象。
判断:正确
原因:在Python中,当一个类实现了 __call__
方法,那么这个类的实例便可以像函数那样被调用。__call__
方法允许一个实例在被调用时执行特定的操作,这通常用于创建函数对象或者实现某种形式的工厂模式。当一个实例被调用时(例如 obj(*args, **kwargs)
),实际上是调用了它的 __call__
方法。
22
判断对错:在基类中使用双下画线开始且不以双下画线结束的成员属于私有成员,无法被派生类继承,在派生类中不能直接访问。
判断:正确
原因:这些成员是命名改编的成员,它们并不是私有的,只是名称被转换成了 _类名_成员名
的形式,以避免命名冲突。派生类可以通过改编后的名称访问这些成员,但这种做法并不推荐。
虽然有这种方法,但是题目中说不能直接访问,不直接那就是对的
23
判断对错:在Python 中定义类时,如果某个成员名称前有两个下画线则表示是私有成员。
判断:错误
原因:
在Python中,没有严格的访问控制,即使名称被改编,成员仍然可以通过改编后的名称被访问。因此,使用双下画线开头的命名约定更多是作为一种文档约定或“暗示”,表明该成员是类的一个内部实现细节,对外属于“非公共”API。
在Python社区,通常认为单下画线开头的成员(如 _single_underline)是受保护的(protected),意味着它们不是公共API,但是子类在需要时可以安全地重写或访问它们。而双下画线开头的成员名通常被解释为“命名空间的私有”(name mangling),而不是真正的私有(private)。
所以,说“如果某个成员名称前有两个下画线则表示是私有成员”这句话是错误的,因为Python不提供真正的私有成员访问控制,而是使用名称改编作为一种减少名称冲突可能性的方法。
24
判断对错:在类定义的外部没有任何办法可以访问对象的私有成员。
判断:错误
原因:虽然Python中没有真正的私有成员,但名称改编提供了一种约定,使得外部代码不应该访问某些成员。然而,名称改编后的成员仍然可以通过特定的方式被访问。
25
判断对错:如果在派生类中没有定义构造方法,会自动继承基类的构造方法,使用派生类定义对象时自动调用基类的构造方法。
判断:正确
原因:如果派生类没有定义自己的 __init__
方法,那么它将继承基类的 __init__
方法。在创建派生类实例时,如果没有其他指定,将调用基类的构造方法。
26
判断对错:如果在派生类中定义了构造方法,使用派生类定义对象时不会自动调用基类的构造方法。
判断:正确,这里题目不严谨
原因:即使在派生类中定义了构造方法,如果派生类的构造方法没有显式调用基类的构造方法(使用 super().__init__()
或 BaseClass.__init__(self)
),那么基类的构造方法仍然不会被调用。但是,如果派生类的构造方法中确实调用了基类的构造方法,那么在创建派生类实例时,基类的构造方法也会被调用。
27
判断对错:定义类时所有实例方法的第一个参数用来表示对象本身,在类的外部通过对象名来调用实例方法时不需要为该参数传值。
判断:正确
原因:实例方法的第一个参数 self
代表当前的实例对象。当在类的外部通过实例对象调用方法时,不需要显式传递 self
参数,因为Python在内部处理这一过程。
28
判断对错:表达式 str.upper('abcd')=='abcd'.upper()
的结果为 True。
判断:正确
原因:
str.upper(‘abcd’) 是对 str 类(字符串类)的 upper 静态方法的调用,它返回传入字符串的全大写形式,结果是一个全新的字符串 ‘ABCD’。这里的 str 是字符串类的名称,而 ‘abcd’ 是传递给该静态方法的参数。
另一方面,‘abcd’.upper() 是对字符串对象 ‘abcd’ 的 upper 实例方法的调用。字符串是可调用的,因为它们有 call 方法,这意味着可以直接在字符串对象上调用 .upper() 来获取它的大写形式,同样是 ‘ABCD’。
str.upper('abcd')
会返回一个新的字符串 'ABCD'
,但它不会调用 'abcd'
上的 .upper()
方法。表达式 str.upper('abcd')
等同于 'ABCD' == 'abcd'.upper()
,这实际上是在比较两个字符串是否相等,由于 'ABCD'
和 'abcd'.upper()
都是 'ABCD'
,所以表达式的结果为 True
。
29
判断对错:已知 x=(i**2 for i in range(10))
,那么表达式 x.__next__()
和 next(x)
的功能是等价的。
判断:正确
原因:在Python中, x.__next__()
和 next()
函数都用于从迭代器中获取下一个元素。对于生成器表达式 x
,它们都可以用来获取生成器的下一个值。(功能上等价的,但实际上使用next() )
30
判断对错:在面向对象程序设计中,函数和方法是完全一样的,都必须为所有参数进行传值。
判断:错误
原因:在面向对象程序设计中,方法通常与对象相关联,并且至少接受一个参数,通常是 self
,代表当前对象的引用。而函数不一定与对象关联,可以独立于类存在。不过,无论是函数还是方法,Python 都是通过传值来传递参数的。
31 简单解释 Python 中以下画线开头和结束的变量名的含义。
类型 | 变量名格式 | 描述 |
---|---|---|
保护变量 | _xxx | 这些变量是类中的成员,但不推荐作为公共API。它们不能通过from module import * 被导入,但仍然可以从类外部访问。 |
私有成员 | __xxx | 类中的私有成员,只有类对象自己能访问,子类对象也不能访问。在对象外部可以通过对象名._类名__xxx 的方式访问。 |
特殊成员 | __xxx__ | 系统定义的特殊成员名字,如__init__ 用于类的构造函数,__del__ 用于类的析构函数等。 |
32~33 jupyter botebook
32
继承6.3节例6-1中的Person类创建Student类,添加新的方法用来设置学生专业,然后生成该类对象并显示信息。
33
设计一个三维向量类,并实现向量的加法、减法以及向量与标量的乘法和除法运算。