这篇文章介绍有关 Python 类中一些常被大家忽略的知识点,帮助大家更全面的掌握 Python 中类的使用技巧
1、与类和对象相关的内置方法
issubclass(class, classinfo)
:检查 class 是否为 classinfo 的子类,classinfo 可以是一个类也可以是由多个类组成元组,注意 class 被认为是 class 的子类,也被认为是 object(所有类默认继承于object)的子类,若传入的类型与期望不符则抛出 TypeError 异常isinstance(object, classinfo)
:检查 object 是否为 classinfo 的实例化对象,classinfo 可以是一个类也可以是由多个类组成元组,注意若 object 不是对象则返回 False,若 classinfo 既不是类也不是由多个类组成元组则抛出 TypeError 异常==
比较两对象是否相等,is
比较两对象是否相同hasattr(object, name)
:检查 object 中是否有特定属性 name,注意 name 需要用双引号包围getattr(object, name [, default])
:获取 object 中特定属性 name 的值,若属性不存在,则抛出异常(没设置 default)或打印提示信息default(有设置 default),注意 name 需要用双引号包围setattr(object, name, value)
:设置 object 中特定属性 name 的值,若属性不存在,则新建一个属性并赋值,注意 name 和 value 都需要用双引号包围delattr(object, name)
:删除 object 中特定属性 name 的值,若属性不存在则抛出 AttributeError 异常,注意 name 需要用双引号包围
2、隐藏
默认情况下,Python 允许在外部直接访问对象的属性
>>> class Test:
num = 0
def setNum(self,num): # 修改器
self.num = num
def getNum(self): # 访问器
return self.num
>>> obj = Test()
>>> # 我们可以直接访问对象的数据成员,而不需要通过访问器和修改器
>>> obj.num = 100
>>> obj.num
# 100
这似乎违反了隐藏的原则,因为在 Python 中没有为私有属性提供直接的支持,而要求程序员知道在什么情况下从外部修改变量才是安全的。但是 Python 有另一种方式实现了类似于私有属性的效果
要让方法或属性成为私有的(不能从外部访问),只需要让名称以两个下划线开头即可
>>> class Test:
__num = 0
def setNum(self,num):
self.num = num
def getNum(self):
return self.num
>>> obj = Test()
>>> # 此时,我们将不能直接访问对象私有的属性和方法
>>> obj.num # AttributeError
>>> # 但是,我们依然可以使用访问器和修改器访问和修改数据
>>> obj.setNum(100)
>>> obj.getNum()
# 100
其实,这只是 Python 玩的一点小把戏,在类定义中,Python 对所有以两个下划线开头的名称进行了转换,即在开头加上下划线和类名,所以对于上面的例子我们依然可以直接访问对象的私有属性
>>> obj._Test__num = 100
>>> obj._Test__num
# 100
总之,你无法禁止别人访问对象的私有方法和属性,只是以双下划线开头向对方发出了强烈的信号,希望他们不要这样做
如果你不希望名称被修改,又想让别人知道不应该从外部修改属性或方法,可以使用一个下划线开头,虽然这只是一种约定,但是还是有一些作用的,例如在使用 import 语句导入模块时,将不会导入以一个下划线开头的名称
3、参数 self
在类定义的方法中,传入的第一个参数一般为 self,self 究竟是什么呢?学过 C++ 的朋友或许很容易理解,Python 中的 self 其实就相当于 C++ 中的 this 指针,它指向调用该方法的实例化对象
在刚刚接触时,常常会出现以下错误
>>> # 常见错误:忘记在一般方法中加上 self 参数
>>> class Test:
def __init__(self):
self.value = 0
def getValue(): # 错误定义,没有加上参数 self
return value
>>> test = Test()
>>> test.getValue()
# TypeError: getValue() takes 0 positional arguments but 1 was given
>>> # 常见错误:忘记在方法中调用参数时加上 self,此时会默认该参数为局部变量
>>> class Test:
def __init__(self):
self.value = 0
def getValue(self): # 正确定义
return value # 这里返回的是局部变量
>>> test = Test()
>>> test.getValue()
# NameError: name 'value' is not defined
在一般情况下,类方法都应该加上参数 self,但是也并非全部如此,在类方法中没有加上 self 参数的方法称为静态方法,它可以直接通过类来调用,而不可以通过对象调用
>>> class Test:
def show():
print('static function')
>>> test = Test()
>>> test.show() # 不可以通过对象调用
# TypeError: show() takes 0 positional arguments but 1 was given
>>> Test.show() # 但可以通过类直接调用
# static function
另外一个不需要加上 self 参数的情况是类方法,该方法传入的第一个参数是类似于 self 的参数 cls,该方法可通过对象直接调用,但参数 cls 将自动关联到类
>>> class Test:
def show(cls):
print('static function')
>>> test = Test()
>>> test.show() # 可以通过对象直接调用
# static function
4、魔法方法
魔法方法是一种特殊的方法,它的名称有以下的格式:__name__
,以两个下划线开头,以两个下划线结尾,其与多种类操作有关
(1)构造函数
Python 提供一种魔法方法 __init__(self)
,也被称为构造函数,该方法在创建对象时自动调用,其返回值必须为 None,主要用于初始化对象
>>> class Test:
def __init__(self,value):
print('__init__ is called.')
self.value = value
def getValue(self):
return self.value
>>> obj = Test(16)
# __init__ is called.
>>> obj.getValue()
# 16
在继承机制中,方法重写对构造函数尤为重要,对于一般的子类而言,在构造函数中不仅需要父类的初始化代码,还需要自己的初始化代码。要注意,在重写构造函数时,必须调用父类的构造函数,否则可能无法正确初始化对象
>>> class Person:
def __init__(self,name):
self.name = name
def show(self):
print('Name:', self.name)
>>> class Student(Person):
def __init__(self,stuID): # 构造函数重写,没有调用父类的初始化方法
self.stuID = stuID
def show(self): # 普通方法重写
print('Name:', self.name)
print('stuID:', self.stuID)
>>> student = Student(1234)
>>> student.show()
# AttributeError: 'Student' object has no attribute 'name'
我们可以看到上述的使用方法是错误的,因为在子类中没有调用父类的初始化方法,父类属性不能被正确初始化,为此,有两种解决方法:一是调用未关联的父类构造函数,二是使用函数 super
>>> # 调用未关联的父类构造函数,旧式用法
>>> class Student(Person):
def __init__(self,name,stuID): # 构造函数重写,调用父类的初始化方法
Person.__init__(self,name)
self.stuID = stuID
def show(self): # 普通方法重写
Person.show(self)
print('stuID:', self.stuID)
>>> student = Student('Peter',1234)
>>> student.show()
# Name: Peter
# stuID: 1234
>>> # 使用函数 super,新式用法
>>> class Student(Person):
def __init__(self,name,stuID): # 构造函数重写,调用父类的初始化方法
super().__init__(name) # 区别仅仅在于这一命令
self.stuID = stuID
def show(self): # 普通方法重写
super().show()
print('stuID:', self.stuID)
>>> student = Student('Peter',1234)
>>> student.show()
# Name: Peter
# stuID: 1234
在上面的例子中,可以看到两种处理方法的效果是一致的,但是调用未关联的父类构造函数难以处理多继承的问题,而使用函数 super 依然可以同样的语法处理,而忽视其内部复杂的处理机制,所以在新版的 Python 中,建议使用 super 代替未关联的父类构造函数
事实上,__init__(self)
并不是实例化对象时第一个被调用的方法,第一个被调用的方法是 __new__(cls)
,该方法的第一个参数是 cls,若还有其它参数则原封不动地传递给 __init__
。__new__
方法一般返回一个实例化对象,正常情况下极少重写 __new__
方法,但当需要继承一个不可变方法,又需要对其进行修改时,可以重写该方法
>>> class MyString(str):
def __new__(cls,string):
string = string.upper()
return str.__new__(cls,string)
>>> obj = MyString('abcdefg')
>>> obj
# 'ABCDEFG'
(2)析构函数
另外,Python 还提供魔法方法 __del__(self)
,也被称为析构函数,该方法在对象销毁前自动调用,但事实上,使用 del 语句销毁对象时并不会立即调用 __del__(self)
方法,而是当所有指向该对象的标签被销毁时,该方法才会被调用
>>> class Test:
def __del__(self):
print('__del__ is called')
>>> obj = Test()
>>> temp = obj
>>> del obj # 删除指向对象的标签时,并不直接调用 __del__
>>> del temp # 删除指向该对象的所有标签时,才会调用 __del__
# __del__ is called
(3)元素访问
使用本小节中介绍的魔法方法,可以帮助我们更好的创建序列和映射类型,实际上,序列和映射基本上是元素的集合,要实现它们的基本行为,不可变对象只需要实现 2 个方法(__len__
和 __getitem__
),而可变对象则需要实现 4 个方法(除上述两个外,加上 __setitem__
和 __delitem__
)
__len__(self)
:返回集合包含的项数,对序列而言是元素个数,对映射而言是键-值对个数,当对象使用内置函数len
时被调用len(self)
__getitem__(self,key)
:返回与指定键相对应的值,对序列而言键应该是整数,对映射而言是键可以是任何类型,当对象被访问时调用self[key]
__setitem__(self,key,value)
:以与键相关联的方式储存值,仅在可变对象中才需要实现,当对象被修改时调用self[key] = value
__delitem__(self,key)
:删除与键相关联的值,仅在可变对象中才需要实现,当对象使用内置函数del
时被调用del self[key]
>>> class MyDic:
def __init__(self):
self.dic = dict()
def __getitem__(self,key):
print('__getitem__ is called')
return self.dic[key]
def __setitem__(self,key,value):
print('__setitem__ is called')
self.dic[key] = value
# 这里为了演示效果,没有实现 __del__ 方法,因此不能使用 del 删除其中的元素
>>> dic = MyDic()
>>> dic['A'] = 1 # 修改器
# __setitem__ is called
>>> dic['A'] # 访问器
# __getitem__ is called
# 1
>>> del dic['A'] # AttributeError: __delitem__
除了以上最基本的魔法方法可以帮助我们创建序列和映射类型外,还有其它一些有用的魔法方法,例如,__contains__(self,item)
定义使用成员运算符 in 或者 not in 时的行为
(4)属性访问
属性访问魔法方法可以拦截对对象属性的所有访问企图,一般可以执行权限检查,日志记录等操作
__getattribute__(self,name)
:在属性被访问时自动调用__getattr__(self,name)
:在属性被访问且对象没有该属性时自动调用__setattr__(self,name,value)
:在试图给属性赋值时自动调用__delattr__(self,name)
:在试图删除属性时自动调用
>>> class Test:
def __init__(self):
self.num = 0
def __setattr__(self,name,value):
print('__setattr__ is called')
self.__dict__[name] = value
# 注意,在这里使用 self.name = value 这样的常规赋值语句是错误的,
# 因为在语句中会再次调用 __setattr__,导致无限循环的错误
# 因此,可以使用对象内置属性 __dict__ 进行赋值
def __getattribute__(self,name):
print('__getattribute__ is called')
return super().__getattribute__(name)
# 同样,在这里使用 self.name 或 self.__dict__[name] 都是错误的
# 因为在语句中会再次调用 __getattribute__,导致无限循环的错误
# 因此,唯一安全的方法是使用 super
def __getattr__(self,name):
print('__getattr__ is called')
raise AttributeError
# 当无法找到属性时被调用,返回异常 AttributeError
>>> test = Test() # __init__ 被调用,执行属性 num 赋值
# __setattr__ is called
# __getattribute__ is called
>>> test.num # 执行属性 num 访问
# __getattribute__ is called
# 0
>>> test.void # 执行属性 void 访问
# __getattribute__ is called
# __getattr__ is called
# AttributeError
(5)运算符
① 算术运算
二元运算符:
__add__(self, other)
:定义加法行为:+__sub__(self, other)
:定义减法行为:-__mul__(self, other)
:定义乘法行为:*__truediv__(self, other)
:定义真正除法行为:/__floordiv__(self, other)
:定义整数除法行为://__mod__(self, other)
:定义取模运算行为:%__pow__(self, other [, modulo])
:定义取幂行为:**__lshift__(self, other)
:定义按位左移行为:<<__rshift__(self, other)
:定义按位右移行为:>>__and__(self, other)
:定义按位与行为:&__or__(self, other)
:定义按位或行为:|__xor__(self, other)
:定义按位异或行为:^
注意:
- 反运算:在函数名前加一个字符 r,当左操作数不支持该算术运算时,则会利用右操作数调用该算术运算方法
- 增量赋值:在函数名前加一个字符 i,支持类似于 x = y (即 x = x y)的操作
一元操作符:
__pos__(self)
:定义正号行为:+__neg__(self)
:定义减号行为:-__abs__(self)
:定义绝对值行为:abs()__invert__(self)
:定义按位取反行为:~
② 比较运算
__lt__(self, other)
:定义小于行为:<__le__(self, other)
:定义小于等于行为:<=__eq__(self, other)
:定义等于行为:=__ne__(self, other)
:定义不等于行为:!=__gt__(self, other)
:定义大于行为:>__ge__(self, other)
:定义大于等于行为:>=
>>> class Test:
def __init__(self,value):
self.value = value
def __add__(self,other):
return Test(self.value + other.value)
def __neg__(self):
return Test(-self.value)
def __gt__(self,other):
return (self.value > other.value)
>>> test1 = Test(100)
>>> test2 = Test(1)
>>> test1 > test2
# True
>>> test3 = test1 + (-test2)
>>> test3.value
# 99
(6)输出函数
__str__(self)
:当使用函数 str 或使用函数 print 时被调用__repr__(self)
:当使用函数 repr 或直接输出对象时被调用
>>> class Test:
def __init__(self,value):
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return 'This is the class, Test'
>>> test = Test(100)
>>> str(test)
# '100'
>>> print(test)
# 100
>>> repr(test)
# 'This is the class, Test'
>>> test
# This is the class, Test
(7)描述符类
描述符类用于描述另一个类的属性,其将某种特殊类型的类实例指派给另一个类的属性,特殊类型的类需至少实现以下三个内置方法之一
__get__(self, instance, owner)
:用于访问属性,返回属性的值,self 为特殊类型的实例,instance 为另一个类的实例,owner 为另一个类的对象__set__(self, instance, value)
:用于设置属性,不返回任何内容,self为特殊类型的实例,instance为另一个类的实例,value为传入的值__delete__(self, instance)
:用于删除属性,不返回任何内容,self为特殊类型的实例,instance为另一个类的实例
在 Python 中有一个自带的描述符类 property,也可以直接使用
property(fget=None, fset=None, fdel=None,doc=None)
当指定 property 时,
若没有指定任何参数,则创建的特性既不可读也不可写
- 若指定一个参数( fget ),则创建的特性将是只读的
- 若指定两个参数( fget 和 fset ),则创建的特性将是可读可写的
- 第三个参数是可选的,指定用于删除属性的方法,这个方法不接受任何参数
第四个参数也是可选的,指定一个文档字符串
>>> class MyProperty: # 自己创建一个描述符类
def __init__(self,fget=None,fset=None,fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self,instance,onwer):
print('__get__ is called')
return self.fget(instance)
def __set__(self,instance,value):
print('__set__ is called')
self.fset(instance,value)
def __delete__(self,instance):
print('__delete__ is called')
self.fdel(instance)
>>> class Test:
def __init__(self):
self._value = 0
def _getValue(self):
return self._value
def _setValue(self,value):
self._value = value
value = MyProperty(_getValue,_setValue)
>>> test = Test()
>>> test.value = 1
# __set__ is called
>>> test.value
# __get__ is called
# 1
5、迭代器
迭代意味着重复多次,我们常常使用 for 循环迭代序列和字典,但实际上我们也可以迭代实现了方法
__iter__
的对象,一般的定义为实现了方法 __iter__
的对象是可迭代的,而实现了方法 __next__
的对象是迭代器
__iter__
:返回一个迭代器,它是包含方法__next__
的对象,调用时可不提供任何参数__next__
:返回下一个值,如果迭代器没有可供返回的值,则引发 StopIteration 异常
那为什么需要使用迭代器呢?这 是因为使用迭代器在某种情形下比列表更节省内存
>>> class Fib: # 斐波那契数列
def __init__(self):
self.a = 0
self.b = 1
def __next__(self):
self.a,self.b = self.b,self.a+self.b
return self.a
def __iter__(self):
return self
>>> fib = Fib() # 迭代器
>>> for i in fib:
if i < 10:
print(i)
else:
break
1
1
2
3
5
8
另外,对可迭代对象使用内置方法 iter 也可以获得一个生成器,并且可以使用内置方法 next 获取迭代器的下一个值,如果迭代器没有可返回的值,则引发 StopIteration 异常
>>> it = iter([1,2])
>>> next(it)
# 1
>>> next(it)
# 2
>>> next(it)
# StopIteration
6、生成器
如果列表元素可以按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,而不必创建完整的列表,从而节省大量的空间,这样的机制称为生成器
相信大家都已经学过列表生成式了,最简单的一种创建生成器方法是把列表生成式中的 []
改为 ()
即可
>>> li = [i for i in range(5)]
>>> type(li)
# <class 'list'>
>>> gene = (i for i in range(5))
>>> type(gene)
# <class 'generator'>
生成器也是可迭代对象,故可以使用 for 循环进行迭代,也可以使用内置方法 next 获取下一个值
>>> next(gene)
# 0
>>> next(gene)
# 1
>>> for i in gene:
print(i)
# 2
# 3
# 4
另外一种创建生成式的方法是在函数中使用 yield,此时函数将不再是普通的函数,而是成为一个生成器
>>> def fib():
a, b = 0, 1
while True:
yield b
a, b = b, a + b
>>> f = fib()
>>> type(f)
# <class 'generator'>
>>> next(f)
1
最难理解的地方就是生成器和普通函数的执行流程不太一样
- 普通函数是顺序执行,遇到 return 语句则返回;
- 生成器在每次请求值时都执行生成器的代码,直至遇到 yield 或 return。yield 意味着返回一个值,且下次执行从上次返回的 yield 语句处开始,而 return 意味着生成器应停止执行