Python中的面向对象编程学习笔记
面向对象编程主要有以下几个方面:类(Class)、对象(Object)、封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)。
1.类:
1.什么是类:
类(Class)可以看作是创建对象的模板或蓝图。它定义了一组规则和结构,这些规则和结构用于描述如何创建特定类型的对象。可以理解为,类就像是一份建筑图纸,描述了一种特定类型的结构和行为,而基于这份图纸我们可以创建出许多具体的建筑(也就是对象)。
2.类的组成:
**1.属性(Attributes):**也称为字段,是定义在类中的变量。它们表示对象的状态或者特征。例如,如果我们定义一个“汽车”类,那么“颜色”、“品牌”、“型号”等可能就是这个类的属性。
**2.方法(Methods):**是定义在类中的函数,表示对象可以进行的操作或行为。还是“汽车”类的例子,可能的方法有“启动”、“停车”、“加速”等。
一个特点是,类定义了属性和方法,但具体的值是存储在由该类创建的对象(或称为该类的实例)中的。
class Car:
def __init__(self, color, brand):
self.color = color
self.brand = brand
def start(self):
print("The car is starting.")
def stop(self):
print("The car is stopping.")
# 在这个例子中,Car 类有两个属性 color 和 brand,以及两个方法 start 和 stop。我们可以基于这个类创建出各种颜色和品牌的汽车对象,并分别对它们进行启动和停车等操作。
3.self
和__init__
:
self
:self
是一个约定俗成的命名,用来指代实例对象本身。在类的方法中,你会看到 self
被作为第一个参数。这是因为当你通过一个实例对象来调用类的方法时,Python会自动将这个实例对象作为第一个参数传入,这个特性也称为绑定行为。因此,无论你给这个参数取什么名字,它都指的是实例对象本身,但是约定俗成的是取名为 self
。
class MyClass:
def print_self(self):
print(self)
# 调用print_self方法时,Python将my_object对象作为参数传递给self,所以print(self)输出的就是my_object对象。
my_object = MyClass()
my_object.print_self() # 输出: <__main__.MyClass object at 0x10d238d90>
print(my_object) # 输出: <__main__.MyClass object at 0x10d238d90>
__init__
:__init__
是特殊的一种方法,被称为类的构造方法或初始化方法。当我们创建了类的一个新实例(也就是一种对象)时,__init__
方法会自动被调用。它的主要作用是进行一些设置或初始化工作,例如我们可以在这个方法中为对象设置一些属性值。
下面是一个简单的例子:
class Car:
def __init__(self, color, brand):
self.color = color
self.brand = brand
在这个例子中,__init__
方法接受两个参数 color
和 brand
,然后将它们保存为对象的属性。当我们创建一个新的 Car
对象时,就需要提供这两个参数,例如 my_car = Car('red', 'Toyota')
。然后,我们就可以通过 my_car.color
和 my_car.brand
访问这些属性了。
2.对象:
1.什么是对象:
在面向对象编程中,对象(Object)是实际存在的一个独立实体,在Python中,它是使用类的定义作为模版创建出来的。以类作为蓝图创建的每个对象都将具有类定义的相同结构(即它们都具有相同的属性和方法),但是它们的属性值可能会各不相同。
例如,如果我们有一个“汽车”类,这个类可能定义了“颜色”、“品牌”等属性,以及“启动”、“停车”等方法。然后,我们可以基于这个类创建出一个颜色是红色、品牌是丰田的汽车对象;也可以创建出一个颜色是蓝色、品牌是本田的汽车对象。这两个对象都是“汽车”类的实例,但它们有各自的属性值,并能独立地进行启动和停车等操作。
2.如何创建对象:
创建对象的这个过程也被称为实例化,因为你正在创建类的一个实例。创建对象非常简单,只需要使用类名并传入适当的参数(如果有的话)即可。
以下是代码示例:
class Car:
def __init__(self, color, brand):
self.color = color
self.brand = brand
def start(self):
print(f"The {self.color} {self.brand} is starting!")
def stop(self):
print(f"The {self.color} {self.brand} is stopping!")
要实例化 Car
类并创建一个对象,你可以这样做:
my_car = Car("black", "BMW")
在上述代码中,我们调用了 Car
类,并传入了两个字符串参数 "black"
和 "BMW"
。这些参数会被传递给 Car
类的 __init__
方法,并设置为对象的属性值。
现在,my_car
是 Car
类的一个实例,我们可以调用它的方法:
my_car.start() # The black BMW is starting!
即使 start
方法使用 self
作为参数,也不需要在调用时显式传入这个参数。Python会自动将 my_car
作为 self
参数传入。
3,实例属性和实例方法:
实例属性:是在类的实例(对象)中定义的属性。每个实例都有其自己的实例属性。即使两个实例对象都来自同一个类,它们的实例属性也可以是完全不同的。
例如:
class Car:
def __init__(self, color):
self.color = color # color 就是一个实例属性
car1 = Car("red")
car2 = Car("blue")
print(car1.color) # 输出 "red"
print(car2.color) # 输出 "blue"
在上面的例子中, color
就是 Car
类的一个实例属性,car1
和 car2
是 Car
类的两个不同实例,它们的 color
属性分别是 "red"
和 "blue"
。
实例方法:实例方法是属于对象的方法,在类的定义中声明。实例方法需要将对象本身作为第一个参数,按照惯例,我们一般使用 self
表示。实例方法可以访问类和实例的属性及其他方法,是进行面向对象编程的关键。
以下是代码示例:
class Car:
def __init__(self, color):
self.color = color
def describe(self): # 这是一个实例方法
print(f"This is a {self.color} car.")
car1 = Car("red")
car1.describe() # 输出 "This is a red car."
在上面的代码中,__init__
是一个特殊的实例方法,被称为类的构造器方法。这个方法在创建类的实例时自动被调用,用于设置新的实例的一些初始化属性。在这个例子中,__init__
方法接受一个参数 color
,然后把这个值赋给 self.color
,这样就创建了一个名为 ‘color’ 的实例属性。describe
方法就是一个实例方法。self
关键字表示实例方法访问的是对象本身,并且实例方法可以访问和修改这个实例的属性。在 describe
方法中,我们通过 self.color
访问了 color
属性,并将它插入到字符串中进行打印。
3.类和实例对象的内存地址
在Python中,每个对象(包括类和实例)都有一个唯一的内存地址。你可以通过内置函数 id()
来获取这个地址。这个内存地址是Python解释器自动分配的,你无法手动控制或修改。
以下是代码示例:
class MyClass:
pass
obj1 = MyClass()
obj2 = Myclass()
print(id(MyClass)) # 打印类的内存地址
print(id(obj1)) # 打印实例对象的内存地址
print(id(obj2)) # 打印实例对象的内存地址
执行这段代码,会打印出 MyClass
类、 obj1
和obj2
实例所在的内存地址。它们的内存地址是不同的,因为它们分别在内存中占用了不同的位置。各个实例对象之间也会有各自的不同内存地址,因为每个实例对象都是一个独立的对象,占用不同的内存空间。
1.类的内存地址存放什么:
一个类的内存地址储存的是该类对象的相关信息。
具体来说,类对象包含以下内容:
- 属性:这包括类级别的量(也被称为类属性),但是类的内存地址无法访问实例属性,还有各种方法。方法在 Python 中也是对象,被储存在类的内存中。
- 元信息:包括这个类的名字,它的基类,以及其他的一些信息,例如它是哪个模块的一部分,它的文档字符串等。
- 方法:类的方法其实是存储在类对象的内存地址中的。这个函数对象也是存储在内存中,一个方法的定义在内存中形成一个函数类型的对象,然后这个函数对象被存储在类对象的对应属性中。
当我们创建一个类的实例时,实例的内存中并不会复制一份类的所有属性和方法,而是通过引用的方式去指向类对象中相应的部分。这使得实例具有较小的内存占用,同时也允许所有的实例共享相同的类属性和方法。
以下面这段代码为例:
class MyClass:
class_var = 1 # 这里定义的class_var是类属性
def __init__(self, val):
self.instance_var = val
def my_method(self):
print('Hello from my_method!')
obj = MyClass(2)
在这个例子中,class_var
,__init__
和 my_method
方法都是存在类的内存地址中的,而obj
实例有自己的内存空间,用来保存实例属性instance_var
。其他像obj.my_method
这样的实例方法其实是个引用,指向了MyClass.my_method
。
2.实例对象的内存地址存放什么:
一个实例对象的内存地址主要用于储存以下信息:
- 实例属性:不同于类属性(存储于类的内存地址中),实例属性是特定于实例的。例如,如果我们实例化一个‘dog’类来创建一个名为‘fido’的狗对象,那么’fido’的名字就是一个实例属性。这些实例属性存储在实例对象的内存地址中。
- 方法引用:虽然方法直接存储在类的内存地址里,但是实例对象也包含了指向这些方法的引用,使得我们可以通过实例对象来调用这些方法。也就是说,实际的代码(方法)存储在类对象中,但实例对象知道怎么找到它们。(类的内存中可以访问实例方法的地址,但是无法调用实例方法)
- 指向类的引用:实例对象还包含了一个指向其类对象的引用,这使得实例对象可以访问存储在类中的所有类属性和方法。
例如,假设我们有如下的类定义:
class Dog:
species = "Canis familiaris" # 类属性
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性
当我们创建一个Dog类的实例对象时,如fido = Dog("Fido", 4)
,那么"fido"实例对象的内存地址里会包含"name"和"age"这两个实例属性,并有一个引用指向Dog类。于此同时,"species"这个类属性仍然存放在Dog类的内存地址中,但"species"可以通过任何Dog类的实例来访问,如fido.species
。
那么,问题来了。实例对象怎样通过内存地址去访问类属性和方法?
类的实例对象访问类属性和方法的机制是通过实例对象内部的一个特殊属性来实现的,那就是__class__
属性。每个实例对象的__class__
属性都包含了一个指向其类对象的引用。因此,当你尝试通过实例对象访问类属性或方法时,Python 会首先在实例属性中查找,如果未找到,则会在它的__class__
属性指向的类对象中查找。这种通过实例对象去访问类属性和方法的机制,被成为“属性查找顺序”或者“属性查找链”。
例如,假设我们有如下的类定义:
class Dog:
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print(f"{self.name} says woof!")
fido = Dog('Fido', 4)
调用fido.bark()
,这个时候Python会先在fido
实例的属性中找bark
,找不到后,就会去fido.__class__
(也就是Dog类)里面查找,找到了就执行这个方法。同样的,当你尝试获取fido.species
时,Python也会先在fido
实例中查找species
属性,如果找不到,就会去fido.__class__
(也就是Dog类)中查找。
3.__class__
属性:
每个类的实例对象都有一个特殊的属性__class__
。这个属性是一个指向创建该实例的类的引用。简单的说,__class__
属性就是用来存储实例对象所属的类。
__class__
具有以下用途:
- 查找类属性和方法:当通过实例对象来访问属性或方法时,Python会先从实例对象的属性中查找,如果找不到,则会通过
__class__
属性找到类对象,然后从类对象的属性中查找。 - 动态获取实例的类型信息:可以通过实例对象的
__class__
属性来动态地获取该实例的类型信息,包括类名、基类、类的属性和方法等。
以下是示例:
class Dog:
breed = "Unknown"
def bark(self):
print("Woof woof!")
fido = Dog()
print(fido.__class__) # 输出:<class '__main__.Dog'>
print(fido.__class__.__name__) # 输出:Dog
在访问属性和方法时也是通过__class__
属性实现的。比如当执行fido.bark()
时,Python实际上是先在fido实例的属性中查找bark
,找不到后,就会跳到其类对象中查找,具体就是通过fido.__class__.bark
来查找和调用。
4.封装:
封装是面向对象编程的三大基本特性之一(另外两个是继承和多态)。它的主要思想是将对象的状态(数据)和行为(代码)组合到一起,并对对象内部的复杂性实施隐藏,仅向外界提供有限的可访问接口。
1.私有变量和公有变量:
在Python中,基于变量的可见性和访问权限,我们通常将类的属性分为公有(Public)和私有(Private)两种。
1.公有变量:也就是公有属性,能够在类的外部直接访问。也就是说,我们可以直接通过实例对象来获取或修改这些属性的值。例如:
class Dog:
def __init__(self, name):
self.name = name # 这里的name就是一个公有属性
# 创建一个Dog对象
my_dog = Dog('Tommy')
# 直接访问公有属性
print(my_dog.name) # 输出:Tommy
2.私有变量:也就是私有属性,只能在类的内部访问,不能通过实例对象直接访问。Python没有像Java那样的private关键字来声明私有属性,而是通过一种命名惯例来实现的。即在属性名前加上两个下划线__
,例如__private_var
。如下例所示:
class Cat:
def __init__(self, name):
self.__name = name # 这里的__name就是一个私有属性(因为name前面有__,这表示私有属性)
# 创建一个Cat对象
my_cat = Cat('Kitty')
# 尝试直接访问私有属性会报错
print(my_cat.__name) # AttributeError: 'Cat' object has no attribute '__name'
在这个例子中,私有属性__name
只能在Cat
类的内部访问,尝试在外部访问会报错。
尽管我们能通过一种诡异的方式(名字改写,Name Mangling)来访问私有属性,例如my_cat._Cat__name
,但这并不是一个良好的做法,破坏了封装性,应当尽量避免。
2.getter和setter方法:
尽管在Python中,约定俗成的规则是私有属性(用下划线__
前缀标识的变量)不能直接通过对象在类的外部被访问,但我们可以通过定义公开的方法在类的内部访问和操作这些私有属性。这些公开的方法通常被称为getter(获取属性)和setter(设置属性)方法。
以一个简单的Person
类为例,这个类有一个私有属性__name
,然后我们定义了公开的getter和setter方法:
class Person:
def __init__(self, name):
self.__name = name
# Getter
def get_name(self):
return self.__name
# Setter
def set_name(self, name):
self.__name = name
然后,我们可以通过公开的getter和setter方法来获取和修改__name
属性的值:
p = Person('John')
print(p.get_name()) # 输出:John
p.set_name('Mike')
print(p.get_name()) # 输出:Mike
在这个例子中,我们首先使用get_name
方法获得了__name
属性的值,然后使用set_name
方法修改了__name
属性的值。在这个例子中,get_name
和set_name
就是getter和setter方法。通过这两个方法,我们可以在不直接访问__name
属性的情况下,获取和设置其值。
5.继承:
在面向对象编程中,继承是一个非常重要的概念。基本上,一个类(称为子类或派生类)可以继承另一个类(称为父类或基类)的属性和方法。
1.基本语法:
继承的语法是在子类的名字后面放置父类的名字,中间用圆括号隔开。
例如,定义一个Person
类,然后让Teacher
类继承Person
类:
class Person:
def __init__(self, name):
self.name = name
class Teacher(Person): # Teacher 类继承 Person 类
pass
t = Teacher('Alice')
print(t.name) # 输出:Alice
在这个例子中,我们可以看到,尽管Teacher
类中没有定义任何内容,但是我们依然可以创建一个Teacher
对象,并为其赋予名字,因为Teacher
类从Person
类中继承了__init__
方法。
2.方法重写:
在Python中,如果子类中定义了一个与父类方法同名的方法,那么,子类的实例调用这个方法时,将会执行子类中定义的版本,而不是父类中定义的版本。这就是方法重写。
这种特性非常有用,因为它允许你在子类中单独自定义和改进父类的行为,而不必改变父类的定义。
以下是Python中方法重写的一个例子:
class Person:
def greet(self):
print("Hello, I'm a person.")
class Teacher(Person): # Teacher类从Person类继承
def greet(self): # 重写父类的greet方法
print("Hello, I'm a teacher.")
p = Person()
p.greet() # 输出:Hello, I'm a person.
t = Teacher()
t.greet() # 输出:Hello, I'm a teacher.
在这个例子中,Teacher
类重写了从Person
类继承的greet
方法。因此,当我们用Teacher
类的实例调用greet
方法时,它执行的是Teacher
类中定义的greet
方法,而不是Person
类中定义的greet
方法。
如果你在重写方法的同时还想调用父类的方法,你可以使用super()
函数:
super()
函数:
super()
函数在Python中用于调用父类(也称为超类或基类)的方法。
它的主要作用是让你能够不必显式地引用基类,就能调用基类的方法。这在有多个父类的情况下(也就是多重继承)尤其有用,因为它可以帮助你避免写出硬编码的基类的名字。
super()
函数的常见使用场景是在子类的方法中调用父类的方法。例如,在方法重写中,你可能希望调用父类的原始实现,然后在此基础上添加一些新的行为。你可以使用super()
函数来实现这一点。
super()
函数的工作原理是通过所谓的方法解析顺序(Method Resolution Order,MRO)来寻找正确的方法。MRO是Python根据类的继承图计算出来的,它决定了在多重继承的情况下,应该从哪个父类中寻找方法。
回到原文,例如,在下面的例子中,可能我们想要在Teacher
类中添加一个新的属性subject
,我们可以这样做:
class Person:
def __init__(self, name):
self.name = name
class Teacher(Person):
def __init__(self, name, subject):
super().__init__(name) # 调用父类的初始化函数,设置name属性
self.subject = subject # 新增一个subject属性
t = Teacher('Alice', 'Math')
print(t.name) # 输出:Alice
print(t.subject) # 输出:Math
我们使用super()
方法调用了父类的__init__
方法,以初始化name
属性,然后新增了一个subject
属性。这样,Teacher
类就具有了和Person
类相同的name
属性,以及新的subject
属性。
3.多重继承:
在Python中,一个类可以从多个父类得到继承,这就是所谓的多重继承。多重继承的一个主要好处是,它可以让一个类继承自多个父类的属性和方法。
1.基本语法:
class Base1:
pass
class Base2:
pass
class MultiDerived(Base1, Base2):
pass
在这个例子中,MultiDerived
是从 Base1
和 Base2
类派生的,它继承了这两个基类的所有属性和方法。
但是,多重继承会带来一个问题,就是如果多个父类有同名的方法,那么子类在调用这个方法时,应该调用哪一个呢?
Python解决这个问题的办法是通过一种叫做方法解析顺序(Method Resolution Order,MRO)的机制。MRO是Python用来确定当调用一个方法时,应该从哪个类中去查找这个方法。MRO的规则如下:
- 子类优先于父类。
- 在多个父类中,位于列表前面的父类优先于位于后面的父类。
你可以使用mro()
方法或者__mro__
属性来查看一个类的MRO:
print(MultiDerived.mro())
这将打印出一个列表,显示了Python在查找方法时,会按照什么顺序来遍历这些类。
6.多态:
多态允许我们将子类对象当作父类对象使用,子类对象可以替换任何一个父类对象。这种特性大大提高了程序的灵活性和可扩展性,使得我们可以更容易地写出通用的代码。
class Animal:
def sound(self):
pass
class Dog(Animal):
def sound(self):
return "Woof!"
class Cat(Animal):
def sound(self):
return "Meow!"
def animal_sound(animal: Animal):
print(animal.sound())
animal1 = Dog()
animal2 = Cat()
animal_sound(animal1) # 输出:"Woof!"
animal_sound(animal2) # 输出:"Meow!"
在这个例子中,animal_sound
函数期望得到一个Animal
类型的对象,然后调用它的sound
方法。但是我们可以传入一个Dog
对象或者Cat
对象,因为他们都是Animal
的子类,所以他们都有sound
方法。这就是多态的一个典型示例。
当我们通过一个父类引用调用一个方法时,实际上执行的是子类的版本。这是因为在运行时,Python会根据对象的实际类型,来决定应该调用哪个方法。这就是所谓的动态绑定,也是多态的核心所在。
7.对象的关联:
1.单个关联对象:
当我们说一个对象关联一个对象时,通常是指一对一关系。
例如:
class Classroom:
def __init__(self, name):
self.classroom_name = name
class Student:
def __init__(self, name):
self.student_name = name
# 创建一个教室对象
class205 = Classroom("205班")
# 创建一个学生对象
stu01 = Student("学生1")
# 直接给教室对象添加属性
class205.stu = stu01
print(class205.stu.student_name)
在Python中,你可以在运行时给任何对象动态添加或修改它的属性。在这个例子中,教室类(Classroom
)对象class205
在初始化时并没有学生属性(stu
),但之后给它动态添加了一个学生属性,并将学生类(Student
)对象stu01
赋值给了这个属性。这造成了一个结果,即class205
对象现在有了一个关联的学生对象。这样,就可以通过class205.stu.student_name
来访问这个学生对象的名字属性。
这种动态添加或修改对象属性的特性,使得Python具有非常大的灵活性,在很多情况下,这会使编程更加简洁高效。然而,这也可能导致代码的结构不清晰,因此在使用时需要谨慎。
2.多个关联对象:
class Student:
def __init__(self, name):
self.student_name = name
class Classroom():
def __init__(self, name):
self.classroom_name = name
self.stus = []
def addr(self,su):
self.stus.append(su)
# 创建一个教室对象
class205 = Classroom("205班")
# 创建一个学生对象
stu01 = Student("学生1")
stu02 = Student("学生2")
class205.addr(stu01)#学生存入教室
class205.addr(stu02)#学生存入教室
print(class205.stus[0].student_name)#通过教室调用学生
print(class205.stus[1].student_name)#通过教室
这段代码演示了如何在一个对象中关联多个对象。在Classroom
类中,我们定义了一个名为stus
的空列表作为属性。我们还定义了一个名为addr
的函数,其功能是将学生对象添加到stus
列表中。
当我们使用addr
函数给教室添加学生时,实际上就是将学生对象放入stus
列表。这样,每个Classroom
对象就可以关联多个Student
对象。我们可以通过索引访问stus
列表中的学生,例如class205.stus[0].student_name
,即可得到第一个学生的名字。
8.静态方法:
静态方法是一种在类定义中声明的,但不需要类实例就可以执行的方法。它不接收类特有的参数如self
(实例方法)或cls
(类方法)。静态方法的行为与常规的函数行为基本一致,但是因为它们在类的定义内部,所以与类的命名空间相关。
我们可以使用@staticmethod
装饰器来定义一个静态方法。
class MyClass:
@staticmethod
def my_method(x):
return x**2
# 静态方法可以通过类名直接调用
result = MyClass.my_method(5)
print(result) # 输出:25
my_method
是一个静态方法,它接收一个参数x
并返回这个参数的平方。注意它没有self
参数,这就意味着它不能访问类的实例属性或者方法,也就是说它与类的实例没有直接的关系。这样的方法可以在不创建类的实例的情况下,通过类名直接调用。
总结:静态方法很适合放置一些于类有关联,但不依赖于类实例状态的函数,比如一些工具函数或者与类相关的辅助函数等。
9.类属性和类方法:
1.类属性:
类属性是定义在类中且在方法外的属性。它们不像实例属性那样隶属于每个对象的实例,而是属于类本身。同一类的所有实例对象都可以访问这些属性。
例如,你可以创建一个包含共享状态或常量的类:
class MyClass:
attribute = "类属性"
print(MyClass.attribute) # 输出:类属性
2.类方法:
类方法是定义在类中的方法,它的第一个参数是类对象,通常以cls
来表示。类方法可以通过类直接调用,也可以通过实例调用。对于前者,Python会自动将类对象作为第一个参数传入。(类方法可以访问类的属性和方法,但是不能直接访问实例的属性和方法)
class MyClass:
@classmethod
def my_method(cls):
print("这是一个类方法")
MyClass.my_method() # 输出:这是一个类方法
10.总结:
面向对象编程(Object-Oriented Programming,简称OOP)是一种程序设计思想,它看待问题的方式是以对象为基础,而不是过程和函数。Python是一种支持面向对象编程的语言。
在Python中,简单来说,面向对象编程主要有以下几个方面:
- 类(Class):这是对象的模板或蓝图。类定义了一组属性(数据元素)和方法(与这些数据相关的操作)。例如,你可能会定义一个“学生”类,该类有姓名、年龄等属性,以及参加课程或学习的方法。
- 对象(Object):对象是类的实例。当你创建了一个类的实例,那就意味着你生成了一个对象。这个对象包含由类定义的属性和方法。
- 属性(Attribute):属性代表对象或类状态的数据。例如,“学生”类的属性可能会有姓名、年龄等。
- 方法(Method):方法是在类中定义的功能或行为,它是向类中添加函数的一种方式。例如,“学生”类可能有一个方法叫“学习”。
- 继承(Inheritance):继承是一种允许我们定义新类(子类)以修改或扩展已有类(父类)行为的方式。
- 封装(Encapsulation):封装涉及到将数据和与数据相关联的方法捆绑在一起,以保持安全和隐藏细节。
- 多态(Polymorphism):多态允许我们定义方法,这些方法由不同的对象以不同的方式执行。