【python学习笔记】类和面向对象

本文详细介绍了Python中面向对象编程的基本概念,包括类的定义、构造方法、对象的创建与使用,以及类属性、实例属性、局部变量的区别。此外,还讨论了实例方法、静态方法和类方法,以及描述符、property函数在属性访问控制中的作用。文章进一步讲解了封装、继承、多态等面向对象的核心特性,并介绍了枚举类的使用。
摘要由CSDN通过智能技术生成

python和java一样,是一种面向对象的语言,作为一种面向对象的语言,python中同样有类、对象、属性、方法等常用的概念。

定义类

python中使用class关键字来定义一个类,比如我们定义一个people类,有姓名和年龄两个属性,可以执行走路的方法。

class People:
    '''这是类的说明文档'''
    name = ""
    age = 0
    def walk(self):
        print("people is walking")

如上面的代码所写,和函数一样,我们也可以为类定义说明文档,位置在类体的第一行。

类的构造方法

python中的构造方法是__init__()方法,在创建类时,可以手动添加该方法。每当创建这个类的对象时,都会调用类的构造方法。
__ init__()中可以包含多个参数,但是第一个参数必须是self参数。如果没有定义类的构造函数,python会自动生成一个参数仅包含self的构造函数。
和java不同的是,python的语法不支持方法重载,所以一个类中只能有一个构造函数。

class People:
    '''这是类的说明文档'''
    name = "ZhangSan"
    age = 18

    def __init__(self):
        print("不带参数的构造函数") 
    
    def walk(self):
        print("people is walking")

类对象的创建和使用

类对象的创建

以上面定义的People类为例,我们将People类实例化为liSi对象。

liSi = People()

在python的类中,self是一个特殊参数,不需要手动传值,python会自动给它传值。

类对象的使用

可以直接使用已创建好的对象来访问其中的属性和方法。

print(liSi.name)
print(liSi.age)
liSi.walk()
#修改变量中的值
liSi.age = 30
print(liSi.age)
#给对象增加属性
liSi.height = 178
print(liSi.height)
#删除对象的属性
del liSi.age
print(liSi.age)
del liSi.height
print(liSi.height)

从上面的删除方法中可以看出,在删除age属性时,删除前的值为30,删除后的值为18;而在删除height属性时,删除后再调用该属性直接就会报错,这一点主要是因为前面通过liSi对象,赋值了age和height两个属性,实际上这两个属性都是实例变量,当删除了这两个实例变量后,age变为了类变量,而height就没有了。这一点后面的类变量和实例变量会讲到。
既然可以给对象动态增加属性,那当然也可以给类动态增加方法,不过需要注意的是,给对象动态添加的方法,不会自动将调用者绑定到self参数,因此,需要手动将调用者绑定到第一个参数。

def say(self):
    print(self.name,self.age)
liSi.say = say
liSi.say(liSi)
#使用lambda表达式给对象动态添加方法
liSi.jump = lambda self:print("LiSi is jumping")
liSi.jump(liSi)

通过types模块下的MethodType,我们可以实现不用手动绑定self值给对象添加方法。

def run(self):
    print(self.name,"is running")
from types import MethodType
liSi.run = MethodType(run,liSi)
liSi.run()

self详解

事实上,python只是规定,无论构造方法还是实例方法,都至少要包含一个参数,将其命名为self是一种约定俗成。python的每个类可以实例化成无数的对象,self参数就是用来区分这些类,确保每个类都只调用它自己的属性和方法。这一点就和java中的this非常类似。

类属性和实例属性

在python类中,根据变量的位置,可以将变量分为三种类型,类变量、实例变量和局部变量。

类变量

在类中,各个方法之外的变量,就叫做类变量,也叫做类的属性。比如下面的Author类的name和gender,就是类变量。

class Author:
    name = "Andy"
    gender = 0

    def write(self):
        print(self.name,"is","writing")

类变量的特点是,在所有实例中都共享类变量,类变量是公共的。类变量可以通过类名调用,也可以通过实例进行调用。

print(Author.name)
a = Author()
print(a.name)

需要注意的是,因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象。

Author.name = "Bob"
print(Author.name)
print(a.name)

另外,通过对象是无法修改类变量的,通过对象对类变量进行赋值,其本质不再是修改类变量的值,而是给该对象定义新的实例变量,接下来的实例变量会详细介绍。
除了可以通过类名访问类变量之外,还可以通过类名给给整个类动态的添加类变量。

Author.nation = "China"
print(a.nation)

实例变量

实例变量指的是在类的方法内部,以self.变量名的方式定义的变量,其特点是只作用于调用方法的对象。实例变量只能通过对象名进行访问,无法通过类名访问。

class People:
    name = "ZhangSan"
    age = 18

    def __init__(self,name,age):
        self.name = name
        self.age = age 
    
    def growUp(self):
        self.age+=1

像这个People类,上面的name和age就是类变量,但是构造函数中的self.name和self.age是实例变量,也就是说,当调用该构造函数创建实例对象时,通过该实例对象调用的name和age属性已经变成了构造函数中的两个实例变量。同理,gouwUp()方法中的age操作的也是实例变量。

zhangSan = People("ZhangSan",18)
print(zhangSan.age)
liSi = People("LiSi",20)
print(liSi.age)
zhangSan.growUp()
print(zhangSan.age)
print(liSi.age)

上面说到,通过对象可以访问类变量,但是无法修改类变量的值。这是因为通过对象修改类变量的值,不是在修改类变量,而是在定义新的实例变量,例如对上面的Author类和a对象进行如下操作:

a.gender = 1
print(a.gender)
print(Author.gender)

这也是造成讲类对象的使用的时候,删除liSi对象的age属性和height属性造成的结果不同的原因。
简单而言,通过修改某个实例对象的值,既不会影响其他实例化的对象,也不会影响类中同名的变量,只会影响该对象自身的实例变量。

局部变量

局部变量就没有那么多弯弯绕了,局部变量就是只在类方法中作用的变量,通常情况下,当方法执行完毕,局部变量也会被销毁。

class Calculation:
    def add(x,y):
        s = x+y
        return s
print(Calculation.add(1,2))

如上面的Calculation类中的变量s,就是一个局部变量。

实例方法、静态方法和类方法

和属性一样,类里的方法也可以进行细分,可以分为实例方法、静态方法和类方法。

实例方法

通常情况下,在类里定义的方法默认都是实例方法,构造方法本质上也是一个实例方法。实例方法最大的特点就是它最少要包含一个self参数,用于绑定调用该函数的实例对象。
实例对象支持通过对象名直接调用,也可以通过类名进行调用,此时需要手动传入self的值。

a.write()
Author.write(a)

类方法

类方法和实例方法类似,也最少需要一个参数,不过这个参数通常命名为cls,python会自动将类本身绑定给cls参数。
和实例方法不同的是,类方法需要用@classmethod进行修饰。

class Author:
    name = "Andy"
    gender = 0

    def write(self):
        print(self.name,"is","writing")
    
    @classmethod
    def die(cls):
        print("大作家死了,并留下了一份巨著")

类方法推荐直接使用类名调用,不推荐使用实例进行调用。

Author.die()

静态方法

静态方法没有必须的参数,和全局空间的函数一样,唯一的区别就是静态方法定义在类这个空间里。也由于静态方法中没有self和cls这样的特殊参数,静态方法无法调用任何类属性和类方法。
静态方法使用@staticmethod来修饰。

class Author:
    name = "Andy"
    gender = 0

    def write(self):
        print(self.name,"is","writing")
    
    @classmethod
    def die(cls):
        print("大作家死了,并留下了一份巨著")

    @staticmethod
    def born(address):
        print("大作家出生在",address)

静态方法的调用既可以使用类名,也可以使用对象。

Author.born("北京")
a = Author()
a.born("巴黎")

描述符

python中使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。
从本质上来说,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理完全委托给描述符类。
描述符类基于__set__(self, obj, type=None),__ get__(self, obj, value)和__delete__(self, obj)三个方法,分别对应设置属性、读取属性和del属性。其中,实现了get和set方法的描述符被称为数据描述符;如果只实现了get方法,则称为非数据描述符。
实际上,在每次查找属性时,描述符协议中的方法都由对象的特殊方法__getattribute__()调用。也就是说,每次使用对象.属性,或者getattr(对象,属性值)的方式调用时,都会隐式的调用__getattribute__()方法,它会按照以下的顺序查找该属性:
1.验证该属性是否为实例对象的数据描述符
2.如果不是,就查看该属性是否能在类实例对象的__dict__中找到
3.最后,查看该属性是否为实例对象的非数据描述符

class revealAccess:
    def __init__(self, initval = None, name = 'var'):
        self.val = initval
        self.name = name
    
    def __get__(self, obj, objtype):
        print("Retrieving",self.name)
        return self.val

    def __set__(self, obj, val):
        print("updating",self.name)
        self.val = val

class MyClass:
    x = revealAccess(10,'var "x"')
    y = 5
m = MyClass()
print(m.x)
m.x = 20
m.y = 8
print(m.x)
print(m.y)

从上面的例子可以看出,如果一个类的属性有数据描述符,那么每次查找这个数据时,都会调用描述符的get方法,并返回它的值;同样,每次对该属性赋值时,也会调用描述符的set方法。而没有描述符的y属性则不会。
虽然上面的例子没有__del__() 方法,但是当每次使用del 对象.属性或者delattr(类对象,属性)时,都会调用该方法。

property()函数

前面的代码中,我们一直在用对象.属性的方式来访问类中定义的属性,其实在面向对象的语言中,这种方式破坏了类的封装原则。就像java的类变量通常都写成private一样,类的属性通常都是不可见的,只允许通过类提供的方法来对其进行操作。因此,通常我们会在类中为各个属性提供getter和setter方法用来访问类的属性。

class People:
    def __init__(self,name):
        self.name = name
    def setName(self,name):
        self.name = name
    def getName(self):
        return self.name
    def delName(self):
        self.name = None
p = People("Jack")
print(p.getName())
p.setName("Rose")
print(p.getName())
p.delName()
print(p.getName())

python中提供了一个property()方法,可以用于在不破坏类的封装性的前提下,继续使用对象.属性的方式访问类中的属性。
property() 函数的完整语法为:属性名=property(fget=None, fset=None, fdel=None, doc=None),其中fget指用于获取该属性值的方法,fset指用来设置属性值的方法,fdel指用来删除属性值的方法,doc是一个文档字符串,用来说明此函数的作用。
比如刚才的People类我们可以改成:

class People:
    def __init__(self,n):
        self.__name = n
    def setName(self,n):
        self.__name = n
    def getName(self):
        return self.__name
    def delName(self):
        self.__name = None

    name = property(getName, setName, delName, '对name属性进行操作')
help(People.name)
p = People("Jack")
p.name = "Rose"
print(p.name)
del p.name
print(p.name)

需要注意的是,由于getName()方法中需要返回name属性,如果使用self.name的话,又回去调用getName(),形成死循环。为了避免这种情况出现,程序中的name属性必须设置为私有属性,即__name。这个我还没学到,后面学到了再详细说。

@property装饰器

python中内置了三种装饰器,前面我们说到了两种@classmethod和@staticmethod,现在来说说最后一种@property。例如上面的People类,我们可以写成这样。

class People:
    def __init__(self,n):
        self.__name = n
    @property
    def name(self):
        return self.__name
    @name.setter
    def name(self,n):
        self.__name = n
    @name.deleter
    def name(self):
        self.__name = None
p = People("Jack")
p.name = "Rose"
print(p.name)
del p.name
print(p.name)

可以看出使用@property装饰器和使用property()方法创建委托没有什么区别,需要注意的就是@property、@name.setter、@name.deleter 装饰的方法的方法名都要相同,这个方法名就相当于托管属性的名称。

封装

简单的理解的话,封装就是在设计类的时候,将一些属性和方法隐藏在类的内部,这样在使用类的时候,将无法使用对象名来直接调用这些属性或方法,只能使用为隐藏的类方法间接操作这些隐藏的属性和方法。
和java不同的是,python中没有public和private修饰符,所以python中采用了如下的方法:
1.默认情况下,python中类的变量和方法的名称前都没有下划线,这种变量和方法都是公有的。
2.如果变量和方法的名称以__开头,则该变量或方法为私有。
除了上面两种情况外,还可以定义_开头的变量或方法,这种命名方式虽然可以通过对象名正常访问,但是一般我们约定将其视为私有的类和方法。

class People:
    def __init__(self,n):
        self.__name = n
    @property
    def name(self):
        return self.__name
    @name.setter
    def name(self,n):
        self.__name = n
    @name.deleter
    def name(self):
        self.__name = None
    
    def __run(self):
        print(self.name,"is running")
    def run(self):
        self.__run()
p = People("Andy")
p.run()

继承

python中的继承,只需要在定义子类时,将父类放在子类后面的括号里即可。

class Man(People):
    def pee(self):
        print("Gundam,standing on the ground.")
m = Man("Jack")
print(m.name)
m.pee()

可以看到Man类中既有People类中的name属性,又有自己的pee()方法。
python中的类可以继承多个父类。这种情况下就会面临多个类中有同名的属性或方法的情况,python中的解决方案是,根据子类继承父类时这些父类的前后顺序,排在前面的父类将会覆盖后面所有父类里同名的属性和方法。

父类方法重写

python中虽然没有方法重载,但是可以对父类的方法进行重写。

class Man(People):
    def run(self):
        self.__run()
    def __run(self):
        print(self.name,"跑不动了")

    def pee(self):
        print("Gundam,standing on the ground.")
m = Man("Jack")
m.run()

super()方法

前面我们提到,python中的子类可以继承多个父类,那么当继承多个父类的时候,构造方法也会被第一个父类给覆盖,这就会导致有一些变量在声明对象时无法赋值。为了解决这个问题,需要重写子类的构造方法,这种时候,可以用到super()方法。
super()函数可以用来调用父类的构造方法,但是当子类继承了多个父类时,只能调用第一个父类的构造方法。

class Transport:
    def __init__(self,speed):
        self.__speed = speed
    
    def showSpeed(self):
        print("速度:",self.__speed)

class Car:
    def __init__(self,way):
        self.__way = way
    
    def showWay(self):
        print("方式:",self.__way)

class Benz(Transport,Car):
    def __init__(self, speed,way):
        #相当于Transport.__init__(self,speed)
        super().__init__(speed)
        #调用其他父类的构造方法,需要手动绑定self
        Car.__init__(self,way)

b = Benz(80,"跑")
b.showSpeed()
b.showWay()

__ slots__

上文中,我们说到了动态的为类和对象添加属性。在python中,也可以动态的为类和对象添加方法,类可以动态的添加实例方法、静态方法和类方法,但是对于对象,只能动态添加实例方法。

class TestClass():
    pass

def method1(self):
    print("实例方法")
@classmethod
def method2(cls):
    print("类方法")
@staticmethod
def method3():
    print("静态方法")
#类可以动态添加三种方法,并影响所有实例对象
TestClass.method1 = method1
TestClass.method2 = method2
TestClass.method3 = method3

t = TestClass()
t.method1()
t.method2()
t.method3()
#实例对象只能动态添加实例方法,不会影响其他实例对象
t2 = TestClass()
t2.method1 = method1
#需要手动传入self
t2.method1(t2)

但是如果频繁地动态添加属性或方法,可能会存在一定的隐患,因此,python提供了__slots__属性。这个属性实际上是一个元组,只有其中指定的元素,才可以作为动态添加的属性和方法的名称。
需要注意的是,__slots__属性限制的是类的实例对象,因此直接给类动态添加属性和方法是不受这个属性限制的。还有就是由该类派生出的子类,也是不受限制的,如果子类也要进行限制,则需要给子类也加上__slots__属性,此时子类的__slots__值为子类和父类的值的和。

class TestClass:
    __slots__ = ("m1","m2")

def m1(self):
    print("m1")
t = TestClass()
t.m1 = m1
t.m1(t)

type()

type()函数是python的内置函数,可以用来查看变量的具体类型。除此之外,type()函数还有一种更高级的用法,即动态创建一个类。type()函数的语法有两种:
1.type(obj),用来查看某个变量的具体类型。
2.type(name, bases, dict),用来创建一个类,name表示类的名称;bases是一个元组,表示该类的父类;dict是一个字典,表示类中的属性和方法。这里我们重点介绍一下这种用法。

def m1(self):
    print("m1:",self.__name)
TestClass = type("TestClass",(object,),dict(__name = "m1",m1 = m1))
t = TestClass()
t.m1()

可以看到,使用type()函数创建的类,和直接使用class定义的类并没有区别,实际上,在使用class定义类的时候,python解释器的底层也是使用type()函数来创建这个类的。

MetaClass元类

MetaClass元类的本质也是一个类,但是和普通类的用法不同,它可以对类内部的定义(包括属性和方法)进行动态的修改。使用元类的主要目的就是为了实现在创建类时,能够动态的改变类中的属性和方法。
举个例子,如果在开发过程中,我们需要为多个类添加一个name属性和一个say()方法,逐个添加是比较麻烦的,这种时候可以使用MetaClass元类。
如果想把一个类设计成元类,需要满足两个条件:
1.必须显式继承type类。
2.类中需要实现__new__()方法,该方法一定要返回一个类的实例对象,因为在使用元类创建类时,会自动执行__new__()方法,用来修改新建的类。

class FirstMetaClass(type):
    #cls代表动态修改的类
    #name代表动态修改的类名
    #bases代表被动态修改的类的所有父类
    #attrs代表被动态修改的类的属性和方法
    def __new__(cls,name,bases,attrs):
        attrs['name'] = "张三"
        attrs['say'] = lambda self:print("我叫:",self.name)
        return super().__new__(cls,name,bases,attrs)

可以看到,在我们创建的元类中,动态的添加了name属性和say()方法,通过该元类创建的类,会额外添加name属性和say()方法。

class TestClass(object,metaclass = FirstMetaClass):
    pass
t = TestClass()
print(t.name)
t.say()

按照我个人理解,元类是type类的子类,实际创建TestClass的时候,指定了FirstMetaClass类代替了原本的type类。

多态

面向对象的三个特性我们已经讲了封装和继承,现在来说一说多态。

class C1:
    def m1(self):
        print("C1")
class C2(C1):
    def m1(self):
        print("C2")
class C3(C1):
    def m1(self):
        print("C3")
c = C1()
c.m1()
c = C2()
c.m1()
c = C3()
c.m1()

从上面的例子可以看到,同一个变量a在执行m1()方法时,a表示不同的实例对象时,调用的并不是同一个m1()方法,这就是多态。多态必须满足两个前提条件:1.多态一定是发生在父类和子类之间;2.子类重写了父类的方法。
python在多态的基础上,衍生出了一种灵活的编程机制,通常被称为鸭子模型。

class whoC:
    def m1(self,who):
        who.m1()
w = whoC()
w.m1(C1())
w.m1(C2())
w.m1(C3())

我们通过创建了一个whoC类,给其传入一个who参数,其内部利用传入的who调用m1()方法,当我们调用whoC的m1()方法时,我们传入的who是什么类,就会去调用哪个类的m1()方法。

枚举类

Python中定义一个枚举类,只需要让类继承enum 模块中的 Enum 类就可以了。

from enum import Enum
class Mouth(Enum):
    January = 1
    February = 2
    March = 3

和普通的类不同,枚举类不能实例化,可以通过如下方法来调用枚举类中的变量。

#调用枚举成员
print(Mouth.January)
print(Mouth['February'])
print(Mouth(1))
#调用枚举成员的值或name
print(Mouth.February.value)
print(Mouth.February.name)
#遍历枚举成员
for m in Mouth:
    print(m)

枚举类的成员不能用来比大小,但是可以用==或者is来判断是否相等。

print(Mouth.February == Mouth.January)
print(Mouth.February.value is Mouth.January.value)

除此之外,枚举类还提供了__members__属性,该属性是一个包含了枚举类中所有成员的字典,通过遍历该属性,也可以访问枚举类中的成员。

for name,member in Mouth.__members__.items():
    print(name,"->",member)

枚举类中的成员name不能相同,但是value可以相同。如果想要避免这种情况,可以使用@unique装饰器,这样当枚举类中出现值相同的成员时,会抛出ValueError异常。

from enum import Enum,unique
@unique
class Mouth(Enum):
    January = 1
    February = 2
    March = 3

除了可以通过继承Enum的方式来创建枚举类,还可以使用Enum()函数创建枚举类。

Mouth = Enum("Mouth",('January','February','March'))

这样创建的枚举类value会从1开始递增,所以这个枚举类和上面的创建方法是一样的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值