python提升二
Python self用法
在定义类的过程中,无论是显式创建类的构造方法,还是向类中添加实例方法,都要求将 self 参数作为方法的第一个参数。
1 | class Person: |
Python 只是规定,无论是构造方法还是实例方法,最少要包含一个参数,并没有规定该参数的具体名称。之所以将其命名为 self,只是程序员之间约定俗成的一种习惯,遵守这个约定,可以使我们编写的代码具有更好的可读性(大家一看到 self,就知道它的作用)。
self 参数的具体作用打个比方
如果把类比作造房子的图纸,那么类实例化后的对象是真正可以住的房子。根据一张图纸(类),我们可以设计出成千上万的房子(类对象),每个房子长相都是类似的(都有相同的类变量和类方法),但它们都有各自的主人,那么如何对它们进行区分呢?
当然是通过 self 参数,它就相当于每个房子的门钥匙,可以保证每个房子的主人仅能进入自己的房子(每个类对象只能调用自己的类变量和类方法)。如果你接触过其他面向对象的编程语言(例如 C++),其实 Python 类方法中的 self 参数就相当于 C++ 中的 this 指针。
也就是说,同一个类可以产生多个对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法,换句话说,Python 会自动绑定类方法的第一个参数指向调用该方法的对象。如此,Python解释器就能知道到底要操作哪个对象的方法了。
程序在调用实例方法和构造方法时,不需要手动为第一个参数传值
1 | class Person: |
Python类变量和实例变量
无论是类属性还是类方法,都无法像普通变量或者函数那样,在类的外部直接使用它们。我们可以将类看做一个独立的空间,则类属性其实就是在类体中定义的变量,类方法是在类体中定义的函数。
前面提到过,在类体中,根据变量定义的位置不同,以及定义的方式不同,类属性又可细分为以下 3 种类型:
1.类体中、所有函数之外:此范围定义的变量,称为类属性或类变量;
2.类体中,所有函数内部:以“self.变量名”的方式定义的变量,称为实例属性或实例变量;
3.类体中,所有函数内部:以“变量名=变量值”的方式定义的变量,称为局部变量。
类变量(类属性)
类变量指的是在类中,但在各个类方法外定义的变量
1 | class CLanguage : |
类变量的特点: 所有类的实例化对象都同时共享类变量,也就是说,类变量在所有实例化对象中是作为公用资源存在的。
类方法的调用方式有 2 种,
既可以使用类名直接调用,也可以使用类的实例化对象调用。
1 | #使用类名直接调用 |
通过类名不仅可以调用类变量,也可以修改它的值。
当然,也可以使用类对象来调用所属类中的类变量(此方式不推荐使用)
clang = CLanguage()
print(clang.name)
print(clang.add)
注意:因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象
通过类名修改类变量,会作用到所有的实例化对象
实例变量(实例属性)
实例变量指的是在任意类方法内部,以“self.变量名”的方式定义的变量,其特点是只作用于调用方法的对象。
另外,实例变量只能通过对象名访问,无法通过类名访问。
1 | class CLanguage : |
此 CLanguage 类中,name、add 以及 catalog 都是实例变量。其中,由于 init() 函数在创建类对象时会自动调用,而 say() 方法需要类对象手动调用。因此,CLanguage 类的类对象都会包含 name 和 add 实例变量,而只有调用了 say() 方法的类对象,才包含 catalog 实例变量。
为什么不推荐使用类对象调用类变量
前面讲过,通过类对象可以访问类变量,但无法修改类变量的值。这是因为,通过类对象修改类变量的值,不是在给“类变量赋值”,而是定义新的实例变量。
1 | clang = CLanguage() |
通过类对象是无法修改类变量的值的,本质其实是给 clang 对象新添加 name 和 add 这 2 个实例变量
实例变量和类变量可以同名,但这种情况下使用类对象将无法调用类变量,它会首选实例变量,这也是不推荐“类变量使用对象名调用”的原因。
另外,和类变量不同,通过某个对象修改实例变量的值,不会影响类的其它实例化对象,更不会影响同名的类变量。
局部变量
除了实例变量,类方法中还可以定义局部变量。和前者不同,局部变量直接以“变量名=值”的方式进行定义
1 | class CLanguage : |
通常情况下,定义局部变量是为了所在类方法功能的实现。需要注意的一点是,局部变量只能用于所在函数中,函数执行完成后,局部变量也会被销毁。
Python实例方法、静态方法和类方法
和类属性一样,类方法也可以进行更细致的划分,具体可分为类方法、实例方法和静态方法。
和类属性的分类不同,对于初学者来说,区分这 3 种类方法是非常简单的,采用 @classmethod 修饰的方法为类方法;
采用 @staticmethod 修饰的方法为静态方法;
不用任何修改的方法为实例方法。
其中 @classmethod 和 @staticmethod 都是函数装饰器
Python类实例方法
通常情况下,在类中定义的方法默认都是实例方法。前面,我们已经定义了不只一个实例方法。不仅如此,类的构造方法理论上也属于实例方法,只不过它比较特殊。
1 | class CLanguage: |
实例方法最大的特点就是,它最少也要包含一个 self 参数,用于绑定调用此方法的实例对象(Python 会自动完成绑定)。实例方法通常会用类对象直接调用
当然,Python 也支持使用类名调用实例方法,但此方式需要手动给 self 参数传值
1 | clang = CLanguage() |
Python 类方法
Python 类方法和实例方法相似,它最少也要包含一个参数,只不过类方法中通常将其命名为 cls,
Python 会自动将类本身绑定给 cls 参数(注意,绑定的不是类对象)。
也就是说,我们在调用类方法时,无需显式为 cls 参数传参。和 self 一样,cls 参数的命名也不是规定的(可以随意命名),只是 Python 程序员约定俗称的习惯而已
1 | class CLanguage: |
注意,如果没有 @classmethod,则 Python 解释器会将 fly() 方法认定为实例方法,而不是类方法。
类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)
Python类静态方法
静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。
静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。
静态方法需要使用@staticmethod修饰
1 | class CLanguage: |
静态方法的调用,既可以使用类名,也可以使用类对象
在实际编程中,几乎不会用到类方法和静态方法,因为我们完全可以使用函数代替它们实现想要的功能
浅谈Python类命名空间
Python 类体中的代码位于独立的命名空间(称为类命名空间)中。换句话说,所有用 class 关键字修饰的代码块,都可以看做是位于独立的命名空间中。
和类命名空间相对的是全局命名空间,即整个 Python 程序默认都位于全局命名空间中。而类体则独立位于类命名空间中。
但需要注意的一点是,当使用类对象调用类方法时,在传参方面是和外界的函数有区别的,因为 Python 会自动会第一个参数绑定方法的调用者,而位于全局空间中的函数,则必须显式为第一个参数传递参数。
Python描述符
Python 中,通过使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作
本质上看,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理全权委托给描述符类。
描述符是 Python 中复杂属性访问的基础,它在内部被用于实现 property、方法、类方法、静态方法和 super 类型
描述符类基于以下 3 个特殊方法,换句话说,这 3 个方法组成了描述符协议:
set(self, obj, type=None):在设置属性时将调用这一方法(后续用 setter 表示);
get(self, obj, value):在读取属性时将调用这一方法(后续用 getter 表示);
delete(self, obj):对属性调用 del 时将调用这一方法。
其中,实现了 setter 和 getter 方法的描述符类被称为数据描述符;
反之,如果只实现了 getter 方法,则称为非数据描述符。
在每次查找属性时,描述符协议中的方法都由类对象的特殊方法 getattribute() 调用(注意不要和 getattr() 弄混)。也就是说,每次使用类对象.属性(或者 getattr(类对象,属性值))的调用方式时,都会隐式地调用 getattribute(),它会按照下列顺序查找该属性:
验证该属性是否为类实例对象的数据描述符;
如果不是,就查看该属性是否能在类实例对象的 dict 中找到;
最后,查看该属性是否为类实例对象的非数据描述符。
1、位于搜索链上的顺序。搜索链(或者优先链)的顺序大概是这样的:
数据描述符>实体属性(存储在实体的_dict_中)>非数据描述符。
这个顺序初看起来挺晦涩。解释如下:
获取一个属性的时候:
首先,看这个属性是不是一个数据描述符,如果是,就直接执行描述符的_get_,并返回值。
其次,如果这个属性不是数据描述符,那就按常规去从_dict_里面取。
最后,如果_dict_里面还没有,但这是一个非数据描述符,则执行非数据描述符的_get_方法,并返回
1 | class revealAccess: |
运行结果为:
Retrieving var “x”
10
updating var “x”
Retrieving var “x”
20
5
可以看到,如果一个类的某个属性有数据描述符,那么每次查找这个属性时,都会调用描述符的 get() 方法,并返回它的值;同样,每次在对该属性赋值时,也会调用 set() 方法