inspect:获取python对象的有用信息
有些时候,我们需要得到一个对象的某些属性,我们最常用的就是通过type来查看该对象的类型,或者使用dir来查看该对象具有哪些属性。但是python提供了一个非常好的模块:inspect
,来帮助我们更好地获取对象的属性,下面就来看看该模块支持哪些方法。
这里指的是种类,不是类型。
关于模块,我们知道python中存在模块和包两个概念,但是其实在底层它们之间并没有区分的那么明显。单独的py文件可以叫一个模块,其实也可以把包看成是一个模块,甚至是空目录也是一个模块,在CPython中,它们都对应PyModuleObject(如果你了解python解释器底层的话)
。
只不过为了更好地区分,我们把单独的py文件称之为模块,把存放多个py文件的目录称之为包。任何一个py文件都有一个__file__
属性,也就是该文件的绝对路径。但是包就不一定了,如果包里面存在__init__.py
文件的话,那么这个包的__file__
就是内部__init__.py
文件的绝对路径,但如果不存在__init__.py
文件,那么会报错,提示没有__file__
属性,但是在新版本python3.8中,改为不报错,而是返回None。除了__file__
,还有一个__path__
,任何的包都有__path__
,不管内部有没有__init__.py
文件,返回的结果是该包的绝对路径,以列表的形式,但是py文件没有__path__
。
我们具体操作一波
import inspect
import tornado # tornado是一个目录
from tornado import web # web是一个py文件
import 一个空目录
# inspect.ismodule可以查看该对象是否为模块
print(inspect.ismodule(tornado)) # True
print(inspect.ismodule(web)) # True
print(inspect.ismodule(一个空目录)) # True
"""
我们看到目录、py文件都是模块,甚至空目录也是一个模块
"""
# 得到该py文件的绝对路径
print(web.__file__) # C:\python38\lib\site-packages\tornado\web.py
# 该目录内部__init__.py文件的绝对路径
print(tornado.__file__) # C:\python38\lib\site-packages\tornado\__init__.py
# 但是空目录则没有__file__,因为它内部没有__init__.py文件
# 低版本会报错,比如3.6,但是3.8返回None
print(一个空目录.__file__) # None
# 任何一个包都有__path__,返回该包的绝对路径,以列表的形式
print(tornado.__path__) # ['C:\\python38\\lib\\site-packages\\tornado']
# 但是我们看到如果没有__init__.py的话,那么返回的还有些不一样,返回的是一个_NamespacePath
print(一个空目录.__path__) # _NamespacePath(['D:\\satori\\一个空目录', 'D:\\satori\\一个空目录'])
try:
# 调用一个py文件的__path__会报错,提示我们没有该属性
print(web.__path__)
except AttributeError as e:
print(e) # module 'tornado.web' has no attribute '__path__'
import inspect
print(inspect.isclass(int)) # True
print(inspect.isclass(object)) # True
print(inspect.isclass(type)) # True
class A:
...
print(inspect.isclass(A)) # True
print(inspect.isclass(123)) # False
方法可以简单认为是类里面定义的函数,其实更准确的说是用实例获取的函数,不能是通过类获取的。怎么理解呢?都说实例在调用类里面函数的时候会自动将实例本身传给self,那么为什么会自动传递呢?其实python中的函数在CPython中对应的是PyFunctionObject,如果是方法的话,那么会对PyFunctionObject进行封装,得到PyMethodObject。PyMethodObject里面有一个im_self属性,就是python中的self,如果是实例调用,那么底层会自动将im_self(该实例对象)
作为第一个参数传进去。至于实例调用的时候为什么会自动传递,则是通过描述符的方式。是的,你没有看错,python底层使用的是描述符,有兴趣可以自己去研究一下。所以类去获取得到的就是普通的函数,实例获取得到的才叫方法,因为实例在获取的时候会将函数包装成方法。
import inspect
class A:
def foo(self):
pass
# 我们说实例去调用才叫做方法,类调用的不算
print(inspect.ismethod(A.foo)) # False
print(inspect.ismethod(A().foo)) # True
描述符有两种,分别是非数据描述符和数据描述符。
非数据描述符:内部实现了__get__方法,但是没有实现__set__方法
数据描述符:内部实现了__set__方法或者实现了__delete__方法
import inspect
class A:
def __get__(self, instance, owner):
pass
class B:
def __get__(self, instance, owner):
pass
def __set__(self, instance, value):
pass
# 判断是否是非数据描述符,我不知道名字为什么叫ismethoddescriptor
# 当然传入的要是类的实例对象,至于类、函数、方法则肯定不是描述符
print(inspect.ismethoddescriptor(A())) # True
print(inspect.ismethoddescriptor(B())) # False
# A内部实现了__get__,所以A()是非数据描述符
# B内部实现了__set__,所以B()是数据描述符
# 判断是否是数据描述符,这个名字就比较形象了
print(inspect.isdatadescriptor(A())) # False
print(inspect.isdatadescriptor(B())) # True
import inspect
def foo():
pass
class A:
def foo(self):
pass
print(inspect.isfunction(foo)) # True
print(inspect.isfunction(A.foo)) # True
print(inspect.isfunction(A().foo)) # False
"""
我们说类获取才是函数,实例获取会包装成方法
"""
# 但如果它们被装饰器装饰了呢?
class B:
@property
def f1(self):
pass
@staticmethod
def f2():
pass
@classmethod
def f3(cls):
pass
print(inspect.isfunction(B.f1)) # False
print(inspect.isfunction(B().f1)) # False
"""
我们看到被property装饰之后,无论谁去调用,都不是函数了
很好理解,因为B.f1得到的就是一个property对象
而B().f1直接拿到了返回值,这里是一个None,当然也不是函数。
当然如果你返回的就是一个函数,那么inspect.isfunction(B().f1)也是正确的,不过此时判断的就不再是f1了,而是f1的返回值
"""
print(inspect.isfunction(B.f2)) # True
print(inspect.isfunction(B().f2)) # True
"""
被staticmethod装饰之后,如果你自己手动实现过staticmethod的话
那么你会发现就等同于没有被staticmethod装饰的B.f2
因为它不需要自动传递参数,不需要包装成方法,所以是一个函数,无论是类获取还是实例获取
"""
print(inspect.isfunction(B.f3)) # False
print(inspect.isfunction(B().f3)) # False
"""
但是我们看到被classmethod装饰之后,就不再是函数了
没错,因为此时类去调用的时候会自动将自身传递给参数cls,那么这和实例调用传递self是一个道理
只不过类传递给cls这是我们自己通过classmethod实现的,而实例传递给self是python底层自动实现的,但是它们的本质都是一样的
都被包装成了方法,此时无论是类获取还是实例获取,得到的都是方法,并且调用的时候第一个参数cls都是类本身
"""
# 显然ismethod返回的结果是正确的,因为它们是方法
print(inspect.ismethod(B.f3)) # True
print(inspect.ismethod(B().f3)) # True
import inspect
x = (_ for _ in [1, 2, 3])
def foo():
yield 123
print(inspect.isgenerator(x)) # True
print(inspect.isgenerator(foo)) # False
print(inspect.isgenerator(foo())) # True
"""
foo是一个生成器函数,它不是生成器,它无法调用__next__产出值
只有在调用foo()的时候,返回的才是一个生成器
"""
import inspect
x = (_ for _ in [1, 2, 3])
def foo():
yield 123
print(inspect.isgeneratorfunction(x)) # False
print(inspect.isgeneratorfunction(foo)) # True
print(inspect.isgeneratorfunction(foo())) # False
import inspect
async def foo():
pass
# 使用def定义的是函数,使用async def定义的是协程函数
# 协程函数调用之后得到的就是一个协程
print(inspect.iscoroutine(foo())) # True
# 另外如果async def定义的协程函数里面出现了yield,那么就不叫协程函数了,而叫做异步生成器函数
# 我们后面会说
import inspect
async def foo():
pass
print(inspect.iscoroutinefunction(foo)) # True
print(inspect.iscoroutinefunction(foo())) # False
import inspect
# 异步生成器,首先是一个生成器,而且还要是异步的
# 这个异步就体现在需要是使用async定义的协程函数
# 当然组合起来就不是生成器、也不是协程函数,而是异步生成器函数了,进行调用会得到异步生成器
async def foo():
yield 123
yield 456
yield 789
# 判断是否是异步生成器
print(inspect.isasyncgen(foo())) # True
# 多提一句,那我要如何获取里面的值呢?
# 显然对于异步生成器没有__next__方法,但是它有__anext__,这里的a指的就是async
# 但是这样获取不到值
print(foo().__anext__()) # <async_generator_asend object at 0x000002457F015FC0>
# 其实对于协程或者异步生成器来讲
# 如果想要运行,必须要扔到事件循环里面去
# 而运行协程则需要使用官方提供了asyncio这个库,当然tornado也是可以的,因为tornado5.0之后底层的事件循环使用的就是asyncio
# 当然如果想运行一个async def定义的协程函数或者异步生成器,必须也要在async def里面定义的协程里面运行
async def main1():
f = foo()
# 另外对于协程或异步生成器来说,无论是yield还是return,我们必须要使用await关键字才能获取值
# 而await关键字只能出现在async def定义的协程函数中
print(await f.__anext__())
print(await f.__anext__())
print(await f.__anext__())
async def main2():
f = foo()
# 当然还有更加pythonic的方法,就是使用async for
# 同理,异步的上下文管理则是async with
async for _ in f:
print(_)
if __name__ == '__main__':
import asyncio
asyncio.run(main1())
"""
123
456
789
"""
asyncio.run(main2())
"""
123
456
789
"""
"""
关于python中的协程,个人觉得学习起来还是有些费劲的
尤其是早期python没有协程,是通过yield和yield from来进行模拟的
如果把协程和yield混合起来使用,确实让人感到困惑。
但并不是说yield不重要,它非常重要,理解yield和yield from能让你更快速理解python中的协程(async 和 await)
只是希望不要把async和yield一起混用,如果是yield的话,那么不要让它出现在async def定义的协程中,就把它当成是普通的生成器来使用即可
"""
# 另外python中的协程是一个稍微复杂的概念,以及asyncio的使用,这里不可能全部讲清楚
# 有兴趣的话可以参考我的这一篇博客:https://www.cnblogs.com/traditional/p/11828780.html
import inspect
async def foo():
yield 123
yield 456
yield 789
print(inspect.isasyncgenfunction(foo)) # True
print(inspect.isasyncgenfunction(foo())) # False
另外关于协程、协程函数,生成器、生层器函数等等,到底带不带函数二字,其实我们也不会区分的这么明显。比如async def是定义一个协程函数,但是我们平常都会说定义一个协程,所以心里面清楚就行。
如果不了解python中的协程的话,那么这个可能有些难理解。可awaitable,其实主要体现在该对象是否可以使用await关键字。
import inspect
async def foo():
return 123
# 我们说一个协程都是可awaitable的
print(inspect.isawaitable(foo())) # True
async def bar():
yield 123
# 但是异步生成器则不行
print(inspect.isawaitable(bar())) # False
# 而异步生成器在使用__anext__获取值的时候是可awaitable的
print(inspect.isawaitable(bar().__anext__())) # True
class A:
def __await__(self):
return 123
# 如果我们自定义的类实现了__await__魔法方法的话,那么这个类的实例对象则也是可awaitable的
print(inspect.isawaitable(A())) # True
这个traceback是发生错误的时候产生的回溯栈。
import inspect
import sys
try:
1 / 0
except ZeroDivisionError:
_, _, tb = sys.exc_info()
print(inspect.istraceback(tb)) # True
关于python中的栈帧,是一个比较复杂的话题,如果扯得话,又能扯很远。所以干脆不扯了,可以看我的其它博客,专门介绍python解释器的。
import inspect
frame = None
def foo():
global frame
# 使用inspect.currentframe()可以获取当前函数的栈帧
frame = inspect.currentframe()
# 此时为None,所以不是
print(inspect.isframe(frame)) # False
# 当执行完函数之后
foo()
print(inspect.isframe(frame)) # True
字节码对象就是python编译之后的结果,就是pyc文件里面存储的内容。
import inspect
def foo():
pass
# 调用函数的__code__方法,可以拿到函数的字节码
# 同理对于生成器、协程、异步生成器来说也是一样的
print(inspect.iscode(foo.__code__)) # True
import inspect
# 要么是builtin里面的函数,要么是里面的类创建的实例对象的某个方法
print(inspect.isbuiltin(globals)) # True
print(inspect.isbuiltin((1).bit_length)) # True
print(inspect.isbuiltin(__name__)) # False
如果是抽象类,那么这个类的元类要是abc.ABCMeta,并且内部要有一个抽象方法,也就是要被abc.abstractmethod装饰的方法
import inspect
import abc
class A(metaclass=abc.ABCMeta):
@abc.abstractmethod
def foo(self):
pass
print(inspect.isabstract(A)) # True
# 但是ABCMeta本身不是抽象类
print(inspect.isabstract(abc.ABCMeta)) # False
我们之前获取对象的属性是通过dir的方式,但是dir返回的是属性的名字,而通过inspect可以同时获取属性名和值。
import inspect
from pprint import pprint
# 返回的是一个列表,里面是多个属性名和属性值组成的二元tuple
pprint(inspect.getmembers(1))
"""
[('__abs__', <method-wrapper '__abs__' of int object at 0x00007FFCAC20C6A0>),
('__add__', <method-wrapper '__add__' of int object at 0x00007FFCAC20C6A0>),
('__and__', <method-wrapper '__and__' of int object at 0x00007FFCAC20C6A0>),
('__bool__', <method-wrapper '__bool__' of int object at 0x00007FFCAC20C6A0>),
('__ceil__', <built-in method __ceil__ of int object at 0x00007FFCAC20C6A0>),
('__class__', <class 'int'>),
...
...
]
"""
# 其实如果把里面元组的第一个元素取出来的话,会发现和dir返回的结果是一样的
# 转成集合,忽略掉顺序
print(set([_[0] for _ in inspect.getmembers(1)]) == set(dir(1))) # True
for _ in inspect.getmembers(10):
if _[0] == "__add__":
print(_[1](20)) # 30
import inspect
from pprint import pprint
class A: pass
# 返回的是多个Attribute对象组成的列表,注意:必须要传递一个类才可以
pprint(inspect.classify_class_attrs(A))
"""
[Attribute(name='__class__', kind='data', defining_class=<class 'object'>, object=<class 'type'>),
Attribute(name='__delattr__', kind='method', defining_class=<class 'object'>, object=<slot wrapper '__delattr__' of 'object' objects>),
Attribute(name='__dict__', kind='data', defining_class=<class '__main__.A'>, object=<attribute '__dict__' of 'A' objects>),
Attribute(name='__dir__', kind='method', defining_class=<class 'object'>, object=<method '__dir__' of 'object' objects>),
Attribute(name='__doc__', kind='data', defining_class=<class '__main__.A'>, object=None),
Attribute(name='__eq__', kind='method', defining_class=<class 'object'>, object=<slot wrapper '__eq__' of 'object' objects>),
Attribute(name='__format__', kind='method', defining_class=<class 'object'>, object=<method '__format__' of 'object' objects>),
Attribute(name='__ge__', kind='method', defining_class=<class 'object'>, object=<slot wrapper '__ge__' of 'object' objects>),
Attribute(name='__getattribute__', kind='method', defining_class=<class 'object'>, object=<slot wrapper '__getattribute__' of 'object' objects>),
...
...
]
"""
# 我们看一些这个Attribute,里面有
# name: 内部的属性名
# kind:种类,如果是一个函数,那么是method,如果是属性,那么是data,同理还有class method和static method
# defining_class:这个属性或者函数是属于谁的,比如__dict__,A这个类自身存在,那么就是<class '__main__.A'>,但__dir__的话,A没有,所以这个方法是object提供的
# object:类调用相应属性或者函数返回的结果
# 我们以__class__和__dir__为例
print(A.__class__) # <class 'type'>
print(A.__dir__) # <method '__dir__' of 'object' objects>
# 怎么样返回的结果和上面的是不是一样的呢?
for _ in inspect.classify_class_attrs(A):
# 具体属性直接通过.来调用即可
if _.name == "__str__":
# 由于_.object返回的都是类调用的函数,那么需要手动传递self
# 因为是object的__str__方法,那么我们传递任何东西都可以
print(_.object(A())) # <__main__.A object at 0x000001B15A571580>
print(int) # <class 'int'>
print([{()}]) # [{()}]
import inspect
class A(int): ...
print(inspect.getmro(A)) # (<class '__main__.A'>, <class 'int'>, <class 'object'>)
print(A.__mro__) # (<class '__main__.A'>, <class 'int'>, <class 'object'>)
import inspect
from functools import wraps
def deco(cls):
@wraps(cls)
def inner():
return "inner"
return inner
@deco
def func():
return "func"
# 我们看到func被deco装饰了,那么此时的func指向了deco函数内部的inner
print(func()) # inner
# 那么我们可以通过inspect.unwrap函数进行还原,得到原本的函数
print(inspect.unwrap(func)()) # func
# 但是注意:只有装饰器内层函数加上了@wraps(cls)才可以还原
# 因为如果不加的话,那么函数的整个信息就变了,unwrap还原是需要原来函数的元信息的
# 即使是多个装饰器也是可以的,但内层函数都要有@wraps(cls)
# 如果多个装饰器出现了没有被@wraps(cls),那么就会停止。至于顺序是从下往上还是从上往下可以自己去研究一下
除此之外unwraps还接收一个stop参数,必须通过关键字传递。该参数需要传递一个接收一个参数的函数,然后在去掉装饰器的时候会将结果传到这个函数里面,如果函数返回True,那么提前终止。如果返回False,那么unwrap会一直解包,直到返回装饰链中的最后一个函数,也就是最原始的函数,有兴趣可以自己试一下。
import inspect
s1 = " aaa"
s2 = " "
s3 = ""
print(inspect.indentsize(s1)) # 1
print(inspect.indentsize(s2)) # 4
print(inspect.indentsize(s3)) # 0
# 返回的实际上就是开头空格的长度
print(len(s1) - len(s1.lstrip())) # 1
print(len(s2) - len(s2.lstrip())) # 4
print(len(s3) - len(s3.lstrip())) # 0
# inspect.indentsize内部也是这么做的
# 个人觉得这个可以自己实现
import inspect
class A:
"""
这是类A
"""
def b():
"""
这是函数B
@return:
"""
print(inspect.getdoc(A)) # 这是类A
print(inspect.getdoc(b))
"""
这是函数B
@return:
"""
# 可以看到inspect.getdoc没有把多余的空格算进去
print(A.__doc__)
"""
这是类A
"""
print(b.__doc__)
"""
这是函数B
@return:
"""
# 但是对象的__doc__属性就是相当于文档字符串原原本本的输出出来
import inspect
class A:
"""
这是类A
"""
def b():
"""
这是函数B
@return:
"""
print(A.__doc__)
"""
这是类A
"""
print(inspect.cleandoc(A.__doc__)) # 这是类A
# 可以看到这个和刚才的getdoc是一样的
# 但是它不仅仅是针对doc,普通的字符串也是可以的
s = """
xxx
xxx
"""
print(inspect.cleandoc(s))
"""
xxx
xxx
"""
# 关于去除缩进,个人更推荐textwrap这个模块,在我的博客<<python常用模块>>里面有,可以翻一下。
import inspect
from tornado.ioloop import IOLoop
import tornado
print(inspect.getfile(tornado)) # C:\python38\lib\site-packages\tornado\__init__.py
print(inspect.getfile(IOLoop)) # C:\python38\lib\site-packages\tornado\ioloop.py
import inspect
print(inspect.getmodulename(r"C:\xxxx\iolaaaoop.py")) # iolaaaoop
print(inspect.getmodulename(r"C:\xxxx\iolaaaoop.pyc")) # iolaaaoop
print(inspect.getmodulename(r"C:\xxxx\iolaaaoop.pyd")) # iolaaaoop
# 所以不管这个文件是否存在,只要是以py、pyc、pyd结尾的
# 那么返回文件名,也就是你用来import的部分
import inspect
from tornado.ioloop import IOLoop
import tornado
print(inspect.getsourcefile(tornado)) # C:\python38\lib\site-packages\tornado\__init__.py
print(inspect.getsourcefile(IOLoop)) # C:\python38\lib\site-packages\tornado\ioloop.py
# 和前面介绍inspect.getfile比较类似
# 或者使用inspect.getabsfile
print(inspect.getabsfile(tornado)) # C:\python38\lib\site-packages\tornado\__init__.py
print(inspect.getabsfile(IOLoop)) # C:\python38\lib\site-packages\tornado\ioloop.py
import inspect
from tornado.ioloop import IOLoop
import tornado
print(inspect.getmodule(tornado)) # <module 'tornado' from 'C:\\python38\\lib\\site-packages\\tornado\\__init__.py'>
print(inspect.getmodule(IOLoop)) # <module 'tornado.ioloop' from 'C:\\python38\\lib\\site-packages\\tornado\\ioloop.py'>
print(inspect.getmodule(tornado).version) # 6.0.3
print(tornado.version) # 6.0.3
# 我们知道还可以通过__module__来查看,但是它们只能针对类、类的实例和函数来用
# 模块和包没有,但是getmodule可以返回,当然返回的就是其本身
import inspect
def foo(
a: int,
b: str,
c: int = 1,
*args: str,
d: int = 1,
**kwargs: dict
) -> None:
pass
# 接收一个函数
print(inspect.getfullargspec(foo))
"""
FullArgSpec(
args=['a', 'b', 'c'],
varargs='args',
varkw='kwargs',
defaults=(1,),
kwonlyargs=['d'],
kwonlydefaults={'d': 1},
annotations={'return': None, 'a': <class 'int'>,
'b': <class 'str'>, 'c': <class 'int'>,
'args': <class 'str'>, 'd': <class 'int'>,
'kwargs': <class 'dict'>})
"""
# 返回的是一个namedtuple,里面属性如下
# args:即可以通过位置参数传递、也可以通过关键字参数传递的 所有参数名
# varargs:通过扩展位置参数传递的参数名,也就是*xxx
# varkw:通过扩展关键字参数传递的参数名,也就是**xxx
# defaults:所有参数的默认值,这里的参数当然是args里面的参数对应的默认值
# kwonlyargs:只能通过关键字参数传递的参数名,我们注意到d是在*args后面,那么如果d不通过关键字传递,那么将永远无法给d传参,因为位置参数永远会被*args接收
# kwonlydefaults:只能通过关键字参数传递的参数的默认值,是一个字典
# annotations:注解,这是在python3.5增加的。其它的不用说,关键来看*args和**kwargs
# 我们上面的注解表示,传递的扩展位置参数都必须是str类型,传递的扩展关键字参数都必须是dict类型
# 即使对于类也是一样的,会自动获取内部__init__函数的参数信息
import inspect
def foo(
a: int,
b: str,
c: int = 1,
*args: str,
d: int = 1,
**kwargs: dict
) -> None:
pass
# 接收一个函数,返回一个Signature对象
print(inspect.signature(foo)) # (a: int, b: str, c: int = 1, *args: str, d: int = 1, **kwargs: dict) -> None
print(type(inspect.signature(foo))) # <class 'inspect.Signature'>
s = inspect.signature(foo)
# 返回所有参数
print(s.parameters)
# 得到的是一个OrderedDict,里面的value是Parameter类型
"""
OrderedDict(
[('a', <Parameter "a: int">), ('b', <Parameter "b: str">),
('c', <Parameter "c: int = 1">), ('args', <Parameter "*args: str">),
('d', <Parameter "d: int = 1">), ('kwargs', <Parameter "**kwargs: dict">)]
)
"""
# Parameter有如下属性
print(s.parameters["a"].name, s.parameters["c"].name) # a c
print(s.parameters["a"].default, s.parameters["c"].default) # <class 'inspect._empty'> 1
print(s.parameters["a"].annotation, s.parameters["c"].annotation) # <class 'int'> <class 'int'>
print(s.parameters["a"].kind, s.parameters["c"].kind) # POSITIONAL_OR_KEYWORD POSITIONAL_OR_KEYWORD
还有一部分方法个人觉得不常用,所以就不说了,因为涉及到栈帧,而且如果你熟悉栈帧的话,那么很多功能你可以自己实现,没必要使用里面的。就比如根据对象获取该对象所在的文件路径,这个自己就可以实现的。但是里面很多方法还是很有用的,至于具体什么时候使用就由你自己决定