一、面向对象技术简介
Python是一门面向对象的语言,所以在Python中创建一个类和对象是很容易的,接下来简要说明一下面向对象的一些基本特征
类:用于描述具有相同属性和方法的对象的集合,它定义了该集合中每个对象所共有的属性和方法,对象是类的实例。
类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外,类变量通常不作为实例变量使用。
数据成员:类变量或者实例变量,用于处理类及其实例对象的相关数据
方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫做方法的覆盖,也称为方法的重写
局部变量:定义在方法中的变量,只作用于当前实例的类
实例变量:在类的声明中,属性是用变量来表示的,这种变量就被称为实例变量,是在类声明的内部但是在类的其他成员方法之外声明的
继承:即一个派生类,继承基类的字段和方法,继承也允许把一个派生类的对象作为一个基类的对象对待,例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟一个(is a)的关系,例如,Dog是一个Animal
实例化:创建一个类的实例,类的具体对象
方法:类中定义的函数
对象:通过类定义的数据结构实例,对象包括两个数据成员(类变量和实例变量)和方法
简要举个例子,如下所示:
#定义类和类变量
class Employee:#这个employee就是类,可以理解为设计图纸,其中的company和count就是类变量
company="某某公司"
count=0
#变量count用于表示公司实际总共招的人数
#定义”实例变量”和方法(含局部变量)
#__init__叫构造方法,专门用来实例化时自动执行
def __init__(self,name,salary):
#下面的self.name和self.salary就是实例变量,
# 区别就是每个员工的姓名和对应的salary都不一样
self.name=name
self.salary=salary
#公司每招一个新人,总人数就会加一,这里是修改类变量
Employee.count+=1
#这里的work就是一个普通的方法,就是类里面定义的函数
def work(self):
#下面的hours就是局部变量
#特点:只在这次work执行时有用,函数一结束,hours就没了
hours=8
print(f"{self.name}had worked {hours} hours")
self._record_work(hours)
#这里演示了调用内部方法,演示局部变量只在方法内有效
def _record_work(self,hours):
print(f"had recorded{self.name}worked {hours}hours")
#“继承”演示
#假设现在要招一个销售经理,他除了是个员工,还有额外的提成
class SaleManager(Employee):
#这里,括号里面写Employee,就表示这个SaleMange就是Employee的子类
def __init__(self,name,salary,bonus):
super(SaleManager, self).__init__(name,salary)
#这里super的作用就是用父类的方法先处理好这两个变量,然后再下面这个是这个子类自己的实例变量
self.bonus=bonus
#“方法重写”演示
def work(self):
#这个work方法和父类里面的方法同名,这就是方法重写,也就是覆盖
print(f"{self.name}今天陪客户吃饭,提成是{self.bonus}")
#“实例化”演示
zhangsan=Employee("张三",8000)
lisi=Employee("李四",9000)
#这两个变量都是对象,也就是具体的实例,都拥有自己的实例变量但是他们共享Employee.company这个类变量
#演示“is - a”关系,把子类当父类用
#这里假设我们要创建一个销售经理对象,因为子类是继承于父类的,所以下面的代码成立
wangwu=SaleManager("王五",12000,5000)
print(f"{wangwu.name}属于{wangwu.company}")
#这个就叫做is -a关系,也就是SaleManage is a Employee
#演示“类变量”是共享的
print(f"公司总人数:{Employee.count}")
#在此之前。每实例化一次,count就加一,到代码最后的时候,可以发现,所有对象共享共用这个count,
# 这就是类变量的特性
类就是一张设计图纸(比如Employee),对象就是照这张图纸造出来的具体实物(比如zhangsan),造实物这个过程叫实例化。图纸上所有实物共有的特征叫类变量(比如公司名company),每个实物独有的特征叫实例变量(比如名字name和工资salary),图纸上定义的功能函数叫方法(比如work()),方法里临时用一下用完就丢的变量叫局部变量(比如hours),这些变量和方法统称为数据成员。你还可以拿一张旧图纸去改造成新图纸,这叫继承(比如SalesManager继承Employee),如果新图纸对旧图纸上的某个功能不满意,可以重新写一遍覆盖掉,这叫方法重写(比如重写work()改成"陪客户")。因为新图纸造出来的东西本质上还是旧图纸那个类型,所以可以说"销售经理是一个员工",这就是is-a关系。
二、Python内置类属性
__dict__:类的属性,包含一个字典,由类的数据属性构成
__doc__类的文档字符串
__name__类名
__module__:类定义所在的模块,类的全名是__main__.className,如果类位于一个导入模块mymod中,那么className.__module__等于mynod
__bases__:类的所有父类构成元素(包含了一个由所有父类组成的元组)
三、Python对象销毁
Python中使用了引用计数这一简单技术来跟踪和回收垃圾,在Python内部记录着所有使用中的对象各有多少引用,一个内部跟踪变量,称为一个引用计数器,当对象被创建时,就创建了一个引用计数器,当这个对象不再需要时,也就是说,这个对象的引用计数变为0的时候,它被垃圾回收,但是回收不是立即回收,而是由解释器在适当的时机,将垃圾对象占用的内存空间回收。
a = 40 # 创建对象 <40>
b = a # 增加引用, <40> 的计数
c = [b] # 增加引用. <40> 的计数
del a # 减少引用 <40> 的计数
b = 100 # 减少引用 <40> 的计数
c[0] = -1 # 减少引用 <40> 的计数
垃圾回收机制不仅针对引用计数为0的对象,同样也可以处理循环引用的情况,循环引用指的是,两个对象相互引用,但是没有其他变量引用他们,在这种情况下,仅使用引用计数器是不够的,Python的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器,作为引用计数器的补充,垃圾收集器也会留心被分配的总量很大(也就是没有通过引用计数销毁的那些)的对象,在这种情况下,解释器会暂停下来,试图清理所有未引用的循环。
class Point:
def __init__(self,x=0,y=0):
self.x=x
self.y=y
def __del__(self):
class_name=self.__class__.__name__
print(class_name,"销毁")
pt1=Point()
pt2=pt1
pt3=pt1
print(id(pt1),id(pt2),id(pt3))
del pt1
del pt2
del pt3
程序被运行结束之后,Python会统一进行清理所有对象,但是不会保证调用__del__方法
或者更准确的说,程序引用计数器时虽然归零了,但是Python解释器关闭时不一定执行每个对象的__del__
但是在上面的例子中,如果只删除前两行,而不删除第三行的话,输出之后三个id,没有Point 销毁,那么简要举个例子来说明一下:
pt1、pt2、pt3是三个小朋友的手,都抓着同一个气球
del pt1 = 一个小朋友松手了,但还有两个抓着,气球不会飞走
del pt2 = 第二个也松手了,还剩一个抓着
del pt3 = 最后一个也松手了,没人抓了,气球飞走(销毁),飞走前喊一声"我走了"(__del__输出)
总的来说,这段代码的作用就是让人直观的看到当一个对象没人引用的时候,Python会处理它的演示过程
四、类的继承过程中的注意事项
面向对象的编程带来的好处之一就是代码的重用,实现这种重用的方法之一就是通过继承机制,通过继承创建的新类称为子类或者派生类,被继承的就叫基类、父类或者超类,在Python中继承中的一些特点是:
1.如果在子类中需要父类的构造方法,就需要显式的调用父类的构造方法,或者不重写父类的构造方法
2.在调用基类的方法时,需要加上基类的类名前缀,且需要带上self参数变量,区别在于类中调用普通函数时并不需要带上self函数
3.Python总是首先查找到对应类型的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找,
如果在继承元组中列了一个以上的类,那么它就被称作“多重继承”
## 前提:我们用这个父类做演示
class Animal:
def __init__(self, name):
print(f"Animal的构造方法,名字是{name}")
self.name = name
def eat(self):
print(f"{self.name}在吃东西")
## 特点1:子类要不要调用父类的构造方法?子类造对象时,如果父类有,"初始化步骤",
# 子类要么主动喊一声老爸帮忙处理,要么干脆不写自己的初始化让老爸全权处理。
# 情况A:子类不写,`__init__`,老爸自动帮忙 **
class Dog(Animal): # 子类没写 __init__
pass
dog = Dog("旺财") # 自动调用父类的 __init__
# 输出:Animal的构造方法,名字是旺财
#情况B:子类写了`__init__`,必须手动喊老爸 **
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name) # ← 这行就是"显式调用父类构造方法",必须有
self.color = color
cat = Cat("咪咪", "白色")
# 输出:Animal的构造方法,名字是咪咪 (父类的构造方法被调用了)
#如果子类写了`__init__`,但不调父类,老爸的初始化就不执行:
class Fish(Animal):
def __init__(self, name):
self.name = name # 没调 super().__init__()
# 老爸的__init__根本没执行,虽然看起来也能用,但破坏了继承规范
## 特点2:调用父类方法时,为什么要加类名前缀和self?
#子类想用老爸的方法时,有两种写法。如果 ** 重写了 ** 这个方法还想用老爸的版本,
# 就得"点名"说我要用`Animal`里的那个。 正常情况(没重写):直接调用,自动去父类找
class Dog(Animal):
pass
dog = Dog("旺财")
dog.eat() # 子类没有eat,自动去父类找
# 输出:旺财在吃东西
#重写后还想用老爸的版本:必须点名 + 传self
class Dog(Animal):
def eat(self):
# 我想先吃老爸的饭,再加点自己的
Animal.eat(self) # ← 加类名前缀,显式传入self
print(f"{self.name}还啃了根骨头")
dog = Dog("旺财")
dog.eat()
# 输出:
# 旺财在吃东西 (老爸的)
# 旺财还啃了根骨头 (自己的)
#对比:类里面调用普通函数不需要self,但调用父类方法必须带self **,
def 普通函数():
print("我是普通函数")
class Test:
def method(self):
普通函数() # ✅ 直接调,不需要self
Animal.eat(self) # ✅ 调父类方法,必须带self
## 特点3:Python找方法的顺序(查找顺序)
#人话版: ** 调用方法时,Python的优先级是 ** 自己(子类)>老爸(父类)>爷爷(父类的父类) ** 。
class Animal:
def say(self):
print("动物叫")
class Dog(Animal):
def say(self):
print("汪汪汪") # 子类重写了
class Husky(Dog):
pass # 二哈啥也没写
h = Husky()
h.say() # Python先找Husky → 没有 → 找Dog → 找到了"汪汪汪"
# 输出:汪汪汪
#如果Dog也没写,就继续往上找Animal的,"动物叫"。
## 额外补充:多重继承(继承元组里有多个类)
# 一个儿子可以有多个老爸,写的时候用逗号隔开。
class Flyable:
def fly(self):
print("飞起来了")
class Swimmable:
def swim(self):
print("游起来了")
class Duck(Flyable, Swimmable): # 多重继承,同时继承两个类
pass
duck = Duck()
duck.fly() # 输出:飞起来了
duck.swim() # 输出:游起来了
五、基础重载方法
|
序号 |
方法, 描述 & 简单的调用 |
|
1 |
__init__ ( self [,args...] )构造函数 |
|
2 |
__del__( self )析构方法, 删除一个对象 |
|
3 |
__repr__( self )转化为供解释器读取的形式 |
|
4 |
__str__( self )用于将值转化为适于人阅读的形式 |
|
5 |
__cmp__ ( self, x )对象比较 |
六、类属性与方法
(1)类的私有属性
__private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs。
(2)类的方法
在类的内部,使用 def 关键字可以为类定义一个方法,与一般函数定义不同,类方法必须包含参数 self,且为第一个参数
(3)类的私有方法
__private_method:两个下划线开头,声明该方法为私有方法,不能在类的外部调用。在类的内部调用 self.__private_methods
class JustCounter:
__secertCount=0#声明这是一个私有变量类
publicCounter=0#这是一个公共类变量
def count(self):
self.__secertCount+=1#每调用一次后加1
self.publicCounter+=1#每调用一次后+1
print(self.__secertCount)#打印当前值
counter=JustCounter()#创建一个JusuCounter类的实例对象,赋值给变量counter
#此时对象内部状态:counter.__secretCount = 0(实例私有变量,初始为0),
#counter.publicCounter = 0(实例公共变量,初始为0)
#JustCounter.__secretCount = 0(类私有变量,未被修改)
#JustCounter.publicCounter = 0(类公共变量,未被修改)
counter.count()
counter.count()
print(counter.publicCounter)
#创建一个计数器对象,然后调用两次计数方法,
# 每次让计数器+1并打印当前值,所以输出1和2。 相当于"买了个计步器,走了两步,看了两眼屏幕"
#print(counter.__secertCount)
七、注意事项
|
写法 |
名称 |
访问权限 |
举例 |
|
|
双下划线开头结尾 |
特殊方法,Python内部定义,你别乱改 |
, |
|
|
单下划线开头 |
受保护(protected),建议只在类内部和子类用,但Python不强制 |
|
|
|
双下划线开头 |
私有(private),名字被改成 ,外部不能直接访问 |
|
如果没有名称修饰,子类一不小心就会覆盖父类的私有变量,造成bug。有了名称修饰,父子类的私有变量各自独立。
class Parent:
def __init__(self):
self.__secret = "老爸的秘密"
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = "儿子的秘密" # 这个和老爸的不是同一个变量!
child = Child()
# 老爸的 __secret → _Parent__secret
# 儿子的 __secret → _Child__secret
# 两个互不干扰!
同时用代码验证“改名过程”
class Runoob:
__site = "www.runoob.com" # 你写的时候叫 __site
runoob = Runoob()
# 用你写的名字访问 → 报错(因为Python已经改名了)
print(runoob.__site) # ❌ AttributeError
# 用Python改完后的名字访问 → 成功
print(runoob._Runoob__site) # ✅ 输出:www.runoob.com
# 更直观:查看对象到底有哪些属性
print(dir(runoob)) # 输出:['_Runoob__site', ...] 看到没,
实际名字带 _Runoob
核心目的还是为了解决子类意外覆盖父类的私有变量

2916

被折叠的 条评论
为什么被折叠?



