Python入门--下

第八章 Python类和对象

Python在设计之初就是一门面向对象的语言。同时,Python也支持面向对象的三大特征:封装、继承和多态。

基础概念

面向对象编程是一种代码封装的方法。代码封装其实就是隐藏实现功能的代码,仅留给用户使用接口。

创建类使用class关键字即可创建。

 class Person:
     weight = "60kg"
     height = "180cm"

     def character(self):
         print("thin")

 nxl = Person()

这段代码涉及到的术语有:

  • 类:读者可以理解为创建了一个模板,根据这个模板可以创建出很多实例。在这段代码里就体现为满足身高体重和character的一类人
  • 对象:类不能直接使用,需要创建对象才可以使用。在这段代码里就体现为最后一行,有一个名为nxl的人满足Person这个类的人
  • 属性:类中所有的变量都成为属性,属性又分为类属性和实例属性。这里的weight和height都是类属性
  • 方法:类中所有的函数都称为方法。不过和之前说的函数不一样的是,这里的函数至少要一个self参数(后文会详细介绍)

除了上述提到的,还有一些常用术语名词:数据成员:类变量或者实例变量用于处理类及其实例对象的相关数据方法重写:如果从父类继承的方法不能满足子类的需求,就可以对其进行改写。多态:对不同类的对象使用同样的操作封装:对外部世界隐藏对象的工作细节继承:即一个派生类继承基类的字段和方法。继承允许把一个派生类的对象作为一个基类对象对待,以普通类为基础建立专门的类对象

定义类

定义类的方法很简单使用class关键字即可。

 class Person:
     weight = "60kg"
     height = "180cm"

     def character(self):
         print("thin")

这里就创建了一个名为Person的类。类里定义了一个叫做character的类方法,更具体地说,它是一个实例方法。

Python允许创建空类:

 class Empty:
     pass

构造方法

__init__(self, ...)称为构造方法,又称构造函数,在创建类时可以手动添加。

 class Person:
     weight = "60kg"
     height = "180cm"

     def __init__(self):
         print("zyh")

     def character(self):
         print("thin")

其实__init__()可以有包含多个参数,但是必须有self参数,而且一定要放在第一位。在Python的语法体系里,就算开发者不手动添加构造方法,Python也会自动添加。

 class class Person:
 
     def __init__(self, name, gender):
         print("name:%s, gender:%s" % (name, gender))
 
 
 nxl = Person("zyh", "male")

在Line 7可以看到在实例化的时候只给了两个参数,说明self参数不需要手动传参。

有关于self参数的具体内容会在后续章节做出介绍。

类对象创建和使用

创建类对象的过程称为类的实例化。

 class Person:
 
     def __init__(self, name, gender):
         print("name:%s, gender:%s" % (name, gender))
 
     @staticmethod
     def character():
         print("good")
 
 
 nxl = Person("zyh", "male") # 创建了叫做nxl的类对象
 nxl.character() # 调用了类对象里的character的实例方法

实例化类对象后可以进行以下操作:

  • 访问或者修改类对象具有的实例变量,甚至添加或者删除
  • 调用类对象的方法,包括现有的方法,以及动态添加

增删改查实例变量

 class Person:
 
     def __init__(self, name, gender):
         print("name:%s, gender:%s" % (name, gender))
 
     @staticmethod
     def character():
         print("good")
 
 
 nxl = Person("zyh","male")
 
 nxl.weight = "60kg"
 print(nxl.weight)

以上这个程序就是为nxl类对象添加属性,若还有其他对象,就不支持对该属性进行访问。如果想要支持其他变量也可以访问此属性,那么只需将此属性在类里增加即可。

 class Person:
 
     def __init__(self, name, gender):
         print("name:%s, gender:%s" % (name, gender))
 
     @staticmethod
     def character():
         print("good")
 
 
 Person.weight = "60kg"
 
 nxl = Person("zyh","male")
 pepsi = Person("zyh","male")
 
 print(nxl.weight,pepsi.weight)

删除属性使用del关键字即可,请读者自行尝试。

动态给类对象添加方法

在学习这个操作之前,建议读者先阅读下一节,了解self参数。C语言中文网对这个知识点的讲解不是很好,笔者结合自己的理解进行改写 动态给类添加方法

class Person:

    def __init__(self, name, gender):
        print("name:%s, gender:%s" % (name, gender))

    @staticmethod
    def character():
        print("good")

def sleep():
    print("sleep")

nxy = Person("zyh","male")

Person.sleep = sleep
Person.sleep()

动态给对象添加方法

import types

class Person:

    def __init__(self, name, gender):
        print("name:%s, gender:%s" % (name, gender))

    @staticmethod
    def character():
        print("good")

def sleep(self):
    print("sleep")

nxl = Person("zyh", "male")

nxl.sleep = types.MethodType(sleep, nxl)
nxl.sleep()

self参数

我们已经明确无论是显式创建构造方法,还是向类中添加实例方法,都要求有self参数,并且放在首位。事实上,Python只是规定,无论是构造方法还是实例方法,最少要包含一个参数,但是并没有规定参数的名称。之所以叫做self,只是一种约定俗成。

关于self参数的具体作用,可以这样理解。一个类是以创建成千上万个类对象,每个类对象都有相同的类变量和类方法,self参数就可以保证每个类对象只可以调用自己的类变量和类方法。

这个其实很像C++的this指针

正式点说就是,同一个类产生多个对象,某个对象调用类方法时,该对象会把自身的引用作为第一个参数(self)自动传递给该方法。Python会自动绑定类方法的第一个参数指向指向调用该方法的对象,这样Python就知道到底要操作哪一个对象的方法了。

self其实还有别的注意点,C语言中文网没有提及,下面进行补充:

class Test(object):
    def __init__ (self, val1):
        self.val0 = val1
    def fun1(self):
        print(self.val0)
    def fun2(self, val2):
        print(val2)
    def fun3(self):
        print(self.fun1)
        self.fun1()

ins=Test(123)
ins.new_val=”I’m a new value”
  1. 在类中,引用实例的属性。示例:self.变量名(如self.val0)。 引用实例的属性的目的是为实例绑定属性、写入或读取实例的属性。 例如,在上段代码中,在类的函数__init__中,self.val0 = val1将属性val0绑定到了实例self(类实例化成ins后,self就代表实例ins了)上,并且将变量val1的值赋给了实例的属性val0。在函数fun1中,print(self.val0),读取了实例self的值val0,并打印出来,当然,在函数中修改属性val0的值也是可以的。

    在补充一点:self.变量,这个变量可以供类中所有的方法使用

    class Test(object): 
        def __init__(self, val1):     
            self.val0 = val1 
        def fun1(self):     
            print(self.val0) 
        def fun2(self, val2):     
            print(val2) 
        def fun3(self):     
            print(self.fun1)     
            self.fun1()
            
    ins = Test(123)
    ins.fun1()
    *********************
    123
    
  2. 在类中,调用实例的方法,例如,self.fun1();获取实例的方法的地址,例如self.fun1。(注意有无括号) 类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。既然,self代表实例,则可以“self.函数名”的方式表示实例的方法地址,以self.函数名()的方式,调用实例的方法。在类的定义中,以及实例化后对实例方法的调用,都可以这样做。

  3. self和变量的关系

    在后文会详述,这里有个概念即可:全局变量:在模块内,在所有函数外,在class外局部变量:在函数内、在class的方法内(未被self修饰)静态变量(类属性):在class内,但不在class方法内实例变量(实例属性):在class的方法内,用self修饰的变量

什么时候应该使用self修饰变量比较好?我的总结如下:

当我们想将某个变量绑定给实例时,就在类中,使用self修饰该变量。一般来说,类实例化为不同实例后,为了不同实例的某一变量互不干扰,就将该变量绑定给实例。

具体的使用场景:

  1. 若需要在类的不同方法中调用同一变量,且属于同一个类的不同实例的该变量互不影响(即排除类属性),则在类中将该变量绑定给实例。
  2. 需要在类实例化得到实例后,修改、或引用实例的某变量,则在类中将该变量绑定给实例。

类变量和实例变量

类变量

class person:
    name = "zyh"
    gender = "male"

    def __init__(self):
        self.age = 22

这段代码里的name和gender就是类变量。类变量是所有类的实例化对象共有的资源。类的调用方法有两种:

  1. 使用类名调用

    class person:
        name = "zyh"
        gender = "male"
    
        def __init__(self):
            self.age = 22
    
    print(person.name)
    ******************
    zyh
    

    使用类名调用对象可以改变类变量的值。

    class person:
        name = "zyh"
        gender = "male"
    
        def __init__(self):
            self.age = 22
    
    person.name = "nxl"
    print(person.name)
    ************************
    nxl
    

    通过类名修改类变量会做用到全部实例化对象

  2. 使用类的实例化对象调用

    这种通过类的实例化对象调用类变量的方法不推荐通过这种方法不是修改类变量,而是相当于是在定义新的实例变量

    class person:
        name = "zyh"
        gender = "male"
    
        def __init__(self):
            self.age = 22
    
    nxl = person()
    print(nxl.name)
    *****************
    zyh
    

结合前面章节说的动态给类对象增删改查变量

实例变量

实例变量只作用于调用方法的实例对象。实例变量只能通过对象名访问,不能通过类名访问。

class persom:

    def __init__(self):
        self.name = "zyh"
        self.gender = "male"

    def charachter(self):
        self.height = "180cm"
nxl = person()
print(nxl.name)

构造方法Python会自动调用,所以不需要手动调用。而character就需要手动调用,如果不调用,直接print(nxl.height)就会报错

前文说过,类对象可以访问类变量,但是不可以修改类变量。即使这样操作了,实质是再定义新的实例变量。使用代码解释。

class person:
    age = 22

    def __init__(self):
        self.name = "zyh"
        self.gender = "male"

    def charachter(self):
        self.height = "180cm"

nxl = person()
nxl.age = 23
print(nxl.age)
print(person.age)
**********************
23
22

由结果可以看出,使用类对象是无法改变类变量的,本质上是添加了名为age的实例变量。

在类中,实例变量和类变量可以同名,但是此时使用类对象无法调用类变量,Python会首选实例变量。这就是为啥不推荐使用类变量调用类对象。不仅如此,python只支持为特定的类对象添加实例变量。换句话说,给谁添的,只能给谁用。

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

区分3种类方法是十分简单的:

  1. 类方法:@classmethod
  2. 静态方法:@staticmethod
  3. 实例方法:没有任何修饰

实例方法

通常情况下,类中定义的方法都是实例方法。理论上,构造方法也是实例方法的一种。

实例方法最大的特点是必须包含一个self参数,用于绑定此方法的实例对象(Python自动完成)。

调用实例方法的方法有两种:

  1. 使用类对象直接调用

    class person():
    
        def __init__(self) -> None:
            self.name = "zyh"
            self.age = "22"
    
        def say(self):
            print("Chinese")
    
    nxl = person()
    nxl.say()
    ******************
    Chinese
    
  2. 使用类名调用,但是要手动传参

    class person():
    
        def __init__(self) -> None:
            self.name = "zyh"
            self.age = "22"
    
        def say(self):
            print("Chinese")
    
    nxl = person()
    person.say(nxl)
    *********************
    Chinese
    

类方法

类方法和实例方法相同点是:都最少必须包含一个参数;不同的是:类方法通常是将其命名为cls。Python会自动将类本身绑定给self参数(注意:绑定的不是类对象)也就是说,在调用类方法的时候,无需显式为cls传参。

类方法的调用方法也有两种,但是强烈建议使用类名直接调用

class person():

    def __init__(self) -> None:
        self.name = "zyh"
        self.age = "22"

    @classmethod
    def say(cls):
        print("Chinese")

nxl = person()
nxl.say() # 使用类对象调用,不推荐
peron.say() # 使用类名调用

静态方法

静态方法就是定义在类空间里的函数。静态方法没有类似self和cls的参数,因此Python不会对它包含的参数做任何类和对象的绑定。正因此,类的静态方法无法调用任何类属性和类方法。

class person():

    @staticmethod
    def say_hi():
        print('hi')

nxl = person()
nxl.say_hi()
person.say_hi()

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

调用类的实例方法

前文说过,实例方法可以通过类名调用也可以使用实例化的类对象使用。通常情况下,更加常用的是使用类对象直接调用。使用类名调用的时候,python不会自动传参。

class person():

    def say_hi(self):
        print('hi')

nxl = person()
person.say_hi(nxl)

虽然需要手动传参,但是并没有要求传递的参数必须是类名。

class person():

    def say_hi(self):
        print('hi',self)

person.say_hi('zyh')
*************************
hi zyh

用类的实例对象访问类成员的方式称为绑定方法,而用类名调用类成员的方式称为非绑定方法。

描述符

通过使用描述符,可以让开发者在引用一个对象属性时自定义要完成的工作。

本质上,描述符是一个类,只不过它定义了另一个类中属性的访问方式。也就是说,一个雷可以将属性全权委托给描述符类。

描述符在python中是复杂属性访问的基础,它在内部被用于实现property、方法、类方法、静态方法和super类型

描述符基于以下3个特殊方法。

  1. __set__(self, obj, type = None):在设置属性时将调用这一方法(后文中用setter表示)
  2. __**get**(self, obj, value)__:在读取属性时调用这一方法(后文中使用getter表示)
  3. __**delete__**(self, obj):对属性调用del时将调用这一方法

实现了setter和getter方法的描述符类称为数据描述符;反之只实现getter方法的称为非数据描述符。

实际上,在每次查找属性的时候,描述符协议中的方法都由类对象的特殊方法__getattribute__()调用。也就是说,每次使用类对象.属性(或者getattr(类对象,属性值))的调用方式时,都会隐式的调用__getattribute__(),会按照以下顺序查找属性:

  1. 验证该属性是否是类实例对象的数据描述符
  2. 如果不是,就查看该属性是都能在类实例对象__dict__中找到
  3. 最后,查看该属性是否为类实例对象的非数据描述符
class realAccess:
    def __init__(self, initval=None, name="var"):
        self.val = initval
        self.name = name

    def __get__(self,obj,objtype):
        print("Getting:", self.name)
        return self.val # 没有return语句只会返回None

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

class myClass:
    x = realAccess(10, "var'x'")
    y = 5

m = myClass()
print(m.x)
m.x = 20
print(m.x)
print(m.y)
*****************
Getting: var'x'
10
updateing var'x'
Getting: var'x'
20
5

可以看出,如果一个类的某个属性有数据描述符的时候,那么每次查找查找这个属性时,都会调用描述符的__get__()方法,并返回它的值;同样,每次在对该属性赋值时会调用__set__()方法。

疑问:

  1. 前面说过类变量只能够通过类名修改,使用实例对象修改实质上是创建新的实例变量
class demo():

    def __init__(self,initval = None, name = "var"):
        self.val = initval
        self.name = name

    def __get__(self,obj,objtype):
        print("Getting",self.name)
        print("self.val:",self.val)
        return self.val

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

class test:
    x = demo(10,'var x')
    y = 20

t = test()
# print(t.x)
t.x = 100
print(t.x)
print("test.x:",test.x)
************************
Updating var x
self.val: 100
Getting var x
self.val: 100
100
Getting var x
self.val: 100
test.x: 100

从结果来看并不是

  1. t.x = 100这样赋值的操作,从代码运行结果来看,是将demo(10,"var x")里的10换成了100.但是如果使用t.x = demo(100, "var x")的话,运行结果就是:
Updating var x
self.val: <__main__.demo object at 0x000001AF0AD82880>
Getting var x
self.val: <__main__.demo object at 0x000001AF0AD82880>
<__main__.demo object at 0x000001AF0AD82880>

有一说一,C语言中文网在这一小节的内容说的很水。

描述符进阶

class Age:

    def __init__(self, value=20):
        self.value = value

    def __get__(self, obj, type=None):
        print('call __get__: obj: %s type: %s' % (obj, type))
        return self.value

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("age must be greater than 0")
        print('call __set__: obj: %s value: %s' % (obj, value))
        self.value = value

class Person:

    age = Age()

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

p1 = Person('zhangsan')
print(p1.age)
# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class '__main__.Person'>
# 20

print(Person.age)
# call __get__: obj: None type: <class '__main__.Person'>
# 20

p1.age = 25
# call __set__: obj: <__main__.Person object at 0x1055509e8> value: 25

print(p1.age)
# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class '__main__.Person'>
# 25

# p1.age = -1
# ValueError: age must be greater than 0

可以看出,

  • 在调用p1.age时,obj是Peron实例,typetype(Person)
  • 在调用Person.age时,参数objNonetypetype(Person)
  • 在调用p1 = 22 时。__set__被调用,参数objPersonvalue是22

描述符的工作原理

在开发过程中,通常会写到这样的代码a.b,这个背后到底发生了什么?这里a和b可能存在以下情况:

  • a可能是一个类,也可能是一个实例,我们这里统称为对象
  • b可能是一个属性,也可能是是一个方法,都由统一的调用逻辑

其实无论什么情况,Python雨大都会遵循一个统一的调用逻辑:

  1. 先调用__geattribute__()尝试获得结果
  2. 如果没有,调用__getattr__

用代码表示就是:

def getattr_hook(obj,name):
    try:
        return obj.__getattribute__(name)
    except AttributeError:
        if not hasatte(type(obj),'__getattr__'):
            raise
    return type(obj).__getattr__(obj,name)

重点关注__getattributr__,因为他是所有属性的查找入口,它内部的实现顺序是这样的:

  1. 要查找的属性是否是是一个描述符;
  2. 如果是描述符,再检查是否是一个数据描述符;
  3. 如果是数据描述符,则调用数据描述符的__get__
  4. 如果不是数据描述符,则从__dict__中查找
  5. 如果在__dict__中查找不到,再看是否是一个非数据描述符;
  6. 如果是非数据描述符,则调用非数据描述符的__get__;
  7. 如果不是非数据描述符,则从类属性中查找;
  8. 如果类属性中还没有,则抛出AttributrError

结合实例理解:

class A:
    def __init__(self) -> None:
        self.foo = "abc"
        
    def foo(self) -> None:
        return "xyz"
    
print(A().foo)

这里定义了一个属性和方法相同名称的foo,请问输出的结果是什么?

答案是abc

为什么是打印实例属性的foo值呢,折旧和非数据描述符有点关系了。执行语句dir(A.foo)后输出结果里有'__get__', '__getattribute__',说明是实现了getter,而且只实现了getter的称为非数据描述符。也就是说,在类中定义的方法,其实本身就是一个非数据描述符

所以在一个在类中,如果存在相同名字的属性和方法,按照调用的顺序,这个属性优先会从实例中获取,如果实例不存在,才会从非数据描述符中获取。

刚学会有点绕,结合调用顺序多看几遍就好。

总计一下:

  • 描述符必须是一个类属性;
  • __getattribute__是查找一个属性(方法)的入口;
  • __getattribute__调用的顺序是:数据描述符、实例属性、非数据描述符、类属性
  • 如果重写了__getattribute__,会阻止描述符的调用;
  • 所有方法都是一个非数据描述符,因为它定义了__get__
posted @   结了冰的可乐  阅读(13)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示