学习笔记:Python3 面向对象
仅为个人查阅使用,如有错误还请指正。
面向对象编程就是一种程序设计思想。把对象当作程序的基本单元,一个对象包含了数据和操作数据的函数。
这种设计思想是从自然界中来的。因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义一个运动员类:Class-Player,是指运动员这个概念,而实例(Instance)则是一个个具体的Player,比如:Jordan,Durant他们是具体的。
有编程经验的都知道,面向对象的三个特点:封装,继承,多态。
-
类和实例
类是抽象的模板。比如Player类。实例是一个个具体的对象,每个对象都拥有相同的方法。
-
定义类
通过关键字
class
,后面紧跟类名(首字母大写),最后是(object)。可以表明他是承继哪个类,如果没有合适的,就选择object
这个类,因为它是所以类的祖宗。class Player(object): pass
-
创建实例
跟函数调用差不多,这里是类名()的操作。
# 创建实例1 player1 = Player() # 创建实例2 player2 = Player()
-
绑定属性
通过创建
__init__.py
的方法,self
代表实例本身。class Player(object): def __init__(self, name, age): self.name = name self.age = age
获取属性的方式可以通过对象.属性
# 创建实例1 player1 = Player("Jordan", 45) # 获取属性 name = player1.name age = player1.age
-
封装
现在,我们知道实例可以拥有这些属性。可以直接在类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。而该函数就是类的方法。
实现一个打印姓名年龄的方法
class Player(object): def __init__(self, name, age): self.name = name self.age = age def print_name_age(self): print("%s, %d"%(self.name, self.age)) player1 = Player("Jack", "30") print(player1.print_name_age())
从一个调用者来看,我只要在创建实例的时候输入
name
,age
。并且调用该方法,获得我想要的。具体类里面怎么实现的,我不需要去管。
-
-
访问限制
在前面的类和实例中,我们了解到在类的内部,可以有属性和方法。并且外部代码还可以自由修改一个实例的属性。这样其实是不安全的,我们应该要避免这样的操作。也就是要把这变量从公有变到私有。
-
属性私有化
在Python中,使用两个下划线
__
就可以表示私有属性。继续改我们的Player这个类。class Player(object): def __init__(self, name, age): self.__name = name self.__age = age def print_name_age(self): return "%s, %d" % (self.__name, self.__age) player1 = Player("Jack", 30) print(player1.__name) # ERROR
属性被私有化之后,就不能在获取了。
-
获取,修改私有化属性
其实很简单,就是通过
get
,set
方法。现在给类中添加get_name
,get_age
方法,以及set_name
,set_age
方法。继续改我们的Player类。class Player(object): def __init__(self, name, age): self.__name = name self.__age = age def get_name(self): return self.__name def get_age(self): return self.__age def set_age(self, age): self.__age = age def print_name_age(self): return "%s, %d" % (self.__name, self.__age) player1 = Player("Jack", 30) player1.set_age(100) print(player1.get_name()) # Jack print(player1.get_age()) # 100
很明显,实现了改功能。本来可以不加方法,就可以直接修改,获取,现在加了方法有点画蛇添足。其实不是,加方法的目的是为了对参数做检查,避免传入无效的参数。
实例
def set_age(self, age): if 0 <= age <= 100: self.__age = age else: raise ValueError('bad score')
当然,在实际开发项目中,你会看到一个下划线开头的实例属性,
_name
。访问是可以访问,但请你把它视为私有。最后,如果不写
get
方法能不能调用,当然可以,因为Python解释器把__name
属性变成了_Player__name
。但是建议你忘记。不推荐
-
-
继承和多态
-
继承
在类的定义就讲过,所有的类都可以继承
object
,同样也可以继承我们自己定义的类。比如说,我定义一个
Animal
动物类,该动物有一个run()
方法。如果我再编写一个狗类和猫类,我就可以去继承这个动物类。
实例
class Animal(object): def run(self): return "animal is running" class Dog(Animal): pass class Cat(Animal): pass
继承的好处:子类可以获得父类的全部功能,也就是说狗类和猫类已经拥有
run()
方法了。基于上面的实例,我们去调用。
dog = Dog() print(dog.run()) cat = Cat() print(cat.run()) # output animal is running animal is running
为了符合逻辑性,我们继续改代码
class Animal(object): def run(self): return "animal is running" class Dog(Animal): def run(self): return "dog is running" class Cat(Animal): def run(self): return "cat is running" dog = Dog() print(dog.run()) cat = Cat() print(cat.run()) # output: dog is running cat is running
这样就很明确了,是谁在跑。
当子类和父类都存在相同的
run()
方法时。子类的已经把父类的给覆盖了。 -
多态
对于初学者来说,多态还是有点难理解的,只能不断的通过程序来强化。
class Animal(object): def run(self): return "animal is running" class Dog(Animal): pass class Cat(Animal): def run(self): return "cat is running" dog = Dog() print(isinstance(dog, Dog)) print(isinstance(dog, Animal)) # output: True True
从上面的代码可以看出,
dog
属于Dog
类型,又属于Animal
类型。同理,cat
也是这样的。当我们传入一个对象时,如果该对象有
run()
方法,就执行。没有的话,就去调用父类的run()
方法。这就是多态。这就是动态语言,动态语言调用实例方法,不检查类型,只要方法存在,参数正确,就可以调用。
继续研究
import json f = open('/path/to/file.json', 'r') print(json.load(f)) class Students(object): def __init__(self, strlist): self.strlist = strlist def read(self): return(self.strlist) s = Students('["Tim", "Bob", "Alice"]') print(json.load(s))
因为
f
对象具有read()
方法,而s
对象也有read()
方法。因此就可以认为,任何对象,只要有
read()
方法,就称为File-like Object,都可以传给json.load()
。
-
-
获取对象信息
判断对象类型:
isinstance()
函数,获取对象类型:type()
。获取对象属性与方法:
dir()
函数。 -
实例属性和类属性
前面我们提到,可以使用
__init__.py
方法进行实例属性绑定。同样,类也可以有属性。且这个属性归类所有。
实例
class Player(object): country = "chinese"
当我们定义一个类属性后,这个属性虽然归类所有,但类的所以实例都可以访问到。
测试以上的说法
p = Player() # 创建实例p print(p.country) # 打印country属性,因为实例并没有country属性,所以会继续查找类属性 print(Player.country) # 打印类属性 p.country = "small Japan" # 给实例绑定country属性 print(p.country) # 由于实例属性的优先级高于类属性,因此屏蔽了类的属性 print(Player.country) # 但是类属性并未消失。还是可以访问 del p.country # 删除实例绑定的属性之后 print(p.country) # 再去调用,就只能委屈得到类属性了。
注意:实际编写程序的时候,千万不要对实例属性和类属性使用相同的名字。
-
类方法
前面说到实例有属性和方法,那类也有属性,同样也有方法。
实例
class Person(object): count = 0 @classmethod def how_many(cls): return cls.count def __init__(self, name): self.name = name Person.count = Person.count + 1 print(Person.how_many()) p1 = Person('Bob') print(Person.how_many())
可以看到,通过标记一个@classmethod,该方法将绑定到
Person
类上,而非类的实例。类方法的第一个参数将传入类本身,通常将参数名命名为cls
,上面的cls.count
实际上相当于Person.count
。因为是在类上调用,而非实例上调用,因此类方法无法获得任何实例变量,只能获取类的引用。
-
_slots_
因为可以动态绑定,所以我们需要限制,该方法就是用来限制实例的属性。例如如下程序:
class Student(object): __slots__ = ('name', 'age') s = Student() s.name = "Jack" s.age = 20 s.score = 90 # AttributeError
上面程序可以看出:用tuple定义允许绑定的属性名称,由于score没有被放到
__slots__
中,所以不能绑定score属性。如果有Student有子类,slots
定义的属性对子类是没有作用的。 -
@property
我们在前面提到了访问限制。关于变量的关系。我们使用了
get
,set
的方法。看上去有点复杂,没有直接用属性这么方便。那Python中有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?是存在的。
好比装饰器可以给函数动态加上功能,对于类方法,装饰器一样起到作用。
Python内置的
@property
装饰器就是负责把一个方法变成属性调用的。例如如下程序class Student(object): @property def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value s = Student() s.score = 60 print(s.score)
@property
的作用就是把一个getter方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器@score.setter
负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作。再看代码,s.score = 60
实际转化为s.set_score(60)
,s.score
实际转化为s.get_score()
。所以本质其实还是通过getter和setter方法来实现的。 -
枚举类
在某些情况下,一个类的对象是有限且固定的,比如季节类,它只有4个对象;行星类,目前只要8个对象;月份类,它有12个对象,这种实例有限且固定的类,被称为枚举类。
-
定义枚举类
通过使用Enum()函数来创建。第一个参数是枚举类的类名,第二个参数是一个元组格式的枚举值。
import enum Season = enum.Enum('Season', ('SPRING', 'SUMMER', 'FALL', 'WINTER'))
-
访问枚举值
每个成员都有 name、value 两个属性。
# 直接访问指定枚举 print(Season.SPRING) # 访问枚举成员的变量名 print(Season.SPRING.name) # 访问枚举成员的值 print(Season.SPRING.value) # output: Season.SPRING SPRING 1
此外,还提高了一个
__members__
属性,该属性返回一个 dict 字典。for name, member in Season.__members__.items(): print(name, '=>', member, ',', member.value) # output: SPRING => Season.SPRING , 1 SUMMER => Season.SUMMER , 2 FALL => Season.FALL , 3 WINTER => Season.WINTER , 4
-
-
type()
前面有提到,type()函数可以查看变量的类型,但如果想使用type()直接查看某个类的类型呢?请看以下代码:
class Role: pass r = Role() # 查看变量r的类型 print(type(r)) # <class '__main__.Role'> # 查看Role类本身的类型 print(type(Role)) # <class 'type'>
从上面的输出结果可以卡的看到,Role类本身的类型是 type。这句话有点拗口,怎样理解 Role 类的类型是 type?
从 Python 解释器的角度来看,当程序使用 class 定义 Role 类时,也可理解为定义了一个特殊的对象(type 类的对象),并将该对象赋值给 Role 变量。因此,程序使用 class 定义的所有类都是 type 类的实例。
实际上 Python 完全允许使用 type() 函数(相当于 type 类的构造器函数)来创建 type 对象,又由于 type 类的实例就是类,因此 Python 可以使用 type() 函数来动态创建类。例如如下程序:
def fn(self): print('fn函数') # 使用type()定义Dog类 Dog = type('Dog', (object,), dict(walk=fn, age=6)) # 创建Dog对象 d = Dog() # 分别查看d、Dog的类型 print(type(d)) print(type(Dog)) d.walk() print(Dog.age) #output: <class '__main__.Dog'> <class 'type'> fn函数 6
使用 type() 定义类时可指定三个参数:
1、参数一:创建的类名。
2、参数二:该类继承的父类集合。由于 Python 支持多继承,因此此处使用元组指定它的多个父类。即使实际只有一个父类,也需要使用元组语法(必须要多一个逗号)。
3、参数三:该字典对象为该类绑定的类变量和方法。其中字典的 key 就是类变量或方法名,如果字典的 value 是普通值,那就代表类变量;如果字典的 value 是函数,则代表方法。
由此可见,第 5 行代码定义了一个 Dog 类,该类继承了 object 类,还为该类定义了一个 walk() 方法和一个 age 类变量。
-
元类
理解元类不难,看完以下整个过程,如果你还不明白,那你别学了。
前面讲了 type() 函数,其实它时适用于动态创建相对简单的类,如果要创建复杂的类,则需要通过 MetaClass(元类)的方式。
元类可以简单的理解为,就是创建类的类。
-
定义元类
需令其继承与 type 类,且默认的命名习惯是,让类名以 MetaClass 结尾。不仅如此,元类中需要定义并实现
__new__()
方法(一定要有返回值)。因为元类在创建类时,该__new__()
方法将会被调用,用来生成新建的类。# 定义Item元类,继承type class ItemMetaClass(type): # cls代表动态修改的类 # name代表动态修改的类名 # bases代表被动态修改的类的所有父类 # attr代表被动态修改的类的所有属性、方法组成的字典 def __new__(cls, name, bases, attrs): # 动态为该类添加一个cal_price方法 attrs['cal_price'] = lambda self: self.price * self.discount return type.__new__(cls, name, bases, attrs)
上面程序中,在重写该方法时为目标类动态添加了一个 cal_price 方法。
-
使用元类创建类
# 定义Book类 class Book(metaclass=ItemMetaClass): __slots__ = ('name', 'price', '_discount') def __init__(self, name, price): self.name = name self.price = price @property def discount(self): return self._discount @discount.setter def discount(self, discount): self._discount = discount # 定义cellPhone类 class CellPhone(metaclass=ItemMetaClass): __slots__ = ('price', '_discount' ) def __init__(self, price): self.price = price @property def discount(self): return self._discount @discount.setter def discount(self, discount): self._discount = discount
上面程序定义了 Book 和 CellPhone 两个类,在定义这两个类时都指定了元类信息,因此当 Python 解释器在创建这两个类时,ItemMetaClass 的
__new__
方法就会被调用,用于修改这两个类。所以定义的这两个类,依然是有 cal_price() 方法。如下代码进行检测。
b = Book("Python基础教程", 89) b.discount = 0.75 # 创建Book对象的cal_price()方法 print(b.cal_price()) cp = CellPhone(2399) cp.discount = 0.85 # 创建CellPhone对象的cal_price()方法 print(cp.cal_price()) # output: 66.75 2039.1499999999999
从上面的运行结果来看,通过使用元类可以动态修改程序中的一批类,对它们集中进行某种修改。这个功能在开发一些基础性框架时非常有用,程序可以通过使用元类为某一批需要具有通用功能的类添加方法。
-
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步