Python编程-面向对象基础与入门到实践一书的内容拓展
通过编程,模拟现实生活中的事物编程,叫做面向对象编程,此过程也叫做实例化编程
简单类的创建
class Test():
def __init__ (self,id):
self.id = id
def print_id(self):
print(self.id)
这里建立了一个名为test
的类,其中有两个函数,__init__
与print_id
,有一个数据成员id
__init__
函数
__init__
是一种特殊方法,用于创建类时的初始化,其中带有两个参数self
与id
,其中__init__
初始化类对象时,将参数全部自动传递给self
(无需我们手动传参),self
相当于为每个对象划定了一个用于存放数据成员的位置,保证每个对象的数据成员独立且不受干扰,需要注意的是**self
必须放置在最前面**,而后面的id
就是我们需要传递的参数,并且它的数量是任意的(需要多少个,参数就写多少个)
- 成员函数访问数据
在一个类中的成员函数使用对应对象的数据成员时,必须要使用self
获取对应的数据成员,如self.id
熟悉c++的同学应该看出来了,这和c++的构造函数是类似的
- 拓展思维
既然成员是函数(虽然类中叫方法),那么能否使用关键字实参,默认值与不定参数呢?当然是可以的,后文实验源码将会写到
__init__
的使用
我们先看如下代码:
class Base():
def fun(self):
self.a = 1
self.data = 2
def print_all(self):
print(self.id, self.data)
op = Base()
op.print_all()
我们定义了上述代码,通过运行我们会发现,这几行代码是没有办法运行的,解释器会丢出以下错误:
可以看到,解释器提示我们,缺少了对应的属性,原因就是因为我们没有使用__init__
来创建对应的self
数据,类中并没有存储我们的id
与data
,但是没有该方法,代码能不能跑呢,答案是可以,如下的特殊情况:
class Base():
def fun(self,x,y):
return x,y
op = Base()
print(op.fun(1,2))
可以看到运行是正常的,但是我们进行类创建时最好加上__init__
,不然实例化将失去意义
此处注意一个细节,建立无参类的对象时需要加上()
,例如 op = Base()
创建与访问对象
class Test():
def __init__ (self,id_):
self.id = id_
self.age = 16
def print_id(self):
print(self.id)
a = Test(11)
a.print_id()
a.id = 12
print(a.id)
print(a.age)
我们的__init__
函数中传入了一个id_
,所以传入了一个参数11
,而我们要访问对应对象的数据或函数成员则需要.
运算符,同理要修改某一成员时,也需要用.
运算符操作,并且我们还可以为对象指定默认参数,如上述代码中的self.age = 16
子类继承的参数传递
通常一个大类下可能有,多个有差异的子类,此时则需要继承操作,我们注意到,前面的
Test
的对象后面有一个括号,在未进行继承时该括号可以省略
无参构造的继承
class Base():
def __init__(self):
self.id = 1
self.data = 2
def print_all(self):
print(self.id, self.data)
class Dome(Base):
def __init__(self):
super().__init__()
self.a = 3
def print_all(self):
print(self.id, self.data, self.a)
object = Dome()
object.print_all()
如上代码中的无参数类继承,这里需要提出几个关键点:
- 在C++中在继承时无参数基类是可以省略派生类与基类的构造,但是python不能够省略,否则无法保留父类属性
- 派生类的
super().__init__()
也不能省略,它是用来链接基类与派生类的,省略后将无法从基类继承
对于第二点有以下情况,当我们不使用基类继承成员时能否省略super().__init__()
呢?我们运行以下代码:
class Base():
def __init__(self):
print("yes") # 我们加入了基类构造提示
self.id = 1
self.data = 2
def print_all(self):
print(self.id, self.data)
class Dome(Base):
def __init__(self):
# super().__init__() # 将之注释掉
self.a = 3
def print_all(self):
print(self.a)
object = Dome()
object.print_all()
通过上述代码运行结果可知,此代码并没有进行继承操作,仅仅只是输出了a
的值,即class Dome(Base)
失去意义,如果我们取消注释,那么它又将正常继承
有参构造的继承
class Base():
def __init__(self, id, data):
self.id = id
self.data = data
def print_all(self):
print(self.id, self.data)
class Dome(Base):
def __init__(self, id, data, a):
super().__init__(id, data)
self.a = a
def print_all(self):
print(self.id, self.data, self.a)
object = Dome(1, 2, 3)
object.print_all()
如上述代码,我们在子类Dome
的括号内填入基类的的名字,Dome
中的第一个__init__
用于初始化传入派生类的值super
函数用于连接基类与派生类,其后跟随的__init__
用于向基类传递参数,并且不加self
(作为实参,调用基类的__init__
),并且在为基类构造时,派生类的构造函数参数数量至少要大于或等于基类参数的数量,否则将构建失败
自动传递参数特性
注意上述操作是在子类扩展基类时的要求,那么继承在不是为了扩展基类时又会是什么样的情况呢?
在 Python 中,如果子类没有显式调用父类的构造函数 __init__
,并且自身也未定义 __init__
,Python 会自动调用父类的构造函数,并将相应的参数传递给父类。这一特性在Python的高级操作装饰器时将会发挥巨大的作用:
class Parent:
def __init__(self, value):
self.value = value
class Child(Parent):
def get_value(self):
return self.value
# 创建子类实例时并没有显式调用父类的构造函数
child = Child(10)
print(child.get_value())
重写基类成员
有时候我们需要对于派生类与基类的不同之处做出不同的行为,但是为了保证代码的可读性,我们要派生类调用同名函数,于是我们有以下操作:
class Base():
def __init__ (self,id_):
self.id = id_
def print_id(self):
print(self.id)
class Dome(Base):
def __init__ (self,id_):
super().__init__(id_)
self.a = 12
def print_id(self):
print(self.a)
object = Dome(11)
object.print_id()
运行上述代码,使用与基类中同名的函数得到子类中a
的值
在子类中调用基类方法
class Base():
def __init__ (self,id_):
self.id = id_
def print_id(self):
print(self.id)
class Dome(Base):
def __init__ (self,id_):
super().__init__(id_)
self.a = 12
def print_id(self):
super().print_id()
object = Dome(11)
object.print_id()
运行上述代码,使用与基类中同名的函数得到基类中id
的值
多继承的super参数处理
class BaseClass:
def __init__(self, base_param):
self.base_param = base_param
print("BaseClass constructor with base_param:", self.base_param)
class Mixin1(BaseClass):
def __init__(self, mixin1_param, **kwargs):
super().__init__(**kwargs) # 调用父类的构造函数
self.mixin1_param = mixin1_param
print("Mixin1 constructor with mixin1_param:", self.mixin1_param)
class Mixin2(BaseClass):
def __init__(self, mixin2_param, **kwargs):
super().__init__(**kwargs) # 调用父类的构造函数
self.mixin2_param = mixin2_param
print("Mixin2 constructor with mixin2_param:", self.mixin2_param)
class MyClass(Mixin1, Mixin2):
def __init__(self, base_param, mixin1_param, mixin2_param):
super().__init__(base_param=base_param, mixin1_param=mixin1_param, mixin2_param=mixin2_param)
print("MyClass constructor")
# 创建实例
obj = MyClass(base_param="BaseParam", mixin1_param="Mixin1Param", mixin2_param="Mixin2Param")
在这个例子中,每个类的构造函数都接收一些参数,并使用 super().__init__(**kwargs)
来调用父类的构造函数。在 MyClass
中,通过使用 super()
来确保调用了 Mixin1
和 Mixin2
的构造函数,并传递了适当的参数。
类的嵌套
总所周知python中套娃是非常普遍的,通常情况下,我们在构建一个实例时,实例的属性是非常复杂的,我们为了简化代码,需要为某些特定属性建立一个类:
class build:
def __init__(self):
self.time = 1998
def print_time(self):
print(self.time)
class Base:
def __init__ (self,id_):
self.id = id_
self.build = build()
def print_id(self):
print(self.id)
ob = Base(22)
ob.build.print_time()
如上,我们为Base
类建立了一个嵌套的子类build
,用于存储建立的时间,但是访问time
时时我们需要注意:要通过Base
对象去访问build
中的成员print_time
类的访问权限
Python中对类中数据的访问权限并没有C++那样严格,可以暂时认为只有私有与公有两种权限(先暂时这样理解,后文将细说)
公有权限
class Test:
def __init__(self,a):
self.a = a
def print_(self):
print(self.a)
object = Test(22)
print(object.a)
object.a = 33
print(object.a)
如上,定义一个简单的类,然后访问并且修改它,可以看到修改成功,即按上述代码进行编写代码,所有数据都是公有的,外界可以修改
私有权限
class Test:
def __init__(self,a):
self.__a = a
def print_(self):
print(self.__a)
ob = Test(22)
print(ob.__a)
先看如上错误代码,我们运行后可以发现会抛出如下错误:
它会提示我们,Test类中没有这个属性,原来我们在成员前加上__
后解释器对数据成员进行了一个类似于重命名的操作,不同版本的解释的方式也不同,如果我们要强行访问,也可以想办法找到对应解释器的重构规则进行访问,一般这类成员我们通过成员方法进行访问
class Test:
def __init__(self,a):
self.__a = a
def print_(self):
print(self.__a)
ob = Test(22)
ob.print_()
需要注意的是:
- 类似于
__init__
这类前后带有双下划线的,在python中是特殊的变量,可以直接访问 - 从上面可知,python中的权限实际上是没有限制的,只要你有对应的访问方式
- 和C++类似,python的方法和C++的函数一样是可以作为私有成员的,如下套娃代码
class Test:
def __init__(self,a):
self.__a = a
def __print_(self):
print(self.__a)
def printf(self):
self.__print_()
ob = Test(22)
ob.printf()
保护权限
python中实际上是没有保护权限的,但是有一种约定俗成的规定,即在变量前加 _
,这类成员实际上仍然是公有的,但是约定为保护成员
class Test:
def __init__(self,a):
self._a = a
def print_(self):
print(self._a)
object = Test(22)
print(object._a)
object.a = 33
print(object._a)
通过上述讲解,我们可以将继承进行拓展,即在继承时,实际上是继承了所有成员,但是由于我们不知道对应变量的变量名,而变得难以访问对应成员
单类模块使用
和函数一样,python也支持将类装进一个模块,我们建立一个Test模块进行使用
# test.py
class Test:
def __init__(self,a):
self._a = a
def print_(self):
print(self._a)
这个模块包含了一个Test类,其中有一个数据成员_a
和一个函数print_
使用该模块可以通过如下引入:from [模块文件名] import [对应类名]
则引入方式为 from test import Test
多类模块的引入
通常在构建项目时,会建立多个类的模块,可能会包含多个同等地位的类,子类嵌套或继承,此时引入方式和上述仍然一致:
# test.py
class Test_one:
def __init__(self,a):
self._a = a
def print_(self):
print(self._a)
class Test_two:
def __init__(self,a):
self._a = a
def print_(self):
print(self._a)
如上建立同等地位的类,我们的访问方式没有太多变化:
from test import Test_one
from test import Test_two
访问带有子类嵌套的类模块
# test.py
class Test_one:
def __init__(self, x, y):
self.a = x
self.b = Test_two(y)
def print_(self):
print(self.a)
class Test_two:
def __init__(self, x):
self.a = x
def print_(self):
print(self.a)
定义类如上,接下来我们进行访问:
from test import Test_one
object = Test_one(22, 33)
print(object.a, object.b.a)
我们注意到运行是正常的,即python在引入带子类的类Test_one
后初始化其对象时,会自动创建其子类,但是并没有自动为文件引入模块中的子类,即如下操作是错误的:
from test import Test_one
object = Test_two(22)
访问带有继承类模块
# test.py
class Base():
def __init__(self, id, data):
self.id = id
self.data = data
def print_all(self):
print(self.id, self.data)
class Dome(Base):
def __init__(self, id, data, a):
super().__init__(id, data)
self.data = data
self.a = a
def print_all(self):
print(self.id, self.data, self.a)
建立如上模块,进行以下操作:
from test import Dome
object = Dome(1, 2, 3)
object.print_all()
正常运行,并且访问带有继承类模块时,导入其派生类不会自动导入基类
还有一点,类模块和函数一样也是可以取别名的,就像以下操作:
from test import Dome as d
object = d(1, 2, 3)
object.print_all()
其他导入情况
我们经常会遇到下述情况:
- 使用同一模块中的所有类
- 使用同一模块中的两个或两个以上的类
这方面和函数类似,我们依次介绍
使用同一模块中的所有类
from test import *
此操作将取出该模块中所有类,并且不使用.
运算符操作
格式:from [模块名] import *
import test
此操作导入了整个模块,需要使用.
运算符操作
格式:import [模块名]
类与函数混合模块的导入
在python中可以将函数与类放在同一模块下,如下:
# test.py
class Base():
def __init__(self, id, data):
self.id = id
self.data = data
def print_all(self):
print(self.id, self.data)
class Dome(Base):
def __init__(self, id, data, a):
super().__init__(id, data)
self.data = data
self.a = a
def print_all(self):
print(self.id, self.data, self.a)
def hello_world():
print(("hello World").title())
但是我们使用时仍然是一致的:
from test import *
object = Dome(1, 2, 3)
object.print_all()
hello_world()
实验源码
# Chicken.py
class Person:
def __init__(self, name, time, *show): # 不定参数
self.name = name
self.time = time
def print_all(self):
print(self.name, self.time)
class PersonShow(Person):
def __init__(self, name, time, *show):
super().__init__(name=name, time=time) # 关键字实参
self.showList = []
for i in range(0, len(show)):
self.showList.append(show[i])
def print_all(self):
byte_str = bytes.fromhex('e894a1e5be90e59da4')
string = byte_str.decode('utf-8')
print("我是练习时长"+str(self.time)+"的个人练习生"+str(self.name))
print("喜欢", self.showList)
if self.name == string:
print("鸡 你 太 美")
print("喜欢的话 请多多为我投票吧")
from Chicken import *
if __name__ == "__main__" :
player = PersonShow("蔡徐坤","两年半","唱","跳","RAP","篮球")
player.print_all()
所有类的超类object
在Python中,所有的类都直接或间接地继承自object
类,类似于Java中的Object
类。不过在Python 3中一般不需要显式继承object
,因为所有的类默认就是新式类。与Java的Object
类类似,Python中的object
类也提供了一些通用的方法,这些方法可以在自定义类中使用或者覆盖:
__str__()
方法: 类似于Java中的toString()
方法,它返回对象的字符串表示形式。
def __str__(self):
return "This is a custom object"
__eq__()
方法: 用于比较两个对象是否相等。默认的实现是比较对象的身份标识(identity),即是否指向同一内存地址。可以根据实际需要覆盖该方法。
def __eq__(self, other):
if isinstance(other, MyClass):
return self.some_value == other.some_value
return False
__hash__()
方法: 返回对象的哈希码值。与Java中的hashCode()
类似,用于在散列表等数据结构中使用。
def __hash__(self):
return hash(self.some_value)
__repr__()
方法: 返回对象的“官方”字符串表示,通常是一个可以用来重建对象的表达式。
def __repr__(self):
return f"MyClass({self.some_value})"
这些方法在Python中的使用与Java中的Object
类的方法类似,但是命名方式略有不同。在Python中,双下划线(__
)表示特殊方法(也称为魔法方法或魔术方法),用于实现类的特殊行为。
注意事项和建议与Java类似:
-
__eq__()
和__hash__()
一致性: 如果两个对象通过__eq__()
方法相等,那么它们的__hash__()
值应该相等,以确保在使用散列表等数据结构时能够正确地工作。 -
__str__()
和__repr__()
可读性: 类似于Java中的toString()
方法,这两个方法的实现应该返回清晰、简洁且可读性强的字符串。
魔术方法
你可能注意到了,上述出现的所有方法均以两个短横线开始或结束,这便是python中的魔术方法,它们大多数都来自object类,下面是一些常见的魔术方法参考表格:
魔术方法 | 描述 |
---|---|
__init__(self, ...) | 初始化对象,在创建对象时调用 |
__del__(self) | 删除对象时调用 |
__str__(self) | 返回对象的字符串表示 |
__repr__(self) | 返回对象的“官方”字符串表示,用于开发和调试 |
__len__(self) | 返回对象的长度,使用内置的len() 函数时调用 |
__getitem__(self, key) | 定义获取元素的行为,例如obj[key] |
__setitem__(self, key, value) | 定义设置元素的行为,例如obj[key] = value |
__delitem__(self, key) | 定义删除元素的行为,例如del obj[key] |
__iter__(self) | 定义迭代器的行为,使对象可迭代 |
__next__(self) | 定义迭代器的next 方法,返回下一个迭代值 |
__contains__(self, item) | 定义成员测试行为,例如item in obj |
__call__(self, ...) | 实例对象可调用,类似函数调用的行为 |
__eq__(self, other) | 定义相等比较的行为,例如obj == other |
__ne__(self, other) | 定义不相等比较的行为,例如obj != other |
__lt__(self, other) | 定义小于比较的行为,例如obj < other |
__le__(self, other) | 定义小于等于比较的行为,例如obj <= other |
__gt__(self, other) | 定义大于比较的行为,例如obj > other |
__ge__(self, other) | 定义大于等于比较的行为,例如obj >= other |
__add__(self, other) | 定义加法行为,例如obj + other |
__sub__(self, other) | 定义减法行为,例如obj - other |
__mul__(self, other) | 定义乘法行为,例如obj * other |
__truediv__(self, other) | 定义真除法行为,例如obj / other |
__floordiv__(self, other) | 定义整除法行为,例如obj // other |
__mod__(self, other) | 定义取模行为,例如obj % other |
__pow__(self, other[, modulo]) | 定义幂运算行为,例如obj ** other |
__enter__(self) | 在进入with 语句块时调用,用于资源的初始化和分配 |
__exit__(self, exc_type, exc_value, traceback) | 在离开with 语句块时调用,用于资源的释放和清理 |
__setattr__(self, name, value) | 在设置属性时调用,例如obj.attr = value |
__getattr__(self, name) | 在获取不存在的属性时调用,例如obj.nonexistent_attr |
类属性使用
在Python中,类属性是属于类而不是类的实例的属性。它们是在类定义中声明的变量,而不是在类的实例中创建的。所有属于类的实例共享相同的类属性。
class CarClass:
# 类属性
num_wheels: int = 4
def __init__(self, make, model) -> None:
# 实例属性
self.make = make
self.model = model
if __name__ == "__main__":
# 创建两个 Car 实例
car_one: CarClass = CarClass("Toyota", "Camry")
car_two: CarClass = CarClass("Honda", "Accord")
# 访问实例属性
print(car_one.make) # 输出: Toyota
print(car_two.model) # 输出: Accord
# 访问类属性(使用类名或实例都可以)
print(CarClass.num_wheels) # 输出: 4
print(car_one.num_wheels) # 输出: 4
print(car_two.num_wheels) # 输出: 4
# 修改类属性(注意这会影响所有实例)
CarClass.num_wheels = 6
# 检查修改后的类属性
print(CarClass.num_wheels) # 输出: 6
print(car_one.num_wheels) # 输出: 6
print(car_two.num_wheels) # 输出: 6
需要注意的是:类属性不会被实例化给每个对象,而是被所有该类的实例所共享。当你创建一个类属性时,它属于类本身,而不是类的实例。所有通过该类创建的实例都可以访问和共享相同的类属性。
在Python中,实例在访问属性时首先查找实例本身是否有该属性,如果没有,它会继续查找类的属性。这就是为什么实例可以访问类属性的原因。
实例属性遮蔽
接下来考虑以下问题:
我们在 CarClass.num_wheels = 6
前增加一行代码 car_one.num_wheels = 233
,此时运行代码将会的出什么结果呢?
当你运行 car_one.num_wheels = 233
时,它会创建一个新的实例属性 num_wheels
,而不是修改类属性。这是因为在 Python 中,如果你为实例赋值一个属性,它会在实例上创建一个同名的属性,而不会影响到类属性。这就是实例属性遮蔽
self引用实例对象
在 Python 中,self
是一个惯例,用于表示对象实例本身。它是在类的方法中作为第一个参数传递的,但在调用该方法时不需要显式传递。self
提供了一种引用对象实例属性和方法的方式
但是self
的命名并不是强制的,你可以使用其他名称,但 self
是一个广泛接受的约定,因此建议在大多数情况下使用它(不推荐自己定义,应使用self
或团队,公司的约定)。如以下不推荐的自定义示例:
class TestClass:
def __init__(my_self, value):
my_self.value = value
def printf(my_self):
print(my_self.value)
obj = TestClass(233)
obj.printf()