Skip to content

Python面向对象概念

类(class)把数据与功能绑定在一起。创建新类就是创建新的对象类型(type of object),从而创建该类型的新实例(instances)。 类实例具有多种保持自身状态的属性(attributes)。 类实例还支持(由类定义的)修改自身状态的方法(methods)。

Python的类支持所有面向对象编程(OOP)的标准特性:

  • 类继承(class inheritance)机制支持多个基类(base classes);
  • 派生类(derived class)可以覆盖基类的任何方法(methods);
  • 类的方法可以调用基类中相同名称的方法
  • 对象可以包含任意数量和类型的数据。
  • 类(class)和模块(module)都拥有动态特性(dynamic nature):在运行时创建,创建后也可以修改。

名称Names和对象Objects

对象之间相互独立,多个名称(names)(在多个作用域内)可以绑定到同一个对象。 其他语言称之为别名(alias)。 别名在某些方面就像指针。例如,传递对象的代价很小,因为实现只传递一个指针;如果函数修改了作为参数传递的对象,调用者就可以看到更改。

作用域Scopes和命名空间Namespaces

**命名空间(namespace)**是一个从名字到对象的映射。 当前大部分命名空间都由 Python 字典实现。

下面是几个命名空间的例子:

  • 存放内置函数的集合(包含abs()这样的函数,和内建的异常等);
  • 模块中的全局名称;
  • 函数调用中的局部名称;

从某种意义上说,对象的属性集合(the set of attributes of an object)也是一种命名空间的形式

关于命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系; 例如,在两个不同的模块中都可以定义一个maximize函数而不会产生混淆,但在调用maximize函数时必须必须在其前面加上模块名称。

任何跟在一个点号之后的名称都称为**属性(attribute)**。例如,在表达式z.real中,real是对象z的一个属性。

按严格的说法,对模块(module)中的名称的引用(reference)都属于属性引用(attribute reference): 在表达式modname.funcname中,modname是一个模块对象(module object)而funcname是它的一个属性。 在此情况下在模块的属性(module’s attribute)和模块中定义的全局名称之间正好存在一个直观的映射:它们共享相同的命名空间。 但存在一个例外。 模块对象有一个只读属性__dict__,它返回用于实现模块命名空间的字典;__dict__是属性但不是全局名称。 使用这个将违反命名空间实现的抽象,应当仅被用于事后调试器之类的场合。

**属性(attribute)**可以是只读或者可写的,所以可以对属性进行赋值,例如modname.the_answer = 42。 删除属性可以用del语句,例如,del modname.the_answer将会从名为modname的对象中移除the_answer属性。

命名空间在不同时刻被创建,拥有不同的生存期(lifetimes)。包含内置名称(built-in names)的命名空间是在Python解释器启动时创建的,永远不会被删除。

模块的全局命名空间(global namespace)在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出。 被解释器的顶层调用(top-level invocation)执行的语句,从一个脚本文件读取或交互式地读取,被认为是__main__模块调用的一部分,因此它们拥有自己的全局命名空间。 内置名称(built-in names)实际上也存在于一个模块中,这个模块被称作builtins

一个函数的本地命名空间(local namespace)在这个函数被调用时创建,并在函数返回或抛出一个无法在该函数内部处理的错误时被删除。 每次递归调用(recursive invocations)都会有它自己的本地命名空间。

一个**作用域(scope)**是一个命名空间可直接访问(directly accessible)的Python程序的代码区域。 这里的 “可直接访问” 意味着不加任何限定的名称引用会在命名空间中进行查找。

虽然作用域是静态地确定的,但它们会被动态地使用。 在代码执行期间的任何时刻,会有3或4个的嵌套作用域供命名空间直接访问:

  • 最先搜索的最内部作用域包含局部名称
  • 从最近的封闭作用域开始搜索的任何封闭函数的作用域包含非局部名称,也包括非全局名称
  • 倒数第二个作用域包含当前模块的全局名称
  • 最外面的作用域(最后搜索)是包含内置名称的命名空间

如果一个名称被声明为全局变量,则所有引用和赋值将直接指向该模块全局名称所在的中间作用域。 如果要重新绑定在最内层作用域以外的变量,可以使用nonlocal语句声明为非本地变量。 如果没有被声明为非本地变量,这些变量将是只读的。给这样的变量赋新值只会在最内层作用域中创建一个*新的*局部变量,而同名的外部全局变量将保持不变。

通常,当前局部作用域(local scope)将引用当前函数作用域的名称(local name)。 在函数作用域以外,当前局部作用域将引用与全局作用域相一致的命名空间:模块的命名空间(the module’s namespace)。

定义一个类,是在本地局部命名空间内建一个新的命名空间。

在一个模块(module )内定义的函数的作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。 另一方面,实际的名称搜索是在运行时动态完成的。 但是,Python正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析!事实上,局部变量已经是被静态确定了。

如果不存在生效的globalnonlocal语句,则对名称的赋值总是会进入最内层作用域。赋值不会复制数据,是将名称绑定到对象。 删除也是如此:语句del x会从局部作用域所引用的命名空间中移除对x的绑定。事实上,所有引入新名称的操作都是使用局部作用域。特别地,import语句和函数定义会在局部作用域中绑定模块或函数名称。

global语句可被用来表明特定变量存在于全局作用域,并且应当在全局作用域中被**重新**绑定;

nonlocal语句表明特定变量生存于外层作用域中,并且应当在其所处的外层作用域中被**重新**绑定。

看下面的例子:

  • 局部赋值(local assignment,这是默认状态)不会改变scope_testspam的绑定。
  • nonlocal赋值会改变scope_testspam的绑定。
  • global赋值会改变模块层级的绑定,即,global spam重新绑定了spam的全局定义,从spam = "spam out of func"变成了spam = "global spam"。如果注释掉def do_global()这一段代码,则spam = "spam out of func"起作用。
spam = "spam out of func"

def scope_test():

    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)


scope_test()
print("In global scope:", spam)


#  运行结果
# scope_test()
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam

# print("In global scope:", spam)
In global scope: global spam

类Class

类定义 Class Definition

类定义与函数定义 (def 语句) 一样必须被执行才会起作用。

class ClassName:
    <statement-1>
    ...
    <statement-N>

在实践中,类定义内的语句通常都是函数定义,但也允许有其他语句。在类内部的函数定义通常具有一种特有形式的参数列表,这是约定的方法规范(conventions for methods)。

编译过程中,进入一个类的内部,将创建一个新的命名空间,一个局部作用域。因此,所有对类内部局部变量的赋值都是在这个新的命名空间之内,包括新定义的函数名称。

当正常离开一个类时,编译过程将创建一个类对象(class object),封装了类定义所创建的命名空间里的内容。

最初的(在进入类定义之前起作用的)局部作用域将重新生效,类对象(class object)将在这里被绑定到类定义头部所声明的类名称 (在上面的示例中是ClassName)。

类对象 Class Objects

类对象支持两种操作:属性引用(attribute references)和实例化(instantiation)。

属性引用(attribute references) 使用Python中属性引用的标准语法: obj.name

存在于类命名空间中的所有名称,类对象被创建时同时被创建了,这些就是有效的属性名称。因此,如果类定义是如下所示,那么MyClass.iMyClass.f就是有效的属性引用,将分别返回一个整数和一个函数对象。

类属性也可以被赋值,因此可以通过赋值来更改MyClass.i的值。__doc__也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"。

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

print(MyClass.i)
# 12345
print(MyClass.__doc__)
# A simple example class

MyClass.i = 10
print(MyClass.i)
# 10

类的**实例化(instantiation)**使用函数表示法。 可以把类对象(class object)看作是一个不带参数的函数,这个函数返回了该类的一个新实例。

在下面的例子中,x = MyClass()创建了MyClass()这个类的一个实例,并赋值给局部变量x

实例化操作(调用类对象)会创建一个空对象。许多类会创建带有特定初始状态的自定义实例。为此类定义中需要包含一个名为__init__()的特殊方法。

当一个类定义了__init__()方法时,类的实例化操作会自动为新创建的类实例调用__init__()。 更新上面的例子,注意__dict__两次返回的不同的字典。复习一下,在命名空间中提到,__dict__是属性但不是全局名称,返回用于实现模块命名空间的字典。

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

    def __init__(self):
        self.data = []


x = MyClass()
print(x.__dict__)
# {'data': []}

x.i = 10
print(x.__dict__)
# {'data': [], 'i': 10}

__init__()方法可以有额外的参数输入,在这种情况下,类实例化的参数将被传递给 __init__()。 如下例:

class Complex:

    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart


x = Complex(3.0, -4.5)

print(x.r, x.i)
# 3.0 -4.5

实例对象 Instance Objects

对实例对象唯一的操作是属性引用。有两种有效的属性名称:数据属性(data attributes)和方法(methods)。

**数据属性(data attributes)**类似于实例变量,数据属性不需要声明。像局部变量一样,数据属性将在第一次被赋值时产生。 例如,如果x是上面创建的MyClass的实例,则以下代码段将打印数值16,且没有留下关于x.counter的痕迹。

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

    def __init__(self):
        self.data = []


x = MyClass()

x.counter = 1

while x.counter < 10:
    x.counter = x.counter * 2

print(x.counter)
# 16

print(x.__dict__)
# {'data': [], 'counter': 16}

del x.counter
print(x.__dict__)
# {'data': []}

另一类实例属性引用称为**方法(methods)。 方法是隶属于对象的**函数

在Python中,方法这个术语并不是类实例所特有的,其他对象也可以有方法。 例如,列表对象(list objects)具有append, insert, remove, sort等方法。

在以下讨论中,我们使用方法一词将专指类实例对象的方法,除非另外明确说明。

实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类定义中所包含的所有函数对象(function objects)都称为属性。

因此在上面的示例中,x.f是有效的方法引用,因为MyClass.f是一个函数,而x.i不是方法,因为MyClass.i不是函数。但是x.fMyClass.f并不是一回事,x.f是一个**方法对象**,而MyClass.f是一个**函数对象**。差别在于f()是否与实例绑定,未绑定,就是函数,绑定,就是方法。

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

    def __init__(self):
        self.data = []


x = MyClass()

print(MyClass.f(0))
# hello world
print(x.f())
# hello world

print(MyClass.f)
# <function MyClass.f at 0x7ff9368b3488>
print(x.f)
# <bound method MyClass.f of <__main__.MyClass object at 0x7ff9368acbe0>>

print(type(MyClass.f))
# <class 'function'>
print(type(x.f))
# <class 'method'>

这里做个小结:

  • 函数(function)是Python中一个可调用对象(callable), 方法(method)是一种特殊的函数。
  • 一个可调用对象是方法和函数,和这个对象无关,仅和这个对象是否与类或实例绑定有关(bound method)。
  • 静态方法没有和任何类或实例绑定,所以**静态方法是个函数**。

方法对象 Method Objects

在 MyClass 示例中,x.f()是一个方法对象,被调用后,将返回字符串'hello world'。可以立即调用,也可以保存起来以后再调用xf = x.f

虽然f()的函数定义指定了一个参数,但上面例子中调用x.f()时并没有带参数,也没有引发异常报错。原因在于,方法(method)的特殊之处就在于实例对象会作为函数的第一个参数被传入。 调用x.f()其实就相当于MyClass.f(x)。 总之,调用一个具有n个参数的方法(method)就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前

当一个实例的非数据属性被引用时,将搜索实例所属的类。 如果被引用的属性名称是类中一个有效的函数对象,则会创建一个抽象的对象,通过打包(parking,即指向)匹配到的实例对象和函数对象,这个抽象对象就是方法对象。 当带参数调用方法对象时,将基于实例对象和参数列表构建一个新的参数列表,并使用这个新参数列表调用相应的函数对象。

类和实例变量 Class and Instance Variables

一般来说,**实例变量**用于每个实例的唯一数据,而**类变量**用于类的所有实例共享的属性和方法:

class Dog:

    kind = 'canine'  # class variable shared by all instances

    def __init__(self, name):
        self.name = name  # instance variable unique to each instance

d = Dog('Fido')
e = Dog('Buddy')

print(d.kind)  # shared by all dogs
# 'canine'

print(e.kind)  # shared by all dogs
# 'canine'

print(d.name) # unique to d instance
# 'Fido'

print(e.name) # unique to e instance
# 'Buddy'

下代码中的tricks列表不应该被用作类变量,因为所有的Dog实例将只共享一个单独的列表:

class Dog:

    kind = 'canine'  # class variable shared by all instances

    tricks = []  # mistaken use of a class variable

    def __init__(self, name):
        self.name = name  # instance variable unique to each instance

    def add_trick(self, trick):
        self.tricks.append(trick)


d = Dog('Fido')
e = Dog('Buddy')

d.add_trick('roll over')
e.add_trick('play dead')

print(d.tricks)
# ['roll over', 'play dead']

正确的类设计应该使用实例变量:

class Dog:

    kind = 'canine'  # class variable shared by all instances

    def __init__(self, name):
        self.name = name  # instance variable unique to each instance
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)


d = Dog('Fido')
e = Dog('Buddy')

d.add_trick('roll over')
e.add_trick('play dead')

print(d.tricks)
# ['roll over']

print(e.tricks)
# ['play dead']

如果同样的属性名称同时出现在实例和类中,则属性查找会**优先选择实例**:

class Warehouse:
    purpose = 'storage'
    region = 'west'

w1 = Warehouse()
print(w1.purpose, w1.region)
# storage west

w2 = Warehouse()
w2.region = 'east'  # Instance W2 has higher priority than class
print(w2.purpose, w2.region)
# storage east

数据属性(Data attributes)可以被方法(method)以及一个对象的普通用户(ordinary users)(“客户端Client”)所引用。 换句话说,类不能用于实现纯抽象数据类型。

方法的第一个参数常常被命名为self,这只是一个约定: self这一名称在Python中没有特殊含义。 但是遵循此约定会使得代码具有很好的可读性。

任何一个作为类属性(class attribute)的函数对象(function object)都为该类的实例定义了一个相应方法。

函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的。如下例。现在f,gh都是类C的引用函数对象的属性,因而它们就都是类C的实例的方法,其中h完全等同于g。但请注意,下面这个例子的可读性非常不好。

# Function defined outside the class
def f1(self, x, y):
    return min(x, x + y)


class C:
    f = f1  # Assign a function object to a local variable in the class

    def g(self):
        return 'hello world'

    h = g

方法(methods)可以通过使用self参数的方法属性(method attributes)调用其他方法(method):

class Bag:

    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

方法可以通过与普通函数相同的方式引用全局名称。 与方法相关联的全局作用域就是包含其定义的模块。 (类永远不会被作为全局作用域。)

虽然我们很少会有充分的理由在方法中使用全局作用域,但全局作用域存在许多合理的使用场景:举个例子,导入到全局作用域的函数和模块可以被方法所使用,在其中定义的函数和类也一样。 通常,包含该方法的类本身是在全局作用域中定义的。

总结

类定义小结

一个类定义类成员属性和成员方法。

一个类可以实例化多个对象,每个实例化对象都是独立的。

创建的类实例化对象,会引用父类中的属性和方法,并不会把类的属性和方法复制给对象,因此:

在访问实例化对象的属性和方法时,会先去找对象自己的属性和方法,然后再去实例化这个对象的类中查找(引用)。

对象成员的添加和修改,都只会影响当前对象自己,不会影响类和其它对象。

删除对象成员的时候,必须是该对象自己具备的成员才可以,不能删除类中引用的成员。

对类成员的操作,会影响这个类创建的对象,包括之前创建的对象(引用)。

类成员操作(不推荐)

  • 成员属性:
  • 访问:ClassName.AttributeName
  • 修改:ClassName.AttributeName = NewValue,等于给这个类对象创建了一个自己的属性,通过这个类创建的对象都具有这个属性。
  • 添加:ClassName.NewAttributeName = Value,等于给这个类对象创建了一个自己的属性,通过这个类创建的对象都具有这个属性。
  • 删除:del ClassName.AttributeName,注意,只能删除类对象自己的属性,通过这个类创建的对象都不再具有这个属性。
  • 成员方法:
  • 访问:ClassName.MethodName()
  • 修改:ClassName.MethodName = NewFunction,等于给这个类对象创建了一个自己的方法,通过这个类创建的对象都具有这个方法。
  • 添加:ClassName.MethodName = Function,等于给这个类对象创建了一个自己的方法,通过这个类创建的对象都具有这个方法。
  • 删除:del ClassName.MethodName,注意,只能删除类对象自己的方法,通过这个类创建的对象都不再具有这个方法。

成员方法中的self

self只是一个形参,不是关键字。 self在方法(method)代表当前对象自己。前面提到过,方法的第一个参数常常被命名为self,这只是一个约定。 可以使用self在类内部操作成员(添加、修改、删除等)。

方法的分类:

  • 含有self或者可以接受对象作为参数的方法,称为**非绑定类方法**,非绑定类的方法可以使用对象去访问。
  • 不含有self或者不能接受对象作为参数的方法,称为**绑定类方法**,绑定方法只能使用类去访问。

魔术方法

魔术方法(Magic Method)和普通方法一样,都是类中定义的成员方法。 魔术方法名称前后各有2个下划线,比如 __init__ 魔术方法是不需要手动调用的,会在某种情况下自动触发(自动执行)。 魔术方法是系统定义好的,不是用户定义的。

__init__初始化方法,也称作**构造方法**

类实例化对象创建后自动触发。

__init__初始化方法可以用来在对象实例化后完成对象的初始化,比如属性赋值,方法调用等。

__del__析构方法

类实例化对象被销毁时自动触发。

__del__析构方法可以在销毁对象时完成一些特殊任务,关闭对象打开的一些资源,如文件等。

注意,是对象被销毁时触发了析构方法,而不是这个析构方法销毁了对象。

对象销毁的情况:

  • 当程序执行完毕,销毁和释放内存中的资源。
  • 使用del删除时。
  • 对象不再被任何对象引用时,会自动销毁。

看下面的例子,对比bmw = Car('BMW')Car('BMW')来理解initdel的触发机制。

编辑文件file1.py

class Car():
    brand = ""

    def __init__(self, car_brand):
        self.brand = car_brand
        print(f"initial method called, create {self.brand} car")

    def __del__(self):
        print(f"delete method called, destroy {self.brand} car")


bmw = Car('BMW')
vw = Car('VW')

执行上面的代码python3 file1.py得到如下输出,在程序执行完毕时,依次执行__del__

    initial method called, create BMW car
    initial method called, create VW car
    delete method called, destroy BMW car
    delete method called, destroy VW car

编辑文件file2.py

class Car():
    brand = ""

    def __init__(self, car_brand):
        self.brand = car_brand
        print(f"initial method called, create {self.brand} car")

    def __del__(self):
        print(f"delete method called, destroy {self.brand} car")


Car('BMW')
Car('VW')

执行上面的代码python3 file2.py得到如下输出:

    initial method called, create BMW car
    delete method called, destroy BMW car
    initial method called, create VW car
    delete method called, destroy VW car

Python函数内省内省

从魔术方法可以延申到Python的**函数内省**,函数内省的意思是说,当你拿到一个“函数对象”的时候,你可以继续知道,它的名字,参数定义等信息。这些信息可以通过函数对象的属性(一些双下划线的魔法方法)得到。简言之,内省是在运行时确定对象类型的能力。

下面的例子列出了常规对象没有而函数有的属性。

class C: 
    pass

obj = C()

def func():
    pass

sorted(set(dir(obj)) - set(dir(func)))
# ['__weakref__']

sorted(set(dir(func)) - set(dir(obj)))
# ['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

下表总结了用户定义的函数的属性。

用户定义的函数的属性

下面的例子是演示了在指定长度附近截断字符串的函数,以及提取关于函数参数的信息的方法。

参数名称在__code__.co_varnames中,但这里面也包含函数定义体中创建的局部变量。因此,参数名称是前N个字符串,N的值由__code__.co_argcount确定,例子里面N是2,即参数名称是textmax_len,局部变量是endspace_beforespace_after

def clip(text, max_len=80):
    """
    Get sub-string by the first blank before or after specified position.
    rfind() 返回字符串最后一次出现的位置,如果没有匹配项则返回 -1.
    """
    end = None

    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)

        if space_before >= 0:
            end = space_before
    else:
        space_after = text.rfind(' ', max_len)

        if space_after >= 0:
            end = space_after

    if end is None:
        end = len(text)

    return text[:end].rstrip()



clip('This is the string', max_len=10)
# 'This is'

clip.__defaults__
# (80,)

clip.__code__
# <code object clip at 0x7f1e04a5c8a0, file "<stdin>", line 1>

clip.__code__.co_varnames
# ('text', 'max_len', 'end', 'space_before', 'space_after')

clip.__code__.co_argcount
# 2

clip.__doc__
# '\n    Get sub-string by the first blank before or after specified position.\n    rfind() 返回字符串最后一次出现的位置,如果没有匹配项则返回 -1.\n    '

上例中,参数的默认值只能通过它们在__defaults__元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来,有些不合理。引入inspect模块后,上面的操作就更容易了。

inspect.signature函数返回一个inspect.Signature对象,它有一个parameters属性,这是一个有序映射,把参数名和inspect.Parameter对象对应起来。各个Parameter属性也有自己的属性,例如namedefaultkind

from inspect import signature

sig = signature(clip)

type(sig)
# <class 'inspect.Signature'>
print(sig)
# (text, max_len=80)
print(str(sig))
# (text, max_len=80)

for name, param in sig.parameters.items():
    print(f'{param.kind} : {name} = {param.default}')

# 1 : text = <class 'inspect._empty'>
# 1 : max_len = 80

函数注解。

Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据。对上例添加注解后如下所示,二者唯一的区别在第一行。

函数声明中的各个参数可以在:之后增加注解表达式。 如果参数有默认值,注解放在参数名和=号之间。 如果想注解返回值,在)和函数声明末尾的:之间添加->和一个表达式。那个表达式可以是任何类型。 注解中最常用的类型是类(如strint)和字符串(如'int > 0')。在下例中,max_len参数的注解用的是字符串。

注解不会做任何处理,只是存储在函数的__annotations__属性(一个字典)中。换句话说,注解对Python解释器没有任何意义。注解只是元数据,可以供IDE、框架和装饰器等工具使用。

return键保存的是返回值注解,即下例中函数声明里以->标记的部分。

def clip(text:str, max_len:'int > 0'=80) -> str:
    """
    Get sub-string by the first blank before or after specified position.
    rfind() 返回字符串最后一次出现的位置,如果没有匹配项则返回 -1.
    """
    end = None

    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)

        if space_before >= 0:
            end = space_before
    else:
        space_after = text.rfind(' ', max_len)

        if space_after >= 0:
            end = space_after

    if end is None:
        end = len(text)

    return text[:end].rstrip()



clip('This is the string', max_len=10)
# 'This is'

clip.__annotations__
# {'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}

signature函数返回一个Signature对象,它有一个return_annotation属性和一个parameters属性,后者是一个字典,把参数名映射到Parameter对象上。每个Parameter对象自己也有annotation属性。

from inspect import signature

sig = signature(clip)

print(sig.return_annotation)
# <class 'str'>

for param in sig.parameters.values():
    note = repr(param.annotation).ljust(13)
    print(f'{note} : {param.name} = {param.default}')

# <class 'str'> : text = <class 'inspect._empty'>
# 'int > 0'     : max_len = 80