# 内置方法
- 用户定义一个 class 时,可以包含任意个 method ,每个 method 的名称由用户任意决定。但有些特殊的 method 名称,被 Python 解释器占用了,称为内置方法。
- 内置方法的命名特点:以两个下划线
__
开头,以两个下划线__
结尾。 - 内置方法用于实现一些特殊的功能。
- 例如定义了
__add__()
方法,就可以让当前类的实例,支持加法运算。 - 由于内置方法的功能特殊,它们又被称为魔法方法(magic method)。
- 例如定义了
- Python 总共设计了几十种内置方法。
- 用户定义一个 class 时,不需要定义所有内置方法。
- 用户定义一个 class 时,即使不主动定义内置方法,也会从元类、父类,继承一些基础的内置方法,例如
__new__()
、__init__()
、__repr__()
、__str__()
- 内置方法的命名特点:以两个下划线
# 关于实例
# __new
def __new__(cls, ...)
- 功能:创建当前类的一个实例,并返回该实例(的引用)。
- 它要求输入当前类,作为第一个实参。之后还可传入任意个参数。
- 例:
>>> str.__new__() TypeError: str.__new__(): not enough arguments >>> str.__new__(str, 'hello') 'hello'
- 如果让
__new__()
总是返回同一个实例,则可以实现单例模式。>>> class Test: ... def __new__(cls): ... if not hasattr(cls, 'instance'): ... cls.instance = super().__new__(cls) ... return cls.instance ... >>> Test() <__main__.Test object at 0x00000140699F54C0> >>> Test() <__main__.Test object at 0x00000140699F54C0> >>> Test() is Test() True
# __init
def __init__(self, ...)
- 功能:用于在新建实例之后,初始化该实例。类似于 C++ 中的构造函数。
- 它要求输入当前实例,作为第一个实参。之后还可传入任意个参数。
- 例:
>>> class Test: ... def __new__(cls): ... print('__new__') ... return super().__new__(cls) ... def __init__(self): ... print('__init__') ... >>> t = Test.__new__(Test) # 创建一个实例,此时该实例尚未执行 __init__() 方法 __new__ >>> t.__init__() __init__
# __del
def __del__(self)
- 功能:用于销毁实例。类似于 C++ 中的析构函数。
- 何时生效?
- 用关键字 del 删除一个对象时,并不会调用该对象的
__del__()
方法,只是使得其被引用数减 1 。 - 当一个对象的被引用数,减少到 0 时,Python 解释器会自动调用该对象的
__del__()
方法,然后销毁该对象。 - 一般情况下,用户不必定义
__del__()
方法。但有时,用户希望在每个对象即将被销毁时,执行某些操作,例如记录日志,则可以定义__del__()
方法。
- 用关键字 del 删除一个对象时,并不会调用该对象的
# __call
def __call_(self, ...)
- 在 Python 的底层,函数也是基于 class 定义的。当用户执行
function_name()
调用一个函数时,实际上是调用该函数的__call__()
方法。 - 因此,用户可以给任何 class 定义
__call__()
方法,使得该 class 的实例,可以当作函数被调用。 - 例:
>>> class Test: ... def __call__(self, *args, **kwargs): ... print(*args, **kwargs) ... >>> t1 = Test() >>> t1('hello') hello
# 关于属性
# __getattribute
def __getattribute__(self, name)
- 功能:当用户读取对象的一个属性(也就是实例变量、实例方法)时,会自动调用对象的该方法。
- 如果未找到该属性,则抛出 AttributeError 异常。
- 例:
>>> class Test: ... def __getattribute__(self, name): ... if name == 'a': ... return 0 ... else: ... # return self.__getattribute__(name) # 这行代码会调用当前 __getattribute__() 方法,导致无限循环 ... return super().__getattribute__(name) # 为了避免无限循环,应该调用父类的 __getattribute__() 方法 ... >>> Test.a # 读取类变量、类方法时,不会调用 __getattribute__() 方法 AttributeError: type object 'Test' has no attribute 'a' >>> Test().a 0 >>> Test().b AttributeError: 'Test' object has no attribute 'b'
# __getattr
def __getattr__(self, name)
- 功能:当
__getattribute__()
方法抛出 AttributeError 异常时,会自动捕捉异常,然后调用obj.__getattr__()
方法,决定如何读取该属性。 - 例:
>>> class Test: ... def __getattr__(self, name): ... if name == 'a': ... return 0 ... else: ... raise AttributeError("this object has no attribute '{}'".format(name)) ... >>> Test().a 0 >>> Test().b AttributeError: this object has no attribute 'b'
# __setattr
def __setattr__(self, name, value)
- 功能:当用户给对象的一个属性赋值时,会自动调用对象的该方法。
- 赋值时,不会调用
__getattribute__()
方法。 - 例:
>>> class Test: ... def __setattr__(self, name, value): ... # self.name = value # 这行代码会调用当前 __setattr__() 方法,导致无限循环 ... super().__setattr__(name, value) # 为了避免无限循环,应该调用父类的 __setattr__() 方法 ... >>> t = Test() >>> t.a = 0 >>> t.b 0
# __delattr
def __delattr__(self, name)
- 功能:当用户删除对象的一个属性时,会自动调用对象的该方法。
# 关于字符串
# __repr
# __str
def __repr__(self) # 执行 repr(obj) 时,会自动调用该方法
def __str__(self) # 执行 str(obj) 时,会自动调用该方法
这两个方法的用途不同,返回值不一定相同。
__repr__()
用于将对象用一个 str 表示(representation),供用户查看这个对象的大致内容,甚至执行eval(repr(obj)) == obj
可以重新创建该对象。__str__()
用于将对象转换成 str 类型,要求准确保留该对象的所有内容。
例:
>>> a = list() >>> a # 将对象直接输入 Python 终端,相当于执行 print(repr(a)) [] >>> repr(a) '[]' >>> print(repr(a)) [] >>> str(a) # 将对象转换成 str 类型 '[]' >>> print(a) # 将对象 print 到 Python 终端,相当于执行 print(str(a)) [] >>> print(str(a)) []
例:
>>> class Test: ... def __repr__(self): ... return 'TEST' ... def __str__(self): ... return 'test' ... >>> Test() TEST >>> str(Test()) 'test' >>> print(Test()) # 执行 print() 函数时,会先将对象转换成 str 类型,然后打印到终端 test
# 关于运算
def __add__(self, other) # 左加。执行 self + other 时,会自动调用该方法
def __radd__(self, other) # 右加。执行 other + self 时,会自动调用该方法
def __sub__(self, other) # 左减
def __rsub__(self, other) # 右减
def __mul__(self, other) # 左乘
def __rmul__(self, other) # 右乘
def __truediv__(self, other) # 左除
def __rtruediv__(self, other) # 右除
def __eq__(self, other) # == other
def __ne__(self, other) # != other
def __lt__(self, other) # < other
def __le__(self, other) # <= other
def __gt__(self, other) # > other
def __ge__(self, other) # >= other
- 定义这些方法,可以改变运算符的工作逻辑,称为运算符重载。
- 例:
>>> class Test: ... def __init__(self, num): ... self.num = num ... def __sub__(self, other): ... return self.num - other ... def __rsub__(self, other): ... return other - self.num ... >>> Test(1) - 2 -1 >>> 2 - Test(1) 1
# 关于容器
def __bool__(self) # 执行 bool(obj) 时,会自动调用该方法
def __len__(self) # 执行 len(obj) 时,会自动调用该方法
def __sizeof__(self) # 用于计算对象占用的内存大小,单位为 bytes 。执行 sys.getsizeof(obj) 时,会自动调用该方法
def __getitem__(self, key) # 执行 obj[key] 时,会自动调用该方法。如果 obj 属于序列类型,则返回 key 索引处的那个 value 。如果 obj 属于 mapping 类型,则返回 key 映射的那个 value
def __setitem__(self, key, value) # 执行 obj[key] = value 时,会自动调用该方法
def __delitem__(self, key) # 执行 del obj[key] 时,会自动调用该方法
# 关于迭代
Python 中,存在 list、str、set、dict 等多种数据类型的对象。
- 这些对象,可以存放一些元素,因此称为容器。
- 这些容器的数据类型不同,访问方法也不同。
- 例如 list 可通过索引获取元素,而 set 不支持索引。
- 每使用一种容器,就需要考虑其独特的访问方法,比较麻烦。
- 好消息是,这些容器都支持迭代,从而方便用户以同一方式获取这些容器中的元素。
很多编程语言都存在迭代的概念:遍历一个容器中的所有元素,每次只获取一个元素。
- Python 中的迭代器,属于单向只读迭代器:只能向前遍历,不能后退。
- 由于是单向遍历,迭代器遍历完所有元素之后,就会停止迭代。
- 让
__next__()
循环返回元素,就可以创建无限循环的迭代器。
- Python 中的迭代器,属于单向只读迭代器:只能向前遍历,不能后退。
# 迭代器
如果一个对象,实现了
__next__()
方法,则属于迭代器(iterator)。- 例:
>>> class Number: ... def __init__(self): ... self.n = 0 ... def __next__(self): ... if self.n < 5: ... self.n += 1 ... return self.n ... else: ... raise StopIteration ... >>> n = Number() >>> n <__main__.Number object at 0x00000203210B9460> >>> next(n) 1 >>> next(n) 2 >>> list(n) [3, 4, 5] >>> list(n) # 此时 n 已经迭代结束,list() 不能获取元素,因此返回一个空列表 [] >>> next(n) # 此时 n 已经迭代结束,不能获取下一个元素,因此抛出异常 StopIteration
- 例:
相关方法:
def __next__(self)
- 功能:调用该方法,会返回迭代器中的下一个元素,如果没有下一个元素,则抛出异常 StopIteration 。
- 例如,执行
next(obj)
时,会自动调用该方法。
相关函数:
def next(iterator, default=...)
- 功能:获取迭代器中的下一个元素。实际上是调用迭代器的
__next__()
方法,从而得到下一个元素。- 如果不存在下一个元素,且未输入 default 参数,则抛出异常 StopIteration 。
- 如果不存在下一个元素,但输入了 default 参数,则返回 default 参数的值。
- 例:
>>> a = iter('Hello') >>> next(a) 'H' >>> next(a) 'e'
- 功能:获取迭代器中的下一个元素。实际上是调用迭代器的
# 可迭代对象
如果一个对象,实现了
__iter__()
方法,则属于可迭代对象(iterable)。- 例:
>>> class Number: ... def __iter__(self): ... return iter(range(5)) ... >>> n = Number() >>> [i for i in n] [0, 1, 2, 3, 4] >>> next(n) TypeError: 'Number' object is not an iterator
- 例:
相关方法:
def __iter__(self)
- 功能:调用该方法,会返回一个迭代器。
- 例如,执行
iter(obj)
时,会自动调用该方法。 - 例如,用 for 语句迭代一个对象时,会调用该对象的
__iter__()
方法,得到一个迭代器,然后开始迭代。
相关函数:
def iter(iterable)
- 功能:将一个可迭代对象,转换成迭代器。
- 实际上该函数并不懂如何转换,只是调用对象的
__iter__()
方法,从而得到一个迭代器。 - 例:
>>> iter('Hello') <iterator object at 0x7f19cd6e6810> >>> list(iter("Hello")) ['H', 'e', 'l', 'l', 'o']
# 生成器
生成器(generator)是一种特殊的迭代器,它会控制函数的执行进度。
如何创建?
- 在函数(或方法)中,使用关键字
yield
指定返回值,取代关键字return
。- 函数执行到
yield
语句时,函数会暂停执行,并返回一个生成器。 - 当用户获取生成器的下一个元素时,会得到
yield
语句的返回值,然后使得函数继续执行。 - 例:
>>> def fun1(): ... print('A') ... yield 1 ... print('B') ... yield 2 ... print('C') ... yield 3 ... >>> g = fun1() >>> g <generator object fun1 at 0x0000015736089D60> >>> next(g) A 1 >>> next(g) B 2 >>> next(g) C 3 >>> next(g) StopIteration
- 函数执行到
- 也可使用推导式的语法,创建生成器。
- 例:
>>> g = (i for i in range(5)) >>> g <generator object <genexpr> at 0x0000015736089890> >>> list(g) [0, 1, 2, 3, 4] >>> next(g) StopIteration
- 例:
- 在函数(或方法)中,使用关键字
为什么使用生成器?通常是为了提高 Python 程序的性能。
假设存在一个 iterable 对象,占用 100MB 内存空间。现在想将 iterable 中每个元素转换成 str 类型。
- 如果使用列表推导式
result = [str(i) for i in iterable]
,此时 result 也会占用 100MB 内存,加上 iterable 就占用 200MB 内存。 - 如果使用生成器推导式
result = (str(i) for i in iterable)
,此时 result 同时只获取一个元素,几乎不占用内存。
- 如果使用列表推导式
除了内存开销,还需要考虑时间开销。
- 假设遍历 iterable 对象的全部元素,耗时较久(比如几分钟)。等遍历完成之后,才开始处理这些元素,导致用户需要等一段时间才能看到处理结果。
result = [str(i) for i in iterable] # 这一步可能耗时较久 for i in result: print(i)
- 而使用生成器,可以每获取一个元素,就立即处理。从而更早地让用户看到处理结果,虽然并不能减少整体耗时。
result = (str(i) for i in iterable) # 这一步几乎没有耗时 for i in result: print(i)
- 假设遍历 iterable 对象的全部元素,耗时较久(比如几分钟)。等遍历完成之后,才开始处理这些元素,导致用户需要等一段时间才能看到处理结果。
综上,用户想创建一个可迭代对象时,有多种方式:
- 只实现
__iter__()
方法。此时只能被 for 迭代,不能被 next() 迭代。例如:for i in obj: print(i)
- 只实现
__next__()
方法。此时不能被 for 迭代,只能被 next() 迭代。例如:try: while True: print(next(obj)) except StopIteration: pass
- 实现
__iter__()
和__next__()
两个方法。此时可以被 for、next() 两种方式迭代。 - 用生成器进行迭代。优点如下:
- 代码更简单,不需要定义
__iter__()
和__next__()
两个方法,却可以被 for、next() 两种方式迭代。 - 减少 Python 程序的开销。
- 代码更简单,不需要定义
- 只实现
yield from
是一种语法糖,用于迭代一个对象,对其中每个元素执行一次 yield 。例:>>> def fun1(): ... yield from range(3) ... >>> list(fun1()) [0, 1, 2]
这相当于:
>>> def fun1(): ... for i in range(3): ... yield i ... >>> >>> list(fun1()) [0, 1, 2]
# 反向迭代
- 如何对一个对象,进行反向迭代?
- 建议让该对象实现
__reversed__()
方法,然后调用reversed()
函数。例:>>> class Number: ... def __iter__(self): # 用于正向迭代 ... n = 0 ... while n < 5: ... n += 1 ... yield n ... def __reversed__(self): # 用于反向迭代 ... n = 5 ... while n > 0: ... n -= 1 ... yield n ... >>> list(Number()) [1, 2, 3, 4, 5] >>> list(reversed(Number())) [4, 3, 2, 1, 0]
- 如果该对象没有实现
__reversed__()
方法,则只能转换成 list 对象,然后进行反向迭代:list(obj)[::-1] # 转换成 list 对象之后,可通过切片,获取反向序列 reversed(list(obj)) # 转换成 list 对象之后,存在 __reversed__() 方法
- 这种做法的缺点:需要遍历全部元素,缓存在一个 list 对象中,占用一些内存、时间。
- 建议让该对象实现
# 部分迭代
- 一个属于迭代器的对象,不一定实现了
__getitem__()
方法,因此不一定支持索引。 - 迭代器中的元素只能被逐个获取,如果用户只想获取部分元素,可采用以下几种方式:
- 手动判断当前是第几个元素。
>>> obj = range(5) >>> [item for index,item in enumerate(obj) if 3 <= index and index < 5] [3, 4]
- 转换成 list 对象之后,可通过切片访问。缺点是,转换时会占用一些内存、时间。
>>> obj = range(5) >>> list(obj)[3:5] [3, 4]
- 调用
itertools.islice()
函数,获取切片:>>> import itertools >>> itertools.islice(range(5), 3, 5) <itertools.islice object at 0x0000014CF029BDB0> >>> list(_) [3, 4]
- 手动判断当前是第几个元素。
# 并行迭代
如果目标对象有多个,用逗号分隔,则不会并行迭代。例如:
>>> for i in 'hello','world': ... print(i) ... hello world
因为 for 语句会将这多个对象,先组成一个元组,然后当作一个对象迭代。相当于:
for i in ('hello','world'): print(i)
可以用 zip() 函数,同时取出每个对象中的第 n 项元素,实现并行迭代。例如:
>>> for i in zip('hello','world'): ... print(i) ... ('h', 'w') ('e', 'o') ('l', 'r') ('l', 'l') ('o', 'd')
在列表推导式中,可以同时使用多个 for 语句,例如:
>>> [x+str(y) for x in 'ABC' for y in range(3)] ['A0', 'A1', 'A2', 'B0', 'B1', 'B2', 'C0', 'C1', 'C2']
但这并不是并行迭代,而是像多维数组一样逐层遍历,相当于:
a = [] for x in 'ABC': for y in range(3): list_item = x+str(y) a.append(list_item)
可以用 zip() 函数,实现并行迭代:
>>> iterables = 'ABC', range(3) >>> [i for i in zip(*iterables)] [('A', 0), ('B', 1), ('C', 2)]
如果只迭代一个 iterable 对象,但希望每次获取多个元素,则可采用以下方式:
>>> iterable = iter(range(6)) >>> iterables = [iterable] * 4 # 将迭代器拷贝多份,这样并行迭代时,会累加迭代进度 >>> [i for i in zip(*iterables)] # 这里每次获取 4 个元素。如果 iterables 中任一对象为空,则会停止迭代。因此可能遗漏一些元素,没有迭代 [(0, 1, 2, 3)]
# 描述器
描述器用于描述一个实例变量,与
@property
类似。- 不过,描述器只能以类变量的形式创建,然后以实例变量的形式访问。
如果一个对象,实现了以下三个方法之一,则属于描述器(descriptor)。
def __get__(self, instance, instance_type) # 读取该对象的值时,会自动调用该方法 def __set__(self, instance, value) # 赋值给该对象时,会自动调用该方法 def __delete__(self, instance) # 删除该对象时,会自动调用该方法
例:读取描述器
>>> class Number: ... def __get__(self, instance, instance_type): ... print(instance) ... print(instance_type) ... return 0 ... >>> class Test: ... n = Number() # 创建类变量,指向一个描述器 ... >>> Test().n # 执行成员运算符 . 时,会发现类变量存在 __get__() 方法,于是自动调用它,并传入参数:当前对象 Test() 、当前类 Test <__main__.Test object at 0x000001C922BBF9D0> <class '__main__.Test'> 0 >>> Test.n # 如果通过类名,访问类变量,则传入的 instance 参数为 None None <class '__main__.Test'> 0
例:读、写描述器
>>> class Number: ... def __init__(self): ... self.n = 0 ... def __get__(self, instance, instance_type): ... if instance is None: ... return self ... else: ... return self.n ... def __set__(self, instance, value): ... if isinstance(value, int): ... self.n = value ... else: ... raise TypeError('Expected value to be an int') ... >>> class Test: ... n = Number() ... >>> Test().n 0 >>> Test().n = 1 >>> Test().n = '' TypeError: Expected value to be an int >>> Test.n <__main__.Number object at 0x000001C922BC5730>
例:用描述器实现一个装饰器,用于将某个实例方法转换成实例变量
>>> class FixedProperty: ... def __init__(self, method): ... self.method = method # 存储原本的实例方法 ... def __get__(self, instance, instance_type): ... if instance is None: ... return self ... else: ... value = self.method(instance) # 调用一次实例方法,得到它的返回值 ... setattr(instance, self.method.__name__, value) # 给 instance 添加一个实例变量,名称与 method 相同,因此覆盖原本的 method ... return value ... >>> import random >>> class Test: ... @FixedProperty ... def num(self): ... return random.randint(1, 10) ... >>> t = Test() >>> t.num 5 >>> t.num # 该实例变量的返回值是固定的。如果删除上面的 setattr() 一行代码,则返回值不固定,与 @property 装饰器的效果相同 5
例:用描述器实现一个装饰器,在赋值时进行类型检查
>>> class TypeCheck: ... expected_type = type(None) ... def __init__(self, attr, **kwargs): # 这里使用 **kwargs ,因此子类调用该方法时,可以传入任意个关键字参数 ... self.attr = attr ... for k, v in kwargs.items(): ... setattr(self, k, v) ... def __set__(self, instance, value): ... if not isinstance(value, self.expected_type): ... raise TypeError('Expected {} to be of data type: {}'.format(self.attr, self.expected_type)) ... # setattr(instance, self.attr, value) # 这行代码会调用当前 __set__() 方法,导致无限循环 ... instance.__dict__[self.attr] = value # 为了避免无限循环,应该采用这种方式赋值 ... >>> class String(TypeCheck): ... expected_type = str ... >>> class Price(TypeCheck): ... expected_type = (int, float) ... >>> class Book: ... name = String('name') ... price = Price('price') ... def __init__(self, name, price): ... self.name = name ... self.price = price ... >>> Book('test', '') TypeError: Expected price to be of data type: (<class 'int'>, <class 'float'>) >>> Book('test', 10) <__main__.Book object at 0x000001B091F5E520> >>> _.name = 10 TypeError: Expected name to be of data type: <class 'str'>
进一步地,在赋值时检查范围:
>>> class RangeCheck(TypeCheck): ... def __init__(self, attr, **kwargs): ... if 'min_value' not in kwargs: ... raise TypeError('Expected the argument: min_value') ... if 'max_value' not in kwargs: ... raise TypeError('Expected the argument: max_value') ... if not kwargs['min_value'] <= kwargs['max_value']: ... raise ValueError('Expected two arguments: min_value <= max_value') ... super().__init__(attr, **kwargs) ... def __set__(self, instance, value): ... if value < self.min_value or value > self.max_value: ... raise TypeError('Expected range: {} <= {} <= {}'.format(self.min_value, self.attr, self.max_value)) ... super().__set__(instance, value) ... >>> class LimitedPrice(Price, RangeCheck): ... pass ... >>> class Book: ... name = String('name', length=10) ... price = LimitedPrice('price', min_value=1, max_value=100) ... def __init__(self, name, price): ... self.name = name ... self.price = price ... >>> Book('test', 10) <__main__.Book object at 0x000001B091F33BE0> >>> _.price = 200 TypeError: Expected range: 1 <= price <= 100
什么时候进行类型检查?
- Python 的初衷是一门动态语言,不对变量进行类型检查。
- 进行类型检查,缺点是使得代码更繁琐、编程工作量更大。优点是保证变量的取值符合预期,减少代码的故障风险。
- 折中的做法是,程序通过 API 与外部交互时,不信任外部输入的变量,因此进行类型检查。程序内部,各个函数相互调用时,不进行类型检查。