python基础学习-魔术方法
特殊属性
属性 | 含义 |
---|---|
__name__ |
类、函数、方法等的名字 |
__module__ |
类定义所在的模块名 |
__class__ |
对象或类所属的类 |
__bases__ |
类的基类的元组,顺序为它们在基类列表中出现的顺序 |
__doc__ |
类、函数的文档字符串,如果没有定义则为None |
__mro__ |
类的mro , class.mro() 返回的结果保存在__mro__ 中 |
__dict__ |
类或实例的属性,可写的字典 |
查看属性
方法 | 意义 |
---|---|
__dir__ |
返回类或者对象的所有成员名称列表。dir() 函数就是调用__dir__() 。如果提供__dir__() ,则返回属性的列表,否则会尽量从__dict__ 属性中收集信息。 |
如果dir([obj])
参数obj
包含方法__dir__()
,该方法将被调用。如果参数obj
不包含__dir__()
,该方法将最大限度地收集参数信息。
dir()
对于不同类型的对象具有不同的行为:
如果对象是模块对象,返回的列表包含模块的属性名。
如果对象是类型或者类对象,返回的列表包含类的属性名,及它的基类的属性名。
否则,返回列表包含对象的属性名,它的类的属性名和类的基类的属性名。
1 | # animal.py |
魔法方法 ***
- 分类
- 创建、初始化与销毁
__init__
与__del__
- hash
- bool
- 可视化
- 运算符重载
- 容器和大小
- 可调用对象
- 上下文管理
- 反射
- 描述器
- 其他杂项
- 创建、初始化与销毁
hash
方法 | 意义 |
---|---|
__hash__ |
内建函数hash() 调用的返回值,返回一个整数。如果定义了这个方法,该类的实例就可hash。 |
1 | class A: |
上例中set
为什么不能剔除相同的key
?
1 | class A: |
方法 | 意义 |
---|---|
__eq__ |
对应== 操作符,判断2个对象是否相等,返回bool 值 |
__hash__
方法只是返回一个hash值作为set
的key
,但是去重
,还需要__eq__
来判断2个对象是否相等。
hash值相等,只是hash冲突,不能说明两个对象是相等的。
因此,一般来说提供__hash__
方法是为了作为set
或者dict
的key
,所以去重
要同时提供__eq__
方法。
不可hash对象isinstance(p1, collections.Hashable)
一定为False。
去重
需要提供__eq__
方法。
思考:
list类实例为什么不可hash?
练习
设计二维坐标类Point,使其成为可hash类型,并比较2个坐标的实例是否相等?
list类实例为什么不可hash
源码中有一句__hash__ = None
,也就是如果调用__hash__()
相当于None()
,一定报错。所有类都继承object
,而这个类是具有__hash__()
方法的,如果一个类不能被hash,就把__hash__
设置为None
。
练习参考
1 | from collections import Hashable |
bool
方法 | 意义 |
---|---|
__bool__ |
内建函数bool() ,或者对象放在逻辑表达式的位置,调用这个函数返回布尔值。没有定义__bool__() ,就找__len__() 返回长度,非0为真。如果__len__() 也没有定义,那么所有实例都返回真。 |
1 | class A: pass |
可视化
方法 | 意义 |
---|---|
__repr__ |
内建函数repr() 对一个对象获取字符串表达。调用__repr__ 方法返回字符串表达,如果__repr__ 也没有定义,就直接返回object 的定义就是显示内存地址信息 |
__str__ |
str() 函数、内建函数format() 、print() 函数调用,需要返回对象的字符串表达。如果没有定义,就去调用__repr__ 方法返回字符串表达,如果__repr__ 没有定义,就直接返回对象的内存地址信息 |
__bytes__ |
bytes() 函数调用,返回一个对象的bytes 表达,即返回bytes 对象 |
1 | class A: |
运算符重载
operator
模块提供以下的特殊方法,可以将类的实例使用下面的操作符来操作
运算符 | 特殊方法 | 含义 |
---|---|---|
<, <=, ==, >, >=, != | __lt__ , __le__ , __eq__ , __gt__ , __ge__ , __ne__ |
比较运算符 |
+, -, *, /, %, //, **, divmod | __add__ , __sub__ , __mul__ , __truediv__ , __mod__ , __floordiv__ , __pow__ , __divmod__ |
算术运算符,移位、位运算也有对应的方法 |
+=, -=, *=, /=, %=, //=, **= | __iadd__ , __isub__ , __imul__ , __itruediv__ , __imod__ , __ifloordiv__ , __ipow__ |
1 | class A: |
练习:
完成Point
类设计,实现判断点相等的方法,并完成向量的加法。
在直角坐标系里面,定义原点为向量的起点。两个向量和与差的坐标分别等于这两个向量相应坐标的和与差若向量的表示为(x,y)
形式,A(X1, Y1) B(X2, Y2)
,则A+B=(X1+X2, Y1+Y2), A-B=(X1-X2, Y1-Y2)
1 | class Point: |
运算符重载应用场景
往往是用面向对象实现的类,需要做大量的运算,而运算符是这种运算在数学上最常见的表达方式。例如,上例中的对+
进行了运算符重载,实现了Point
类的二元操作,重新定义为Point + Point
。
提供运算符重载,比直接提供加法方法要更加适合该领域内使用者的习惯。
int
类,几乎实现了所有操作符,可以作为参考。
@functools.total_ordering 装饰器
__lt__
, __le__
, __eq__
, __gt__
, __ge__
是比较大小必须实现的方法,但是全部写完太麻烦,使用@functools.total_ordering
装饰器就可以大大简化代码。
但是要求__eq__
必须实现,其它方法__lt__
, __le__
, __gt__
, __ge__
实现其一
1 | from functools import total_ordering |
上例中大大简化代码,但是一般来说比较实现等于或者小于方法也就够了,其它可以不实现,所以这个装饰器只是看着很美好,且可能会带来性能问题,建议需要什么方法就自己创建,少用这个装饰器。
1 | class Person: |
容器相关方法
方法 | 意义 |
---|---|
__len__ |
内建函数len() ,返回对象的长度(>=0的整数),如果把对象当做容器类型看,就如同list或者dict。bool() 函数调用的时候,如果没有__bool__() 方法,则会看__len__() 方法是否存在,存在返回非0为真 |
__iter__ |
迭代容器时,调用,返回一个新的迭代器对象 |
__contains__ |
in 成员运算符,没有实现,就调用__iter__ 方法遍历 |
__getitem__ |
实现self[key] 访问。序列对象,key 接受整数为索引,或者切片。对于set和dict,key为hashable。key不存在引发KeyError异常 |
__setitem__ |
和__getitem__ 的访问类似,是设置值的方法 |
__missing__ |
字典或其子类使用__getitem__() 调用时,key不存在执行该方法 |
1 | class A(dict): |
思考
为什么空字典、空字符串、空元组、空集合、空列表等可以等效为False?
练习
将购物车类改造成方便操作的容器类
1 | class Cart: |
可调用对象
Python中一切皆对象,函数也不例外。
1 | def foo(): |
函数即对象,对象foo
加上()
,就是调用对象的__call__()
方法
可调用对象
方法 | 意义 |
---|---|
__call__ |
类中定义一个该方法,实例就可以像函数一样调用 |
可调用对象:定义一个类,并实例化得到其实例,将实例像函数一样调用。
1 | class Point: |
练习:
定义一个斐波那契数列的类,方便调用,计算第n项
1 | class Fib: |
上例中,增加迭代的方法、返回容器长度、支持索引的方法
1 | class Fib: |
可以看出使用类来实现斐波那契数列也是非常好的实现,还可以缓存数据,便于检索。
上下文管理
文件IO操作可以对文件对象使用上下文管理,使用with...as
语法
1 | with open('test') as f: |
依照上例写一个自己的类,实现上下文管理
1 | class Point: |
提示属性错误,没有__exit__
,看来需要这个属性
上下文管理对象
当一个对象同时实现了__enter__()
和__exit__()
方法,它就属于上下文管理的对象
方法 | 意义 |
---|---|
__enter__ |
进入与此对象相关的上下文 。如果存在该方法,with 语法会把该方法的返回值绑定到as 子句中指定的变量上 |
__exit__ |
退出与此对象相关的上下文 |
1 | class Point: |
实例化对象的时候,并不会调用enter
,进入with
语句块应用__enter__
方法,然后执行语句体,最后离开with
语句块的时候,调用__exit__
方法。
with
可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。
上下文管理的安全性
看看异常对上下文的影响
1 | class Point: |
可以看出在enter
和exit
照样执行,上下文管理是安全的。
极端的例子
调用sys.exit()
,它会退出当前解释器。
打开Python解释器,在里面敲入sys.exit()
,窗口直接关闭了。也就是说碰到这一句,Python运行环境直接退出了。
1 | import sys |
从执行结果来看,依然执行了__exit__
函数,哪怕是退出Python运行环境。
说明上下文管理很安全
with语句
1 | class Point: |
问题在于__enter__
方法上,它将自己的返回值赋给f
。修改上例
1 | class Point: |
__enter__
方法返回值就是上下文中使用的对象,with
语法会把它的返回值赋给as
子句的变量。
__enter__
方法和__exit__
方法的参数
__enter__
方法没有其他参数。
__exit__
方法有3个参数:
__exit__(self, exc_type, exc_value, traceback)
这三个参数都与异常有关。
如果该上下文退出时没有异常,这3个参数都为None。
如果有异常,参数意义如下
exc_type
,异常类型
exc_value
,异常的值
traceback
,异常的追踪信息
__exit__
方法返回一个等效True的值,则压制异常;否则,继续抛出异常
1 | class Point: |
练习
为加法函数计时
方法1,使用装饰器显示该函数的执行时长
方法2,使用上下文管理方法来显示该函数的执行时长
1 | import time |
装饰器实现
1 | import time |
上下文实现
1 | import time |
另一种实现,使用可调用对象实现
1 | import time |
根据上面的代码,能不能把类当做装饰器用?
1 | import time |
思考
如何解决文档字符串问题?
方法一
直接修改__doc__
1 | class TimeIt: |
方法二
使用functools.wraps
函数
1 | import time |
上面的类即可以用在上下文管理,又可以用做装饰器
上下文应用场景
增强功能
在代码执行的前后增加代码,以增强其功能。类似装饰器的功能。
资源管理
打开了资源需要关闭,例如文件对象、网络连接、数据库连接等
权限验证
在执行代码之前,做权限的验证,在
__enter__
中处理
contextlib.contextmanager
contextlib.contextmanager
它是一个装饰器实现上下文管理,装饰一个函数,而不用像类一样实现__enter__
和__exit__
方法。
对下面的函数有要求,必须有yield
,也就是这个函数必须返回一个生成器,且只有yield
一个值。也就是这个装饰器接收一个生成器对象作为参数。
1 | import contextlib |
f
接收yield
语句的返回值
上面的程序看似不错,但是,增加一个异常试一试,发现不能保证exit
的执行,怎么办?
增加try finally
。
1 | import contextlib |
上例这么做有什么意义呢?
当yield
发生处为生成器函数增加了上下文管理。这是为函数增加上下文机制的方式。
- 把
yield
之前的当做__enter__
方法执行 - 把
yield
之后的当做__exit__
方法执行 - 把
yield
的值作为__enter__
的返回值
1 | import contextlib |
总结
如果业务逻辑简单可以使用函数加contextlib.contextmanager
装饰器方式,如果业务复杂,用类的方式加__enter__
和__exit__
方法方便。
反射概述
运行时,区别于编译时,指的是程序被加载到内存中执行的时候。
反射,reflection,指的是运行时获取类型定义信息。
一个对象能够在运行时,像照镜子一样,反射出其类型信息。
简单说,在Python中,能够通过一个对象,找出其type
、class
、attribute
或method
的能力,称为反射或者自省。
具有反射能力的函数有:type()
、isinstance()
、callable()
、dir()
、getattr()
反射相关的函数和方法
需求
有一个Point类,查看它实例的属性,并修改它。动态为实例增加属性
1 | class Point: |
上例通过属性字典__dict__
来访问对象的属性,本质上也是利用的反射的能力。
但是,上面的例子中,访问的方式不优雅,Python提供了内置的函数。
内建函数 | 意义 |
---|---|
getattr(object,name[,default]) |
通过name返回object的属性值。当属性不存在,将使用default返回,如果没有default,则抛出AttributeError。name必须为字符串 |
setattr(object,name,value) |
object的属性存在,则覆盖,不存在,新增 |
hasattr(object,name) |
判断对象是否有这个名字的属性,name必须为字符串 |
用上面的方法来修改上例的代码
1 | class Point: |
思考
这种动态增加属性的方式和装饰器修饰一个类、Mixin方式的差异?
这种动态增删属性的方式是运行时改变类或者实例的方式,但是装饰器或Mixin都是定义时就决定了,因此反射能力具有更大的灵活性。
练习
命令分发器,通过名称找对应的函数执行。
思路:名称找对象的方法
1 | class Dispatcher: |
上例中使用getattr
方法找到对象的属性的方式,比自己维护一个字典来建立名称和函数之间的关系的方式好多了。
反射相关的魔术方法
__getattr__()
、__setattr__()
、__delattr__()
这三个魔术方法,分别测试
__getattr__()
1 | class Base: |
一个类的属性会按照继承关系找,如果找不到,就会执行__getattr__()
方法,如果没有这个方法,就会抛出AttributeError
异常表示找不到属性。
查找属性顺序为:
instance.__dict__ --> instance.__class__.__dict__ --> 继承的祖先类(直到object)的__dict__ --找不到--> 调用__getattr__()
__setattr__()
1 | class Base: |
实例通过.
点设置属性,如同self.x = x
,就会调用__setattr__()
,属性要加到实例的__dict__
中,就需要自己完成。
1 | class Point(Base): |
__setattr__()
方法,可以拦截对实例属性的增加、修改操作,如果要设置生效,需要自己操作实例的__dict__
。
__delattr__()
1 | class Point: |
可以阻止通过实例删除属性的操作。但是通过类依然可以删除属性。
__getattribute__
1 | class Base: |
实例的所有的属性访问,第一个都会调用__getattribute__
方法,它阻止了属性的查找,该方法应该返回(计算后的)值或者抛出一个AttributeError
异常。
它的return值将作为属性查找的结果。如果抛出AttributeError
异常,则会直接调用__getattr__
方法,因为表示属性没有找到。
1 | class Base: |
__getattribute__
方法中为了避免在该方法中无限的递归,它的实现应该永远调用基类的同名方法以访问需要的任何属性,例如object.__getattribute__(self, name)
。
注意,除非你明确地知道__getattribute__
方法用来做什么,否则不要使用它。
总结
魔术方法 | 意义 |
---|---|
__getattr__() |
当通过搜索实例、实例的类及祖先类查不到属性,就会调用此方法 |
__setattr__() |
通过. 访问实例属性,进行增加、修改都要调用它 |
__delattr__() |
当通过实例来删除属性时调用此方法 |
__getattribute__ |
实例所有的属性调用都从这个方法开始 |
属性查找顺序:
实例调用__getattribute__() -> instance.__dict__ -> instance.__class__.__dict__ -> 继承的祖先类(直到object)的__dict__ -> 调用__getattr__()
笔记
1 | python的名词空间指模块或作用域 |
hash
用hash就是为了O1的时间复杂度。缓存的目的是再查,缓冲是为了匹配生产者与消费者的速度
1 | from collections import Hashable |
eq
1 | a == b # 相当于下面。这就是二元操作符如何等价到一个方法 |
练习
1 | # 设计二维坐标类Point,使其成为可hash类型,并比较2个坐标的实例是否相等? |
bool
1 | class Point: |
可视化
1 | class Point: |
运算符重载
运算符重载可以提高类的可用性
1 | class A: |
练习
完成Point
类设计,实现判断点相等的方法,并完成向量的加法
1 | # 答案1 |
容器相关方法
练习
将购物车类改造成方便操作的容器类
1 | class Item: |
实现一个字典
1 | class MyDict(dict): # 直接继承 |
可调用对象
1 | def foo(): |
练习
定义一个斐波那契数列
1 | # 答案1 |
上下文管理
通过上下文管理,我们可以只管打开,其他的不用关心,python解释器会为我们做,python会做一些自动化操作。你这段代理的上文和下文就是上下文
1 | # with open ('test') as f: |
练习
为加法函数计时
- 使用装饰器显示该函数的执行时长
- 使用上下文管理显示该函数的执行时长
1 | import datetime |
contextlib.contextmanager
yield返回单值,如yield x,y就是返回单值,x,y也是单值,是被封装后的单值
1 | import contextlib |
total_ordering
1 | from functools import total_ordering |
反射
理解反射先要理解什么是编译时什么是运行时,python是动态语言,更多的表现是在运行时,运行时指解释器加载代码后是什么状态,它更多的在意在执行的过程中是什么情况,执行过程中就是加载到内存中了。
反射指在运行时,它能够通过对象找到对象相关的类型信息,类型信息指我是什么类型,我有什么属性,因为有些类型在方法上,所以就要在运行时在类上找到他类型相关的所有信息。找信息与反射的关系是从它自身反射出了它的所有类型信息,这个过程就叫反射,通过这种机制,就可以在运行时掌握所有类型信息。一个对象可以在运行时反射出自己与类型相关的所有信息,这就是反射机制。反射也称为自省
1 | class A(): |
在运行时需要添加属性,就需要上面的方法了。装饰器和Mixin是在编译期要定义好的,不能动态改变
练习
命令分发器
1 | def dispatcher(): |
反射相关的魔术方法
1 | class A: |
__getattribute__
1 | class Base: |
python基础学习-魔术方法