剑客
关注科技互联网

第三期 · 形象的理解Class的init、self 、@staticmethod、 @classmethod、@property

@staticmethod、@classmethod、@property这几个带@符号的语法是Python里的「语法糖」。这些语法中文常译作「装饰器」,对于没有任何编程经验的新手,这些都是经常难以理解的地方。再有就是类的self参数,新手通常留于表面的认识造成一些误会。

需要说明一下,本教程针对Python3。

要理解这几样,我们先要对Python类有一个认识,让我们先简单回顾一下「类」。

class 类

我们学习编程语言时,了解到语言有面向过程和面向对象之分,可以面向对象的语言一般都是「高级语言」,为什么高级?因为使用更容易,理解更容易,当然这一切都有代价,但那是另一件事情了,我们有机会在说。

「在Python中一切皆对象」这句话估计你也不是头一回听到了。Python中对象最直接的化身便是class语法,中文称之为「类」,让我们基于Python造「一批」Girlfriend(对象)吧。注意:这里我强调了「一批」这个词。

    # 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

    # 生产几个女朋友
    lili = Girlfriend()
    mimi = Girlfriend()

以上,我们做了这几件事情:

1)我们定义了一个Girlfriend类,用来一会生产女朋友当相好的对象;

2)基于我低俗的品味,女朋友要定义几个属性:年纪要22岁,罩杯要C,为了谨防意外,性别定义清楚是女,免得造出人妖;

3)基于我的承受能力,我实例化了lili和mimi两个女朋友,两个就够了。

通过上述例子,我们回顾了类与实例的关系:类就好比生产具体产品的「模具」或者「模子」,先做好模具,定义好将来生产的产品期望的样子,然后再用实例化语法通过模具生产一些产品。

我可以随时通过「实例 . 属性」这个语法知道女朋友的一些属性值,比如:

    lili.age  # 会得到22这个值
    mimi.age  # 也是22
    lili.size  # C杯

显然,我的两个女朋友年纪、罩杯都是一致的,因为我在定义模具(类)的时候就明确指定了这些属性。

class的init方法和self参数

什么都一样没意思,我希望至少每个妞的发型不要一样,并且我能指定,想要什么发型就造出来什么发型的女朋友。注意,我的需求有一个细节:女朋友的头发是在女朋友造出来后就已经有的。要想实现这个需求细节,意味着必须在生产女朋友的过程中就完成发型的构建。如果你还有印象,应该依稀还记得Python有一个内置的配合class用的方法init。

init的作用很容易理解,就是每次实例化一个类的时候,会默认自动调用执行这个方法,完成这个方法里定义的行为。这正好符合我们上面的需求,即在生产过程中,加一个发型工艺环节。现在我们改造一下模具和生产工艺:

    # 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'
        def __init__(self, hair):
            self.hair = hair
    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')

我们来检验一下结果:

    lili.age  # 会得到22这个值
    mimi.age  # 也是22
    lili.hair  # 结果是'乌黑长发'
    mimi.hair  # 结果是'金黄短发'

非常好,两个妞发型已经不一样了。

我们已然知道init的作用:在实例化一个类的时候,会默认自动调用这个方法执行。这就是说,我们不必明显的告诉Python去执行这个方法,在运行下面这行代码时,Python已经悄悄自己执行这个方法了:

 lili = Girlfriend('乌黑长发')

需要明显的告诉程序去干什么事情,有一个针对的术语叫「显式」;相对应的,不需要明显的告诉程序去干事情,程序会按照既定的规则自己办好事情叫「隐式」。在以后的文章中,我们将会再看见这些词,请大家理解和记住意思。

显然,init方法被隐式调用了。我们只在定义class时定义了这个方法,但后续并没有显式的调用他,但他确实执行了,帮我的女朋友做好了发型。如前所述,隐式调用是程序按照「既定」的规则办事,既定的意思就是事先都定下来了怎么做,既然如此,就不能随意变更事先定下的细节。因此,init方法的在名称上很特别,init前后各有两根下划线,这种加下划线的方法名你现在只需记得其目的就是不要让你随便改动。所以,init的方法名不能有任何改动,少根下划线都不行。如果改动,则Python不会把改动后的方法当成init方法去隐式调用了。

让我们再来看看self这个参数。当我们在定义类时,显然无法确定后续会造多少个女朋友,也无法确定女朋友的名字,更无法确定她们的发型。所以我们不能再用定义年纪、性别、罩杯类似办法去定义发型,否则只会导致发型千篇一律。基于此,我们用了init方法,在生产过程中完成发型工艺,并在init方法里定义接收两个参数:self和hair,当我们生产女朋友时:

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')

对于init工艺,其实我们是这样告诉程序的:

lili和mimi年纪、性别、罩杯都一样

lili的hair是’乌黑长发’

mimi的hair是’金黄短发’

对于「lili的hair是’乌黑长发’」这句话,本质上是在说明「谁的头发是什么发型?」。所以有两层意思:1)谁的头发?2)头发的发型是什么?,两层意思组合在一起才是有意义的,完整的意思表达,缺一不可。我们套上程序来看,显然程序刚好也要求传入两个参数。参数hair对应「头发的发型是什么?」再显然不过,那么剩下的self应该就是对应「谁的?」了。但是「谁的」在英文里不是「whose」吗?所以这样行不行:

    # 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

        def __init__(whose, hair):
            whose.hair = hair

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')

即self参数更名为whose,行不行?行!

举上述的这个例子,是要明确的告诉你self这个参数名并不是Python的关键字,没有不能更名的限制,然而由于全世界程序员的习惯,约定俗成总是用self这个英文单词,所以你在Python代码中经常看见他。而在Python中,「谁的?」这个信息通常就是实例本身。实例就是造出来的女朋友,女朋友是「对象」,所以实例也是对象。既然self是用来接收实例,那么self最后也是对象,综上,形成了你以前也许已经看到或了解的几个概念:

1)self的作用是用来接收实例的,用来形成实例的命名空间(这涉及Python里「命名空间」的概念,你要有一个初步的认识,否则这句话看不懂。命名空间的概念以后会讲到)。

2)self类似于中文里的「你、我、他、她、它」的概念,在中文中「你、我、他、她、它」指代了具体的人或物,在Python中self则指代了具体的实例,实例就是「lili = Girlfriend(‘乌黑长发’)」这段代码等于号左边的那个具体的东西。指代在中文里也可以换种说法:「引用」。

3)既然self其实就是指代了具体的实例,那么self也将是对象。

现在,你应该对class的init和self的应用场景以及含义有了进一步的认识。

@staticmethod

让我们接着再对模具做一个微小的改进:

  # 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

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

        def about_me(self):
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(self.age, self.sex, self.size, self.hair))

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')

上面微小的改进中,我们增加了一个名为「about_me()」方法,顾名思义,通过这个方法两位美女能来一小段自我介绍,我们尝试一下:

    lili.about_me()  # 结果:年芳22,性别女,身材C棒棒哒,一头乌黑长发
    mimi.about_me()  # 结果:年芳22,性别女,身材C棒棒哒,一头金黄短发

真好,我们不用再逐个查询美女的属性了。通过上面的这个例子,美女会通过自我介绍直接告诉我她的全部信息。format方法是一个在Python3中出现的基础方法,用来格式化字符串,任何Python3的基本教程里都会谈及这个方法,这里不再赘述。

在你用PyCharm或其他比较智能的编辑器写about_me这个方法时,编辑器会自动往括号里写一个self参数。我十分好奇去掉会怎么样,那就去掉试试:

    # 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

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

        def about_me():
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(age, sex, size, hair))

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')

我们最新的模具里,将about_me方法中原来的参数self去掉了。而运行生产过程时,居然没有任何报错,可以运行。使用类似lili.age的属性查询也正常,接着我们试试lili.about_me(),程序报错:

Traceback (most recent call last):

File "<input>", line 1, in <module>

TypeError: about_me() takes 0 positional arguments but 1 was given

报错信息很好理解,意思是about_me()方法不接受任何参数,但我们试图要传入一个。这就奇怪了,我们刚才还有之前运行about_me() 方法时,都没有传入任何参数。确实,我们的确没有在about_me() 的括号里显式的传入任何参数,但前面我们已经知道程序会隐式的干一些事情。没错,答案就是每次运行一个实例方法时,程序会自动将实例作为第一个参数传入到方法,这是程序在「硬肛」方法,而显然我们把方法原来的唯一的「入口」self参数给去掉了,没了「入口」,程序「硬肛」折了,所以报错埋怨我们了。再来看about_me方法里,我们在format里亦传入了age, sex, size, hair这四个变量。format方法是在about_me里运行的,基于我们已经了解的基本语法原则,显然这有问题。因为方法里运行的没有定义赋值的变量,都要靠方法本身传入的参数去一一对应,这便是方法参数存在的意义。所以我们恢复原样,先对about_me方法要求传入一个参数用来接应实例,先解决程序要「硬肛」的需求。文章之前讲self时,我们已经知道接收实例的参数约定俗称用self这个单词,所以我们也用这个。那又冒出一个问题:既然format要用到四个参数,为什么我们只传入了一个?答案也许你有了。没错,self传入的是一整个实例对象,或者说是一整个美女,所以美女的年龄,美女的性别,美女的头发都可以知晓了,只需要用我们熟悉的「美女.属性」就可以得到,而结合之前谈到的指代关系,那么顺理成章就是用「self.age」这样的方式就能获取到所需的属性了,因而我们只需要self一个参数即可。

对恢复原样后的模具,我们再加以改进,一样,「做一点微小的工作」:

# 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

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

        def about_me(self):
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(self.age, self.sex, self.size, self.hair))

        def bio():
            print('全心全意为主人服务!')

为了节省一点篇幅,我就不重复「生产几个女朋友」的过程了。让我们看上面的这个模具,我们又增加了一个名为bio的方法,顾名思义,就是让造出来的美女口头禅就是这个。有了前面的经验,我们可以猜到bio这个方法肯定会出问题。因为程序还会没节操的「硬肛」,所以肯定会报错。但bio这个方法本身就是打印一句口头禅,不需要任何参数参与,总不能脱了裤子放屁,为了能正常运行在不需要的情况下也开个入口(设置一个参数)吧?

非常好的是,Python早已贴心的为我们应对这种场景提供了手段,是时候请出本节的主角@staticmethod了,让我们稍加改进一下方法:

# 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

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

        def about_me(self):
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(self.age, self.sex, self.size, self.hair))

        @staticmethod    
        def bio():
            print('全心全意为主人服务!')

真是微小的工作,我们在bio方法上面一行增加了@staticmethod。先别管太多,让我们运行一下lili.bio()看看:

 lili.bio()  # 结果:全心全意为主人服务!

结果正常!现在再抱着钻研的心态,对bio方法增加一个self参数,我们再运行一把看看。结果报错:

Traceback (most recent call last):

File "<input>", line 1, in <module>

TypeError: bio() missing 1 required positional argument: ‘self’

意思也很好理解,这回程序埋怨的是「我不想肛了你非要弄个入口让我肛」,即方法要求传入一个参数,但程序没有任何参数传入。

所以@staticmethod的应用场景和作用也显而易见了。一旦对一个方法应用@staticmethod,则在实例调用这个方法时,不会再试图传入实例本身。这样很容易联想到@staticmethod就是用在那些不需要实例直接参与的方法上。那什么样的类方法不需要实例直接参与呢?最常见的莫过于两种:一种是我们上面举的例子,即实例通用的方法。另一种是供实例方法调用的方法,比如类里有方法一,方法二,方法三,方法二和方法三需要借助方法一运算出一个结果,这时会在方法二、方法三的代码里调用方法一,方法一的作用只在接收两个参数然后返回一个运算结果,不需要传入实例,这时可以在方法一上写@staticmethod,而且这种例子你可以猜到方法一应该是算一个(仅供内部调用的)特殊方法,并不希望被外部随意调用,所以这种方法的函数名通常会在前面加一根下划线。

在谈到@staticmethod时举的例子其实相当无聊,上面说的方法一、二、三的例子其场景是很少发生的,所以@staticmethod的出镜率相对而言其实很低,很少被用到。

@classmethod

当我们理解@staticmethod的场景和作用后,理解@classmethod也将变得容易得多。因为某种程度上说,@classmethod和@staticmethod有共通的地方。还是让我们还是从改造模具开始:

 # 定义类
    class Girlfriend:
        age = 22
        sex = '女'
        size = 'C'

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

        def about_me(self):
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(self.age, self.sex, self.size, self.hair))

        @staticmethod
        def bio():
            print('全心全意为主人服务!')

        @classmethod
        def cls_name(cls):
            print('当前使用模具是{0}'.format(cls.__name__))

这次我将模具进行了一个小的改进,添加了一个名为cls_name的方法。因为我后续打算再做一些其他各种friend的模具(类),为了避免我弄混了不知道现在用的是哪个模具在生产,我加了这个方法。这个方法的作用是可以让程序显示出当前模具的名称,也就是代码class的名称。让我们看看如何使用:

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')     

    lili.cls_name()  # 显示:当前使用模具是Girlfriend
    Girlfriend.cls_name()  # 显示:当前使用模具是Girlfriend

从刚才的例子我们可以发现,用了两种不同的方式得到了同一个结果,有关于这点我们稍后再谈,先让我们回到开头,从开始说起。

当我们学习Python基础的时候,几乎所有的图书教程都会告诉我们一个知识点:Python为了内置的对象提供了一些内置的方法和属性,目的是为了更方便使用。简而言之,Python为class这个对象内置了一些方法和属性供我们直接使用,比如__name__和__dict__等等,我们已经谈到过是内置的名称前后就会都有双下划线,这里也是这样。在上面最新的模具中,我们应用了class的一个内置的name属性,而这个name记载了class的名称,所以用类似「class.__name__」这样的语法就能获得这个名称。

我们已经知道,如果对class里的方法(函数)不用@staticmethod装饰,调用的时候就会被程序硬塞一个实例作为第一个参数传入,用@staticmethod则调用的时候不隐式的传东西了,而现在我们的需求是查看显示模具本身的一个属性,跟实例无关,所以可以想到必须让方法能接收模具(类)而不是实例作为一个参数传入。到目前为止,我们学会了要么隐式传实例,要么啥也不传的手段,那么这种需要隐式传模具(类)可怎么办?Python早已考虑到了这种场景并准备了对应的装饰器供我们使用,没错,就是@classmethod。

可想而知,@classmethod就是让实例在调用被@classmethod装饰的方法时,隐式的传入模具(类)本身。类似self,由于约定俗称和class是一个Python关键字因而不能瞎用的原因,通常用「cls」这个英文作为指代类的参数名,这是Python里cls参数的由来。

@classmethod 的出场率也不高,但比@staticmethod用途更大,因为@classmethod 可以作为一种媒介,让我们更方便的操纵Python为class内置的一些属性和方法。那么,@classmethod 的应用场景也就不言而喻了。

@property

@property 是本文最后要谈及的一款装饰器了,让我们还是从改造模具开始:

# 定义类
    class Girlfriend:
        age = 22
        sex = '女'

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

        def about_me(self):
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(self.age, self.sex, self.size, self.hair))

        @staticmethod
        def bio():
            print('全心全意为主人服务!')

        @classmethod
        def cls_name(cls):
            print('当前使用模具是{0}'.format(cls.__name__))

        @property
        def size(self):
            self.bra = 'C'
            return self.bra

在刚才定义的最新的模具中,我做了这样几件事情:先将原来的属性size变成了一个同名方法,然后对其应用了@property装饰器,这些改变了什么?

在谈及@classmethod和之前的装饰器时,我们举例的模具在生产出女朋友之后,实际上都可以这么做:

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')     

    lili.size = 'D'  # 增大女朋友的罩杯
    lili.size  # 显示:D

也就是如果对女朋友的属性(胸)不满意,可以再调整。刚才的例子中我们对lili进行了「丰胸」,将lili原来的size由C升到了D。丰胸的过程是很简单的属性赋值语法,不用太多解释。但让我们用最新的定义了@property 装饰器的模具时,「丰胸」就失败了。在最新的模具中,我们可以使用「lili.size」得到C这个结果,但是无法再运行「lili.size = ‘D’」,一旦试图「丰胸」程序就会报错,报错信息大意是:不能写入size的属性值,原始报错信息如下:

Traceback (most recent call last):

File "<input>", line 1, in <module>

AttributeError: can’t set attribute

那么过程和结果以及区别都看到了,但要理解@property还得从定义最新的模具开始说起。由于比较喜欢纯天然的妹纸,所以我对模具进行了改进,使得一旦生产出女朋友,则不能随意再去改变女朋友的size,并且只能是C。理解成对于代码的修改需求,就是让实例的size的值不能写入新的,但是可以读旧的,并且size仍旧要是一个属性。

Python也为我们考虑到了这种需求场景,并提供了@property 这个装饰器来满足这种需求,于是乎就有了本节最开始的新模具的改变。首先,我们注意到size由属性变成了一个方法,而基于我们Python的基础知识,对于Python方法的调用语法,都是类似「size()」这样方法(函数)名称后面加括号的形式,但就在刚才我们对新模具仍旧使用了「lili.size」并得到了结果,反而「lili.size()」无法运行(程序会报错),于是我们发现了@property的第一个作用:

@property 会将被装饰的类方法模拟成一个类属性

将原来是类的一个属性变成了这个类的方法,并且还行不更名坐不改姓,然后又将这个方法模拟成类属性一样可以调用,感觉这真是绕了好大一个圈。但我们并没有回到原点,因为既然是方法,那么就可以在方法里定义很多赋值语句无法完成的行为。但是我们举一反三之下紧接着有了一个疑问:不错,@property 把方法装饰成属性,就可以实现方法能做的,而原本的属性不能做的事情;那原来属性能做的事情,@property 装饰的方法是否也相应的不行了呢?

我们基础知识告诉我们,对于一个对象的属性,起码能做增、删、改、查这几件基本的事情,在前面最后的例子中,我们通过「lili.size」得到了女朋友的size属性,这是查询,但我们试图修改时,程序则报错了。看起来@property 将方法变成的属性并没有完整实现基本属性该有的几样功能,真是这样吗?让我们再继续改造一下模具,这是最后的一次改造:

    # 定义类
    class Girlfriend:
        age = 22
        sex = '女'

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

        def about_me(self):
            print('年芳{0},性别{1},身材{2}棒棒哒,一头{3}'.format(self.age, self.sex, self.size, self.hair))

        @staticmethod
        def bio():
            print('全心全意为主人服务!')

        @classmethod
        def cls_name(cls):
            print('当前使用模具是{0}'.format(cls.__name__))

        @property
        def size(self):
            self.bra = 'C'
            return self.bra

        @size.setter
        def size(self, size):
            self.bra = size
            print('丰胸手术完成!')

在最后的一次改造中,我们增加了一个名为size()的方法,并对之使用了「@size.setter」这个装饰器。现在我们可以很容易的理解最新的size()方法干了些什么:先接收一个参数,并将这个参数赋给bra,这毫无疑问就是在丰胸。然后如果顺利,用print打印一个结果。在学习Python的基础时,我们已经知道同一命名空间下有两个重名方法是有问题的,会造成最后一个同名方法覆盖掉先前的那个。这里有两个size()方法让我们有些疑惑,@size.setter 又是什么呢?还是先试试:

    # 生产几个女朋友
    lili = Girlfriend('乌黑长发')
    mimi = Girlfriend('金黄短发')     

    lili.size  # 显示:C
    lili.size = 'D'  # 增大女朋友的罩杯,显示:丰胸手术完成!
    lili.size  # 显示:D

看起来丰胸手术已经可以顺利实施,与之前不同,程序不会再报错。可以联想到这一切一定是最新添加的那一小段代码的作用。

原来,Python不仅光提供@property 装饰器供你将一个方法变成属性调用,还提供了一系列配套的工具(setter、getter、deleter),这些正好对应增、删、改、查,这些工具让方法去更加完美的模拟属性的行为。这其中,当你用@property去装饰一个方法名为A的方法时,你可以再用@A.setter去装饰另一个同名方法。顾名思义,setter有写入的含义,那么@A.setter定义的就是方法A赋值时的行为。在上述的代码中,我们就定义了作为属性使用的size方法,在赋值时可以打印一个语句,提示「丰胸手术完成」。其余的几个工具就不多说,从字面意思就能理解做的事情,而使用方法都是一样的。所以,@property 和之后的工具使用,本质上都是针对@property装饰的方法做更多的定制,所以像上面的那样重名方法才不会有问题。

现在,你应该大致也理解了@property能干的事情,其作用和场景也很明显了。让我们在本文最后再看一段来自《Flask Web开发:基于Python的Web应用开发实战》P78,示例8-1的代码:

     class User(db.Model):
         password_hash = db.Column(db.String(128))

         @property
         def password(self):
             raise AttributeError('password is not a readable attribute')

         @password.setter
         def password(self, password):
             self.password_hash = generate_password_hash(password)

        def verify_password(self, password):
             return check_password_hash(self.password_hash, password)

这段代码中,作者使用了@property把原本是User的password方法变成属性一样可以调用。由于password是加密存储到数据库中的,于是需要一个方法完成加密过程,这正好可以由工具setter去干,于是乎我们看到@password.setter又装饰了一个password方法,并且这个方法我们能够看出是调用generate_password_hash()把原来的密码进行了加密,并将加密后的内容传给了password_hash这个属性,这个属性就是数据库中的用户密码字段。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址