Python学习笔记|面向对象编程

Bruce
2024-01-11 / 0 评论 / 17 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2024年01月11日,已超过130天没有更新,若内容或图片失效,请留言反馈。
AI摘要:面向对象编程(OOP)是一种程序设计思想,将对象作为程序的基本单元,对象包含数据和操作数据的函数。与面向过程的程序设计不同,面向对象的程序设计将计算机程序视为一组对象的集合,对象之间通过消息传递进行交互。通过一个例子,可以看到面向过程和面向对象在程序流程上的不同。面向过程的程序使用字典表示学生的成绩,通过函数来处理学生成绩;而面向对象的程序将学生的成绩表示为一个对象,对象拥有属性和方法,可以自己处理数据并打印出来。类是创建实例的模板,实例是具体的对象,方法是与实例绑定的函数。继承和多态是面向对象编程的重要特点,继承可以将父类的功能直接拿来使用,子类可以新增自己特有的方法,多态可以根据对象的类型调用相应的方法。获取对象信息可以使用type()、isinstance()和dir()函数,可以判断对象的类型、判断对象是否是某种类型、获取对象的属性和方法。实例属性和类属性是对象的属性,实例属性是通过实例变量绑定的,类属性是在类中定义的属性,类的所有实例都可以访问到。

一、定义

定义:面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据操作数据的函数

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。

面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

举一例说明面向过程和面向对象在程序流程上的不同,假设我们要处理学生的成绩表,为了表示一个学生的成绩:

# 面向过程的程序可以用一个dict表示

std1 = {'name':'Michael','score':98}
std2 = {'name':'Bob','score':81}

# 处理学生成绩可以通过函数实现,比如打印学生的成绩
def print_score(std):
    print('%s:%s' % (std['name'], std['score']))

print_score(std1)
print_score(std2)
Michael:98
Bob:81

如果采用面向对象的程序设计思想,首先思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。

如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来。

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
Bart Simpson: 59
Lisa Simpson: 87

二、类(Class)和实例(Instance)

(一)定义

类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

在Python中,通过class关键字定义类:

class Student(object): # 
    pass

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。

定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:

bart = Student()
bart
<__main__.Student at 0x1ee22272dd0>

变量bart指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样;

Student
__main__.Student

Student本身则是一个类。

可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:

bart.name = 'Bart Simpson'
bart.name
'Bart Simpson'

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:

class Students(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:

bart = Students('Bart Simpson', 59)
bart.name
'Bart Simpson'
bart.score
59

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且调用时不用传递该参数。此外,可以用默认参数、可变参数、关键字参数和命名关键字参数。

(二)数据的封装

面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的namescore这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

def print_score(std):
    print('%s:%s' % (std.name, std.score))

print_score(bart)
Bart Simpson:59

Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。

这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))
        
bart = Student('Bart Simpson', 59)
bart.print_score()
Bart Simpson: 59

封装的另一个好处是可以给Student类增加新的方法,比如get_grade:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'
        
lisa = Student('Lisa', 99)
bart = Student('Bart', 59)
print(lisa.name, lisa.get_grade())
print(bart.name, bart.get_grade())
Lisa A
Bart C

总结:

是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;

方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;

通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。

和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.age = 8
lisa.age
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In[41], line 4
      2 lisa = Student('Lisa Simpson', 87)
      3 bart.age = 8
----> 4 lisa.age
AttributeError: 'Student' object has no attribute 'age'

三、访问限制

从前面Student类的定义来看,外部代码可以自由地修改一个实例的name、score属性:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

bart = Student('Bart Simpson', 59)
bart.score
59
bart.score = 99
bart.score
99

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__。在Python中,实例的变量名如果以__开头,就变成了一个私有变量,只有内部可访问:

class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s:%s' % (self.__name, self.__score))

bart = Student('Bart Simpson', 59)
bart.__name
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In[6], line 11
      8         print('%s:%s' % (self.__name, self.__score))
     10 bart = Student('Bart Simpson', 59)
---> 11 bart.__name
AttributeError: 'Student' object has no attribute '__name'

这样即可确保外部代码不能随意修改对象内部的状态。

如果需要外部代码获取name和score,可以给Student类增加get_nameget_score方法:

class Student(object):
    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score

bart = Student('Bart Simpson', 59)
a = Student.get_name(bart)
b = Student.get_score(bart)
print(a,':',b)
Bart Simpson : 59

如果要允许外部代码修改score,可以再给Student类增加set_score方法:

class Student(object):
    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score
        
    def set_score(self, score): # 在定义方法实,可以对参数做检查,避免传入无效参数
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

bart = Student('Bart Simpson', 59)
bart.set_score(87)
print(bart.get_score())
87

在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量;

下划线开头的实例变量名,比如_name,外部是可以访问的,但按照约定俗成的规定,要视为私有变量,不要随意访问;

双下划线开头的实例变量也不是一定不能从外部访问,Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量;但不同版本的Python解释器可能会把__name改成不同的变量名,因此不要这样访问变量名。

一种错误写法示例:

>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'

表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。

四、继承和多态

(一)继承

当定义一个class的时候,可以从某个现有的class继承,新的class称为子类,被继承的class被称为父类。

比如一个名为Animal的class,有一个run()方法可以打印:

class Animal(object):
    def run(self):
        print('Animal is running...')

当需要编写Dog和Cat类时,就可以直接从Animal类继承:

class Dog(Animal):
    pass

class Cat(Animal):
    pass

通过继承,子类获得了父类的全部功能:

dog = Dog()
dog.run()

cat = Cat()
cat.run()
Animal is running...
Animal is running...

同时可以给子类添加一些方法:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

    def eat(self):
        print('Eating meat...')

可以对代码做一些改进:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

class Cat(Animal):

    def run(self):
        print('Cat is running...')

dog = Dog()
dog.run()

cat = Cat()
cat.run()
Dog is running...
Cat is running...

当子类和父类存在相同的run()方法时,子类的run()覆盖了父类的run(),在运行代码的时候,会优先调用子类的run()

(二)多态

a = list() #a是list类型
b = Animal() #b是Animal类型
c = Dog() #c是Dog()类型

可以使用isinstance()判断变量的类型:

isinstance(a, list)
True
isinstance(b, Animal)
True
isinstance(c,Dog)
True

Dog从Animal继承,因此Dog的实例c的数据类型即是Dog,也是Animal。

isinstance(c, Animal)
True

总结:在继承关系中,如果一个实例的数据类型是某个子类,那么它的数据类型可以被看做是父类。

def run_twice(animal):
    animal.run()
    animal.run()

run_twice(Animal())
Animal is running...
Animal is running...
run_twice(Cat())
Cat is running...
Cat is running...

定义一个Tortoise类型,也从Animal派生:

class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')
run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

新增一个Animal的子类,不必对run_twice()做任何修改。

传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法。

开闭原则:

对扩展开放:允许新增Animal子类;数。

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

总结:继承可以把父类的所有功能都直接拿过来,不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

五、获取对象信息

判断对象的类型可以使用以下办法:

(一)使用type()

type(123)
int
type('str')
str
type(None)
NoneType

如果一个变量指向一个函数或者类,也可以使用type()判断:

type(abs)
builtin_function_or_method

如果要在if语句中判断,就需要比较两个变量的type类型是否相同:

type(123) == type(456)
True
type(123) == int
True
type('abc') == int
False
type('abc') == type('123')
True
type('abc') == str
True
type('abc') == type(123)
False

如果要判断一个对象是否是函数怎,可以使用types模块中定义的常量:

import types
def fn():
    pass
type(fn) == types.FunctionType
True
type(abs) == types.BuiltinFunctionType
True
type(lambda x:y) == types.LambdaType
True
type((x for x in range(10))) == types.GeneratorType
True

(二)使用isinstance()

对于class的继承关系来说,使用type()不方便,要判断class的类型可以使用isinstance()函数。

如果继承关系是object -> Animal -> Dog -> Husky,那么通过isinstance()可以判断一个对象是否是某种类型:

class Animal(object):
    pass
    
class Dog(Animal):
    pass
    
class Husky(Dog):
    pass
    
a = Animal()
d = Dog()
h = Husky()
isinstance(h, Husky)
True
isinstance(h, Dog)
True
isinstance(d, Husky)
False

isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。

能用type()判断的基本类型也可以用isinstance()判断。

isinstance('a', str)
True
isinstance(123, int)
True
isinstance(b'a', bytes)
True

还可以判断一个变量是否是某些类型中的一种:

isinstance([1, 2, 3], (list, tuple))
True
isinstance((1, 2, 3), (list, tuple))
True

(三)使用dir()

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list。比如,获得一个str对象的所有属性和方法:

dir('ABC')
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rmod__',
 '__rmul__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isascii',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'removeprefix',
 'removesuffix',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

len('ABC')
3
 'ABC'.__len__()
3

自己写的类,如果也想用len(myObj)可以写一个__len__()方法:

class MyDog(object):
    def __len__(self):
        return 100

dog = MyDog()
len(dog)
100

配合getattr()setattr()以及hasattr()可以直接操作一个对象的状态:

class MyObject(object):
    def __init__(self):
        self.x = 9
    def power(self):
        return self.x * self.x

obj = MyObject()

配合getattr()setattr()以及hasattr(),可以直接操作一个对象的状态:测试对象属性:属性name,则返回False。

getattr(object, name[, default]):name,则返回False。

getattr()函数用于获取对象的属性值。它接受三个参数:对象object、属性名name和可选的默认值default。如果对象object具有属性name,则返回该属性的值;如果对象object没有属性name,则返回默认值default(如果提供了默认值);如果没有提供默认值,则会抛出AttributeError异常。

setattr(object, name, value):

setattr()函数用于设置对象的属性值。它接受三个参数:对象object、属性名name和属性值value。如果对象object具有属性name,则将其设置为value;如果对象object没有属性name,则会创建一个新的属性,并将其设置为value。

hasattr(object, name):

hasattr()函数用于检查对象是否具有指定的属性。它接受两个参数:对象object和属性名name。如果对象object具有属性name,则返回True;如果对象object没有属性name,则返回False。

hasattr(obj, 'x') # 有属性'x'吗?
True
obj.x
9
hasattr(obj, 'y') # 有属性'y'吗?
False
setattr(obj, 'y', 19) # 设置一个属性'y'
hasattr(obj, 'y') # 有属性'y'吗?
True
getattr(obj, 'y') # 获取属性'y'
19
obj.y # 获取属性'y'
19

如果试图获取不存在的属性,会抛出AttributeError的错误:

getattr(obj, 'z') # 获取属性'z'
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In[52], line 1
----> 1 getattr(obj, 'z') # 获取属性'z'
AttributeError: 'MyObject' object has no attribute 'z'

可以传入一个default参数,如果属性不存在,就返回默认值:

getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404

也可以获得对象的方法:

hasattr(obj, 'power') # 有属性'power'吗?
True
getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x000002A390FEE170>>
fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x000002A390FEE170>>
fn() # 调用fn()与调用obj.power()是一样的
81

(六)实例属性和类属性

Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量,或者通过self变量:

class Student(object):
    def __init__(self,name):
        self.name = name

s = Student('Bob')
s.score = 90

如果Student类本身需要绑定一个属性,可以直接在class中定义属性,这种属性是类属性,归Student类所有:

class Student(object):
    name = 'Student'

定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。

class Student(object):
    name = 'Student'

s = Student()
print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student


print(Student.name) # 打印类的name属性
Student


s.name = 'Michael' # 给实例绑定name属性
print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
del s.name # 如果删除实例的name属性
print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

总结: 在编写程序的时候,不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当删除实例属性后,再使用相同的名称,访问到的将是类属性。

0

评论 (0)

取消