【python核心知识点】函数、类和装饰器

文章目录

一. 函数

1. 传递参数
  • 参数的传递是通过自动将对象赋值给本地变量名来实现的。函数参数[调用者发送的(可能的)的共享对象引用值]在实际中只是Python赋值的另一个实例而已。因为引用是以指针的形式实现的,所有的参数实际上都是通过指针进行传递的。作为参数被传递的对象从来不自动拷贝

  • 在函数内部的参数名的赋值不会影响调用者。在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内的。函数参数名和调用者作用域中的变量名是没有别名的。

  • 改变函数的可变对象参数的值也许会对调用者有影响。换句话说,因为参数是简单地赋值给传入的对象,函数能够就地改变传入的可变对象,因此其结果会影响调用者。可变参数对于函数来说是可以做输入和输出的。

    • 不可变参数“通过值”进行传递。像整数和字符串这样的对象是通过对象引用而不是拷贝进行传递的,但是因为你无论怎样都不可能在原处改变不可变对象,实际的效果就很像创建了一份烤贝。
    • **可变对象是通过“指针”进行传递的。**例如,列表和字典这样的对象也是通过对象引用进行传递的,这一点与C语言使用指针传递数组很相似:可变对象能够在函数内部进行原处的改变,这一点和C数组很像。
2. 函数返回值

这里有一个较为纯粹的技巧:因为return能够返回任意种类的对象,所以它也能够返回多个值,如果这些值封装进一个元组或其他的集合类型。

def multiple(x,y):
    x = 2
    y = [3,4]
    return x,y #省略了括号。

x = 1
L = [1,2]
print(multiple(x,L)) #(2, [3, 4]) 
x,L = multiple(x,L)
print(x,L) #2 , [3,4]

看起来这里的代码好像返回了两个值,但是实际上只有一个:一个包含有2个元素的元组,它的圆括号是可选的,这里省略了。

3. 特定参数匹配模型
  • 位置: 从左到右
    • 一般情况下,也是我们迄今为止最常使用的那种方法,是通过位置进行匹配把参数值传递给函数头部的参数名称,匹配顺序为从左到右。
  • 关键字参数: 通过参数名进行匹配
    • 调用者可以定义哪一个函数接受这个值,通过在调用时使用参数的变量名,使用name=value这种语法。
  • 默认参数: 为没有传入值的参数定义参数值
    • 如果调用时传入的值过于少的话,函数能够为参数定义接受的默认值,再一次使用语法name=value。
  • 可变参数: 收集任意多基于位置或关键字的参数
    • 函数能够使用特定的参数,它们是以字符*开头,收集任意多的额外参数(这个特性常常叫做可变参数,类似C语言中的可变参数特性,也能够支持可变长度参数的列表)。
  • Keyword-only参数: 参数必须按照名称传递
    • 在Python 3.0中(不包括Python 2.6中),函数也可以指定参数,参数必须用带有关键参数的名字(而不是位置)来传递。这样的参数通常用来定义实际参数以外的配置选项。
语法位置解释
func(value)调用者常规参数:通过位置进行匹配
func(name=value)调用者关键字参数:通过变量名匹配
func(*sequence)调用者以name传递所有的对象,并作为独立的基于位置的参数
func(**dict)调用者以name成对的传递所有的关键字/值,并作为独立的关键字参数
def func(name)函数常规参数:通过位置或变量名进行匹配
def func(name=value)函数默认参数值,如果没有在调用中传递的话
def func(*name)函数匹配并收集(在元祖中的)所有包含位置的参数
def func(**name)函数匹配并收集(在字典中的)所有包含位置的参数
def func(*args,name)函数参数必须在调用中按照关键字传递
#1.常规参数
def f(a,b,c):
    print(a,b,c)

f(1,2,3) #必须传递三个参数,不然报错。

#2.默认参数
def f(a,b,c=1):
    print(a,b,c)

f(1,2) #c可以不传递。
f(1,2,3)
f(a=10,b=10,c=100) #也可以这样调用。这种属于混合了。

# 3.关键字参数
def f(a=1,b=1,c=1):
    print(a,b,c)

f(c=100) #只传递c
f(10,c=100) #常规参数必须在前面。这里只传递了a和c。

# 4.任意参数*args
def f(*args):
    print(args)

f(1,2,3) #(1, 2, 3)
# f(a=1,b=1)这种调用就错误的。

# 5.任意参数**kargs
def f(**kargs):
    print(kargs)

f(name='joker',age=30) #{'name': 'joker', 'age': 30}
#f(1,2,3) 这种调用方式是错误的。

# 6.混合。
def f(a,*args,**kargs):
    print(a,args,kargs)

f(1,2,3,name='joker',age=30) # 1 (2, 3) {'name': 'joker', 'age': 30}

#7.只能使用keyword-only传递
def kwonly(a,*b,c):
    print(a,b,c)

kwonly(1,2,3,4,5,c=100) #结果:1 (2, 3, 4, 5) 100 ;必须传递c,否则报错。

def kwonly(a,*,c):
    print(a,c)

kwonly(1,c=100) #要传递两个,也只能传递两个参数。

def keyWordOnly2(*args,sep=' ',end='\n'):
    pass
  • 解包参数

    在最新的Python版本中,我们在调用函数时能够使用*语法。在这种情况下,它与函数定义的意思相反**。它会解包参数的集合**,而不是创建参数的集合。

def f(name='',age=0):
    print(name,age)

person = {"name":"joker","age":30}
f(**person)

def g(a,b,c):
    print(a,b,c)
aTuple = (1,2,3)
g(*aTuple)
4. 函数对象: 属性与注解
1. 函数的注解

在Python 3.0中(但不包括Python 2.6),也可以给函数对象附加注解信息—与函数的参数和结果相关的任意的用户定义的数据。Python为声明注解提供了特殊的语法,但是,它自身不做任何事情﹔注解完全是可选的,并且,出现的时候只是直接附加到函数对象的**annotations_**属性以供其他用户使用。

def func(a: 'spam',b:float,c:int = 100) ->int:
    return a+b+c
    
print(func(1,2,3))

很遗憾,标准库和python内置的模块都没使用函数的注解。

2. 函数的内置属性
def func(a,b,c=100):
    print(a,b,c)

print(dir(func))

['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', 
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

再看如下:

def func(a,b,c=100):
    print(a,b,c)

print(help(func.__annotations__))
属性或方法含义
__subclasshook____subclasshook__(…) method of builtins.type instance. Abstract classes can override this to customize issubclass(). This is invoked early on by abc.ABCMeta.__subclasscheck__(). It should return True, False or NotImplemented. If it returns NotImplemented, the normal algorithm is used. Otherwise, it overrides the normal algorithm (and the outcome is cached).
__str____str__ = <method-wrapper ‘__str__’ of function object> Return str(self).
__sizeof__Size of object in memory, in bytes.
__setattr____setattr__ = <method-wrapper ‘__setattr__’ of function object> Implement setattr(self, name, value).
__repr____repr__ = <method-wrapper ‘__repr__’ of function object> Return repr(self).
__reduce_ex____reduce_ex__(protocol, /) method of builtins.function instance Helper for pickle.
__reduce__Helper for pickle.
__qualname__
__new____new__(*args, **kwargs) method of builtins.type instance. Create and return a new object. See help(type) for accurate signature.
__ne____ne__ = <method-wrapper ‘__ne__’ of function object> Return self!=value.
__name__函数名
__module__Help on module __main__
__lt____lt__ = <method-wrapper ‘__lt__’ of function object> Return self<value.
__le____le__ = <method-wrapper ‘__le__’ of function object> Return self<=value.
__kwdefaults__
__init_subclass__
__init__
__hash____hash__ = <method-wrapper ‘__hash__’ of function object> Return hash(self).
__gt____gt__ = <method-wrapper ‘__gt__’ of function object> Return self>value.
__globals__全局属性
__getattribute____getattribute__ = <method-wrapper ‘__getattribute__’ of function object> Return getattr(self, name).
__get____get__ = <method-wrapper ‘__get__’ of function object> Return an attribute of instance, which is of type owner.
__ge____ge__ = <method-wrapper ‘__ge__’ of function object> Return self>=value.
__format____format__(format_spec, /) method of builtins.function instance. Default object formatter.
__eq____eq__ = <method-wrapper ‘__eq__’ of function object> Return self==value
__doc__文档,在函数中,三引号注释的内容。
__dir____dir__() method of builtins.function instance Default dir() implementation.
__dict__
__delattr____delattr__ = <method-wrapper ‘__delattr__’ of function object> Implement delattr(self, name).
__defaults__默认命名参数,以元组的方式展示。比如def a(a,b,c=200); (200,)
__code__返回一个code类实例。
__closure__
__class__
__call__
__builtins__
__annotations__
 |  code
 |    a code object
 |  globals
 |    the globals dictionary
 |  name
 |    a string that overrides the name from the code object
 |  argdefs
 |    a tuple that specifies the default argument values
 |  closure
 |    a tuple that supplies the bindings for free variables
3. 自定义函数属性
def func(a,b,c=100):
    t = 100
    print(a,b,c)
func.age=20
print(func.age)
5. lambda匿名函数

格式

lambda argument1,argument2,...argumentN: expression using arguments

lambda是一个表达式,而不是一个语句。

实例:

f = lambda x,y,z=100: x+y+z
#或者 f= (lambda x,y,z=100: x+y+z)
print(f(1,2,200))
6. 闭包函数
def f():
    z =10
    def test(x,y):
        return x+y+z
    return test
k = f()
print(k(100,200))
7. 作用域
1. global

global语句是Python中唯一看起来有些像声明语句的语句。但是,它并不是一个类型或大小的声明,它是-一个命名空间的声明。它告诉Python函数打算生成一个或多个全局变量名。也就是说,存在于整个模块内部作用域(命名空间)的变量名。

  • 全局变量是位于模块文件内部的顶层的变量名。
  • 全局变量如果是在函数内被赋值的话,必须经过声明(global声明)。
  • 全局变量名在函数的内部不经过声明也可以被引用。
PI=3.1415
def changePi():
    global PI
    PI = 3.1
changePi()
print(PI) #3.1
2. nonlocal

nonlocal语句是global的近亲,前面已经介绍过global。nonlocal和global一样,声明了将要在一个嵌套的作用域中修改的名称。和global的不同之处在于,nonlocal应用于一个嵌套的函数的作用域中的一个名称,而不是所有def之外的全局模块作用域;而且在声明nonlocal名称的时候,它必须已经存在于该嵌套函数的作用域中——它们可能只存在于一个嵌套的函数中,并且不能由一个嵌套的def中的第一次赋值创建。

def tracer(func):
    call = 0
    def wrapper(*args,**kwargs):
        nonlocal call
        call += 1
        print('call {} to {}'.format(call,func.__name__))
        func(*args,**kwargs)
    return wrapper

@tracer
def spam(a,b,c):
    print(a+b+c)

if __name__ == '__main__':
    spam(1,2,3)
    spam(3,4,5)

就是说: 嵌套的低一层函数,需要声明nonlocal,来获取上一层函数的变量修改权;nonlocal只能从嵌套函数自身的内部看到.

换句话说,nonlocal即允许对嵌套的函数作用域中的名称赋值,并且把这样的名称的作用域查找限制在嵌套的def。直接效果是更加直接和可靠地实现了可更改的作用域信息,对于那些不想要或不需要带有属性的类的程序而言。

def tracer(func):
    call = 0
    def wrapper(*args,**kwargs):
        nonlocal call
        call += 1
        print('call {} to {}'.format(call,func.__name__))
        func(*args,**kwargs)
    return wrapper

@tracer
def spam(a,b,c):
    print(a+b+c)

if __name__ == '__main__':
    spam(1,2,3)
    spam(3,4,5)

同理,如果只是访问的话,不需要加nonlocal

二. 模块

注意:模块和包是有区别的

简而言之,模块通过使用自包含的变量的包,也就是所谓的命名空间提供了将部件组织为系统的简单的方法。在一个模块文件的顶层定义的所有的变量名都成了被导入的模块对象的属性。

事实上,在一个导入语句中的模块名起到两个作用:

  • 识别加载的外部文档,但是它也会变成赋值给被载入模块的变量。

模块定义的对象也会在执行时创建,就在import执行时;import会一次运行在目标文档中的语句从而建立其中的内容。

1. import如何工作

这么比较:在Python中,导入并非只是把一个文件文本插入另一个文件而已。导入其实是运行时的运算,程序第一次导入指定文件时,会执行三个步骤:

  • 找到模块文件。
  • 编译成位码(需要时)。
  • 执行模块的代码来创建其所定义的对象。

记住,这三个步骤只在程序执行时,模块第一次导入时才会进行。在这之后,导入相同模块时,会跳过这三个步骤,而只提取内存中已加载的模块对象。从技术上讲,Python把载入的模块存储到一个名为sys.modules的表中,并在一次导入操作的开始检查该表。

1. 搜索

首先,Python必须查找到import语句所引用的模块文件。注意:上一节例子中的import语句所使用的文件名中没有a.py,也没有目录路径,只有import b,而不是import c: \dir1lb.py事实上,只能列出简单名称。路径和后缀是刻意省略掉的,因为Python使用了标准模块搜索路径来找出import语句所对应的模块文件

如何搜索:可以导入sys并打印list(sys.modules.keys())查看,按照顺序如下:

  • 程序的主目录 (自动定义的,在当前程序目录)
    • 当你运行一个程序的时候,这个入口就是包含程序的顶层脚本文件的目录。当在交互模式下,就是当前工作目录。
    • 如果这个路径上有和其他目录中有相同的目录名,这个目录下的优先,其他目录下的会被覆盖,不会被调用。
  • PYTHONPATH目录(如何已经进行了设置)
    • 可以设置为:c:\pycode;d:\package
    • 不过,有些程序确实需要修改sys.path。例如,在网站服务器上执行的脚本通常会以用“nobody”的身份执行,从而限制机器的读取权限。因为这类脚本通常用“nobod以特定方式来设置PYTHONPATH,因此,在执行任何import语句前,通常会手动设置path来将所需的源代码目录包括在内。通常使用sys.path.append(dirname)就可以了
  • 标准链接库目录 (自动定义的)
    • 自动搜索标准库模块安装在机器上的那些目录
  • 任何.pth文件的内容(如果存在的话)
    • 自定义的文件,里面都是一行行的路径名。类似window的环境变量path一样。

最后这4个组件组合起来就是sys.path

>>>import sys
>>>sys.path

模块文件选择:文件名的后缀(如,.py)都是刻意从import语句中省略的。python会选择在搜索路径中第一个符合导入文件名的文件。例如:import b形式:优先级从上往下。

  • 源代码文件b.py
  • 字节码文件b.pyc
  • 目录b,包导入。
2. 编译(可选)

遍历模块搜索路径,找到符合import语句的源代码文件后,如果必要的话,Python接下来会将其编译成字节码。

**Python会检查文件的时间戳,如果发现字节码文件比源代码文件旧(例如,如果你修改过源文件),就会在程序运行时自动重新生成字节代码。**另一方面,如果发现.pyc字节码文件不比对应的.py源代码文件旧,就会跳过源代码到字节码的编译步骤。此外,如果Python在搜索路径上只发现了字节码文件,而没有源代码,就会直接加载字节码(这意味着你可以把一个程序只作为字节码文件发布,而避免发送源代码)。换句话说,如果有可能使程序的启动提速,就会跳过编译步骤。

  • 只有被导入的文件才会在机器上留下.pyc
  • 顶层文件通常是设计成直接执行,而不是被导入的
3. 运行

import操作的最后步骤是执行模块的字节码。文件中所有语句会依次执行,从头至尾,而此步骤中任何对变量名的赋值运算,都会产生所得到的模块文件的属性。因此,这个执行步骤会生成模块代码所定义的所有工具。例如,文件中的def语句会在导入时执行,来创建函数,并将模块内的属性赋值给那些函数。之后,函数就能被程
序中这个文件的导入者来调用。
因为最后的导入步骤实际上是执行文件的程序代码,如果模块文件中任何顶层代码确实做了什么实际的工作,你就会在导入时看见其结果。例如,当一个模块导入时,该模块内顶层的print语句就会显示其输出。函数的def语句只是简单地定义了稍后使用的对象。

2. 模块编写
1. 模块定义
  • 定义模块:只要使用文本编辑器,把一些Python代码输入至文本文件中,然后以“.py”为后缀名进行保存,任何此类文件都会被自动认为是Python模块。
2. import和from语句
  • 使用import或from语句,获取模块;

    • import会读取整个模块,所以必须进行定义后才能读取它的变量名

      import sys
      print(sys.path)
      
      #或者
      import sys as ss
      print(ss.path)
      
    • from将获取(或者说是复制)模块特定的变量名。

      from sys import path
      print(path)
      
    • from *语句:会取得模块顶层所有赋了值的变量名的拷贝。在Python 3.0中,这里所描述的from ...*语句形式只能用在一个模块文件的顶部,不能用于一个函数中。

      from sys import *
      print(path)	
      

就像def一样,import和from是可执行的语句,而不是编译期间的声明,而且它们可以嵌套在if测试中,出现在函数def之中等,直到执行程序时,Python执行到这些语句,才会进行解析。换句话来说,被导入的模块和变量名,直到它们所对应的import或from语句执行后,才可以使用。此外,就像def一样,import和from都是隐性的赋值语句。

  • import将整个模块对象赋值给一个变量名。
  • from将一个或多个变量名赋值给另一个模块中同名的对象。
3. from语句的陷阱
  • from语句有破坏命名空间的潜质,这是真的,至少从理论上讲是这样的。如果使用from导入变量,而那些变量碰巧和作用域中现有变量同名,变量就会被悄悄地覆盖掉。
  • 另一方面,和reload调用同时使用时,from语句有比较严重的问题,因为导入的变量名可能引用之前版本的对象。再者,from module import *形式的确可能破坏命名空间,让变量名难以理解,尤其是在导入一个以上的文件时。在这种情况下,没有办法看出一个变量名来自哪个模块,只能搜索外部的源代码文件。事实上,from *形式会把一个命名空间融入到另一个,所以会使得模块的命名空间的分割特性失效。
3. 文件生成命名空间

在模块文件顶层(也就是不在函数或类的主体内)每一个赋值了的变量名都会变成该模块的属性。

例如,假设模块文件M.py的顶层有一个像x=1这样的赋值语句,而变量名x会变成M的属性,我们可在模块外以M.x的方式对它进行引用。变量名x对M.py内其他程序而言也会变成全局变量,但是,我们需要更正式地说明模块加载和作用域的概念以了解其原因。

  • 模块语句会在首次导入时执行。系统中,模块在第一次导入时无论在什么地方,Python都会建立空的模块对象,并逐一执行该模块文件内的语句,依照文件从头到尾的顺序。
  • 顶层的赋值语句会创建模块属性。在导入时,文件顶层(不在def或class之内)赋值变量的语句(例如,=和def) ,会建立模块对象的属性,赋值的变量名会存储在模块的命名空间内。
  • 模块的命名空间能通过属性_dict_dir(M)获取。由导入而建立的模块的命名空间是字典,可通过模块对象相关联的内置的_dict_属性来读取,而且能通过dir函数查看。dir函数大至与对象的_dict__属性的键排序后的列表相等,但是它还包含了类继承的变量名。也许不完整,而且会随版本而异。
  • 模块是一个独立的作用域(本地变量就是全局变量)。模块顶层变量名遵循和函数内变量名相同的引用/赋值规则,但是,本地作用域和全局作用域相同(遵循LEGB范围规则,但是没有L和E搜索层次)。但是,在模块中,模块范围会在模块加载后变成模块对象的属性辞典。和函数不同的是(本地变量名只在函数执行时才存在),导入后,模块文件的作用域就变成了模块对象的属性的命名空间。

module2.py

import sys
def te():
    pass

test.py

import module2
print(module2.sys)
print(module2.te)
print(module2.__dict__['sys'].path)

输出:

<module 'sys' (built-in)>
<function te at 0x0000026008D7EB90>
['c:\\Users\\Administrator\\Desktop\\jumpPointer\\example',...]

此处,sys、te都是在模块语句执行时赋值的,所以在导入后都变成了属性。

4. 导入和作用域

module2.py

X=100
def changeX():
    global X
    X = 999

test.py

import module2
X=777
module2.changeX()
print(X)
print(module2.X)

输出:

777
999

执行时,module2.changeX修改module2中的x,而不是test.py中的X。module2.changeX的全局作用域一定是其所在的文件,无论这个函数是由哪个文件调用的.

换句话说,导入操作不会赋予被导入文件中的代码对上层代码的可见度:被导入文件无法看见进行导入的文件内的变量名。更确切的说法是:

  • 函数绝对无法看见其他函数内的变量名,除非它们从物理上处于这个函数内。
  • 模块程序代码绝对无法看见其他模块内的变量名,除非明确地进行了导入。
5. reload重载模块

正如我们所见到过的,模块程序代码默认只对每个过程执行一次。要强制使模块代码重新载入并重新运行,你得刻意要求Python这么做,也就是调用reload内置函数。

与import和from不同的是:

  • reload是Python中的内置函数,而不是语句。
  • 传给reload的是已经存在的模块对象,而不是变量名。
  • reload在Python 3.0中位于模块impimportlib之中,并且必须导入自己。
import importlib
import module2
import time
time.sleep(3)
importlib.reload(module2)
  • **reload会在模块当前命名空间内执行模块文件的新代码。**重新执行模块文件的代码会覆盖其现有的命名空间,并非进行删除而进行重建。
  • **文件中顶层赋值语句会使得变量名换成新值。**例如,重新执行的def语句会因重新赋值函数变量名而取代模块命名空间内该函数之前的版本。
  • **重载会影响所有使用import读取了模块的客户端。**因为使用import的客户端需要通过点号运算取出属性,在重载后,它们会发现模块对象中变成了新的值。
  • **重载只会对以后使用from的客户端造成影响。**之前使用from来读取属性的客户端并不会受到重载的影响,那些客户端引用的依然是重载前所取出的旧对象。

三、包

除了模块名之外,导入也可以指定目录路径。Python代码的目录就称为包

  • 模块指文件
  • 包指含__init__.py文件的目录

导入目录,也就是包导入了。

事实上,包导入是把计算机上的目录变成另一个Python命名空间,而属性则对应于目录中所包含的子目录和模块文件。

形式:加入存在dir0\dir1\dir2\mod.py,此时我们在dir0下面。

  • import dir1.dir2.mod
  • from dir1.dir2.mod import x

如果有__init__.py文件,则是包导入,每个层级的目录都要有__init__.py(除了入口的顶层文件)

1. 使用import导包

使用import的形式,在使用的时候,有时候很麻烦(当__init__.py啥都不做的时候,只是一个空文件的时候)

#目录层级如下
"""
main.py
dir1\
 __init__.py
 dir2\
 	__init__.py
 	dir2Mod
"""
#main.py
import dir1.dir2.dir2Mod
print(dir1)
print(dir1.dir2)
dir1.dir2.dir2Mod.mod2() #可以看出只能这么调用它,前面要写一大串。

dir2Mod.py

def mod2():
    print('222')

此时必须借助__init__.py来优化使用体验

2. 使用from语句导包

文件结构还是上面的,再main.py中写

from dir1.dir2 import dir2Mod #方式 1
from dir1.dir2.dir2Mod import mod2 #方式2
print(dir2Mod)
dir2Mod.mod2()
mod2()
3. 相对导入(.)

在Python 3.0和Python 2.6中,我们可以使用from语句前面的点号来表示,导入应该相对于外围的包——这样的导入将只是在包的内部搜索,并且不会搜索位于导入搜索路径(sys.path)上某处的同名模块。直接效果是包模块覆盖了外部的模块。

  • 点号使用时,首先当前是一个包,即当前目录有__init__.py

  • 包内部搜索

例子:

#目录结构
"""
main.py
dir1/
	__init__.py
	dir2/
		__init__.py
		dir2Mod.py
		dir2Say.py
"""

#dir2Mod.py
"""
from .dir2Say import say
def mod2():
    say()
"""

#dir2Say.py
"""
def say():
    print('say hello')
"""
from dir1.dir2 import dir2Mod
dir2Mod.mod2()

形式:带点号的才是相对导入

  • from . import spam

    告诉Python把位于与语句中给出的文件相同包路径中的名为spam的一个模块导入

  • from .spam import name

    从名为spam的模块导入变量name,而这个spam模块与包含这条语句的文件位于同一个包下

  • import string

    在Python 3.0中,不带点号的一个import总是会引发Python略过模块导入搜索路径的相对部分,并且在sys.path所包含的绝对目录中查找。例如,在Python 3.0的方式中,如下形式的一条语句,总是在sys.path上的某处查找一个string模块,而不是查找该包中具有相同名称的模块。

  • from .. import moduleX

    引入该语句所属的包的兄弟节点包。

    #目录结构
    """
    main.py
    dir1/
    	__init__.py
    	dir2/
    		__init__.py
    		dir2Mod.py
        dir3/
        	__init__.py
        	dir3Mod.py
    """
    #dir2Mod.py
    from .. import dir3
    def mod2():
        dir3.mod3()
    

四、模块高级运用

正如我们所学过的,不导入一个文件,就无法存取该文件内所定义的变量名。

导入一个包(含__init__.py的目录),实际是导入这个__init__.py.

1. 最小化的from *的破坏:_x__call__

有种特定的情况,把下划线放在变量名前面(例如,_x),可以防止客户端使from*语句导入模块名时,把其中的那些变量名复制出去。这其实是为了对命名空间的破坏最小化而已。因为from*会把所有变量名复制出去,导入者可能得到超出它所需的部分(包括会覆盖导入者内的变量名的变量名)。下划线不是“私有”声明:你还是可以使用其他导入形式看见并修改这类变量名。例如,使用import语句。

*一个下划线的变量会使_x被from 处理,导致外部看不到。

__all__ = ["Error", "encode""decode"] # Export these only

使用此功能时,from*语句只会把列在__all__列表中的这些变量名复制出来。事实上,这和_x惯例相反:__all__是指出要复制的变量名,而_X是指出不被复制的变量名。Python会先寻找模块内的_all_列表;如果没有定义的话,from*就会复制出开头没有单下划线的所有变量名。

所以要想使_X变量导出来,可以写在__all__里面。

main.py

from dir1.say import *
import sys
hello()
print(_price)

dir1/say.py

__all__ = ['hello','_price']
_price=10
def hello():
    print('hello')

__all__列表只对from*语句这种形式有效,它并不是私有声明。

2. __name____main__

每个模块都有个名为__name__的内置属性,Python会自动设置该属性:

  • 如果文件是以顶层程序文件执行,在启动时,__name__就会设置为字符串__main__

  • 如果文件被导入,__name__就会改设成客户端所了解的模块名

结果就是模块可以检测自己的__name__来确定它是在执行还是在导入。例如,假设我们建立下面的模块文件,名为runme.py,它只导出了一个名为tester的函数。

def tester():
	print("test")
if __name__ == "__main__": #如果是被导入,则不会等于__main__,也就不会执行下面的语句了。
	tester()
3.修改模块搜索路径
sys.path.append('c:\\youCode\\someExamples')
4. import和from语句中的as扩展

形式:

  • import moduleName as newName

    #等价
    import moduleName
    newName = moduleName
    del moduleName
    
  • from moduleName import attrName as newName

  • import dir1.dir2.mod as mod

5. getattr和__dict__
import dir1.say  as say
import sys
say.hello()
hello = getattr(say,'hello')
hello()
sys.modules['dir1.say'].hello()
say.__dict__['hello']()
#效果等同。
6.使用字符串导入和__import__
"""
错误实例:
import "string"
或
x="string"
import x
"""
#正确方式1如下:
import sys
x="string"
exec("import "+x)
print(sys.modules[x])


#最推荐的方式2:
x="string"
string = __import__(x)
print(string)

五、类和OOP

1. 类对象提供默认行为
class Person:
    name = "paopao"

    @classmethod
    def echoName(cls):
        print(cls.name)

    def echoAge(self):
        pass
    
if __name__ == "__main__":
    print(Person.name) #paopao
    Person.echoName()
  • class语句创建类对象并将其赋值给变量名。就像函数def语句,Python class语句也是可执行语句。执行时,会产生新的类对象,并将其赋值给class头部的变量名。此外,就像def应用,class语句一般是在其所在文件导入时执行的。

  • **class语句内的赋值语句会创建类的属性。**就像模块文件一样,class语句内的顶层的赋值语句(不是在def之内)会产生类对象中的属性。**从技术角度来讲,class语句的作用域会变成类对象的属性的命名空间,就像模块的全局作用域一样。**执行class语句后,类的属性可由变量名点号运算获取object.name。

  • **类属性提供对象的状态和行为。**类对象的属性记录状态信息和行为,可由这个类所创建的所有实例共享。位于类中的函数def语句会生成方法,方法将会处理实例。

2. 实例对象是具体的元素
class Person:
    name = "paopao"
    age = 30

    @classmethod
    def echoName(cls):
        print(cls.name)

    def echoAge(self):
        print(self.age)

if __name__ == "__main__":
    p = Person()
    p.echoName() #可以调用
    p.echoAge()  #30
    p.name = "joker"
    Person.echoName() #paopao

实例化(调用)一个类,会得到一个新的对象

  • 像函数那样调用类对象会创建新的实例对象。每次类调用时,都会建立并返回新的实例对象。实例代表了程序领域中的具体元素。

  • 每个实例对象继承类的属性并获得了自己的命名空间。由类所创建的实例对象是新命名空间。一开始是空的,但是会继承创建该实例的类对象内的属性。

  • **在方法内对self属性做赋值运算会产生每个实例自己的属性。**在类方法函数内,第一个参数(按惯例称为self)会引用正在处理的实例对象。对self的属性做赋值运算,会创建或修改实例内的数据,而不是类的数据。

3. 类通过继承进行定制
class Person:
    name = "paopao"
    age = 30
    type = "Cosmic man"

    @classmethod
    def echoName(cls):
        print(cls.name)

    def echoAge(self):
        print(self.age)

class Earthman(Person):

    def echoType(self):
        print(self.type)

if __name__ == "__main__":
   e = Earthman()
   e.echoAge()
   e.echoType()
  • **超类(父类)列在了类开头的括号中。**要继承另一个类的属性,把该类列在class语句开头的括号中就可以了。含有继承的类称为子类,而子类所继承的类就是其超类。
  • **类从其超类中继承属性。**就像实例继承其类中所定义的属性名一样,类也会继承其超类中定义的所有属性名称。当读取属性时,如果它不存在于子类中,Python会自动搜索这个属性。
  • **实例会继承所有可读取类的属性。**每个实例会从创建它的类中获取变量名,此外,还有该类的超类。寻找变量名时,Python会检查实例,然后是它的类,最后是所有超类。
  • 每个object.attribute都会开启新的独立搜索。Python会对每个属性取出表达式进行对类树独立搜索。这包括在class语句外对实例和类的引用(例如,X.attr),以及在类方法函数内对self实例参数属性的引用**。方法中的每个self.attr表达式都会开启对self及其上层的类的attr属性的搜索。**
  • **逻辑的修改是通过创建子类,而不是修改超类。**在树中层次较低的子类中重新定义超类的变量名,子类就可取代并定制所继承的行为。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g09wZfdK-1676732517789)(./static/QQ截图20220519144105.png)]

超类的连接是通过class语句首行的括号内列出类而生成的。

简而言之,对经典类(python2)而言,继承搜索程序是绝对深度优先,然后才是由左至右。Python一路往上搜索,深入树的左侧,返回后,才开始找右侧。在新式类(python3)中,在这类情况下,搜索相对来说是宽度优先的。Python先寻找第一个搜索的右侧的所有超类,然后才一路往上搜索至顶端共同的超类。

class Human:
    width=100
    height=200
    color="black"

class Man(Human):
    __slots__ = ['width','color']

if __name__ == '__main__':
    m = Man()
    print(m.height) # 200
    # print(m.width) #报错,被限制了。
4. 类的运算符重载

运算符重载就是让用类写成的对象,可截获并响应用在内置类型上的运算:加法、切片、打印和点号运算等。

  • 以双下划线命名的方法(__X__)是特殊钩子。Python运算符重载的实现是提供特殊命名的方法来拦截运算。Python语言替每种运算和特殊命名的方法之间,定义了固定不变的映射关系。
  • **当实例出现在内置运算时,这类方法会自动调用。**例如,如果实例对象继承了__add__方法,当对象出现在+表达式内时,该方法就会调用。该方法的返回值会变成相应表达式的结果。
  • **类可覆盖多数内置类型运算。**有几十种特殊运算符重载的方法的名称,几乎可截获并实现内置类型的所有运算。它不仅包括了表达式,而且像打印和对象建立这类基本运算也包括在内。
  • **运算符覆盖方法没有默认值,而且也不需要。**如果类没有定义或继承运算符重载方法,就是说相应的运算在类实例中并不支持。例如,如果没有__add__,+表达式就会引发异常。
  • **运算符可让类与Python的对象模型相集成。**重载类型运算时,以类实现的用户定义对象的行为就会像内置对象一样,因此,提供了一致性,以及与预期接口的兼容性。

还有注意的一点,如果忘记运算符,可以查看object类。

1. 减号运算符__sub__
class Turtle:
    count = 1

    def __sub__(self,value):
        return self.count-value

if __name__ == "__main__":
    t = Turtle()
    print(t-10) #9
2. 常见运算符重载方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EL7ENLyA-1676732517790)(./static/QQ截图20220519152702.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dr9Ywvwd-1676732517791)(C:\Users\xy\AppData\Roaming\Typora\typora-user-images\image-20220519153949294.png)]

3. 索引和分片__getitem____setitem__
class Turtle:
    count = [1,2,3]

    def __getitem__(self,index):
        return  self.count[index]

    def __setitem__(self,index,value):
        self.count[index] = value

if __name__ == "__main__":
    t = Turtle()
    print(t[1]) #2
    t[1]=22
    print(t[:]) #2[1,22,3]
4. 迭代器对象__iter____next__

上面说的__getitem__也可以实现迭代。不过并没有__iter__级别优先。

class Turtle:
    count = [1,2,3]
    def __getitem__(self,index):
        return  self.count[index]

    def __setitem__(self,index,value):
        self.count[index] = value

if __name__ == "__main__":
    t = Turtle()
    for x in t:
        print(x)

从技术角度来讲,迭代环境是通过调用内置函数iter去尝试寻找_iter__方法来实现的,而这种方法应该返回一个迭代器对象。如果已经提供了,Python就会重复调用这个迭代器对象的next方法,直到发生StopIteration异常。如果没找到这类__iter__方法,Python会改用__getitem__机制,就像之前那样通过偏移量重复索引,直到引发IndexError异常(对于手动迭代来说,一个next内置函数也可以很方便地使用:next(B)与B.__next__()是相同的)。

class Array:
    def __init__(self,start,stop):
        if  not ( isinstance(start,int) and isinstance(stop,int)) :
            raise TypeError('Not integer type')
       
        if start > stop:
            start,stop = stop,start
        
        self.start = start - 1
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.start >= self.stop:
            raise StopIteration
        self.start+=1
        return self.start

if __name__ == "__main__":
    t = Array(1,5)
    for x in t:
        print(x,end=" ") #1 2 3 4 5

关于迭代器的原理,详见迭代器章节。

5. __contains__和in操作符
class Array:
    def __init__(self,array):
       self.array = array 

    def __contains__(self,value):
        try:
           return bool(self.array.index(value))
        except ValueError:
            return False
       

if __name__ == "__main__":
    t = Array([2,5,4])
    if 8 in t:
        print('is in ')
    else:
        print('not in')
6. __getattr____setattr__属性调用
  • __getattr__方法是拦截属性点号运算。更确切地说,当通过对未定义(不存在)属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果Python可通过其继承树搜索流程找到这个属性,该方法就不会被调用。
  • 有个相关的重载方法__setattr__会拦截所有属性的赋值语句。如果定义了这个方法,self.attr = value会变成self.__setattr__( ‘attr’,value)。这一点技巧性很高,因为在__setattr__中对任何self属性做赋值,都会再调用__setattr__,导致了无穷递归循环(最后就是堆栈溢出异常))。如果想使用这个方法,要确定是通过对属性字典做索引运算来赋值任何实例属性的(下一节讨论)。也就是说,是使用self.__dict__[ ‘name’ ] = x,而不是self.name = x。
class Person:
    name = 'joker'

    def __getattr__(self,attr):
       if attr == 'name':
           return self.__name 
       
    def __setattr__(self,attrName,value):
        if attrName == 'name':
            self.__dict__[attrName] = value

if __name__ == "__main__":
    p = Person()
    p.name = 'big'
    print(p.name)#big
7. 其他属性管理工具
  • __getattribute__方法拦截所有的属性获取,而不只是那些未定义的,但是,当使用它的时候,必须比使用__getattr__更小心地避免循环。
  • Property内置函数允许我们把方法和特定类属性上的获取和设置操作关联起来。
  • 描述符提供了一个协议,把一个类的__get____set__方法与对特定类属性的访问关联起来。
8. __repr____str__会返回字符串表达形式
  • 打印操作会首先尝试_str__和str内置函数(print运行的内部等价形式)。它通常应该返回一个用户友好的显示。

  • __repr_用于所有其他的环境中:用于交互模式下提示回应以及repr函数,如果没有使用__str__,会使用print和str。它通常应该返回一个编码字符串,可以用来重新创建对象,或者给开发者一个详细的显示。

  • 总而言之,__repr__用于任何地方,除了当定义了一个_str_的时候,使用print和str。然而要注意,如果没有定义__str__,打印还是使用__repr__,但反过来并不成立—–其他环境,例如,交互式响应模式,只是使用__repr__,并且根本不要尝试__str__:

class Person:
    def __str__(self): #可不写这个,用下面那个函数就可
        return "class Person"

    def __repr__(self):
        return "class Person 2" 

if __name__ == "__main__":
    p = Person()
    print(p)

__repr__是最佳选择.两者必须返回字符串。

9. 右侧加法和原地加法:__radd____iadd__
class Commuter(object):
    def __init__(self,val):
        self.val = val
    
    def __radd__(self,other):#右侧加法
        return self.val + other

    def __iadd__(self,other): #查看object基类,发现必须返回self对象。
        self.val += other 
        return self

    def __repr__(self):
        return "val is {}".format(self.val)

    def echoVal(self):
        print(self.val)
        
if __name__ == '__main__':
    t = Commuter(99)
    val = 1 + t #不是t+1哦,t+1是__add__。
    print(val)#100

    t += 1000
    t.echoVal()
    print(t)
10. __call__
class Commuter:
    def __init__(self,val):
        self.val = val
    
    def __call__(self, *args, **kwds) :
        print('__call__',args,kwds)

    def echoVal(self,*args,**kwargs):
        print(args,kwargs)
        
if __name__ == '__main__':
    t = Commuter(99)
    t(1,2,3,4) #这里调用了一次__call__; _call__ (1, 2, 3, 4) {}
    t.echoVal(1,2,3,4,5,name="joker",age=30)#这里没调用。
11. 对象析构函数: __del__

每当实例空间被收回时(在垃圾收集时),__del__,也就是析构函数(destructor method),就会自动执行。

class Commuter:

    def __del__(self):
        print('good bye')

if __name__ == '__main__':
    t = Commuter()
    t = 1

基于某些原因,在Python中,析构函数不像其他OOP语言那么常用。

12. __slots__限制
class Human:
    width=100
    height=200
    color="black"

class Man(Human):
    __slots__ = ['width','color']

if __name__ == '__main__':
    m = Man()
    print(m.height) # 200
    # print(m.width) #报错,被限制了。
    m.width = 300#但是能被赋值。

这个特殊属性一般是在clas s语句顶层内将字符串名称顺序赋值给变量_slots_而设置:只有_slots_列表内的这些变量名可赋值为实例属性。然而,就像Python中的所有变量名,实例属性名必须在引用前赋值,即使是列在__slots__中也是这样。

13.__get____set__
object.__get__(self, instance, owner) 
object.__set__(self, instance, value) 

get: 调用以获取所有者类的属性(类属性访问)或该类的实例的属性(实例属性访问)。owner始终是自身类,而instance是通过其访问属性的实例(可以是其他类的实例对象),或者在通过owner访问属性时为None。此方法应返回(计算的)属性值或引发AttributeError异常。

参考装饰器-函数装饰器-类装饰类方法一节。

set:调用以将所有者类的实例实例上的属性设置为新值value。

14.__new__

调用以创建一个类的新实例。cls.__new__()是一个静态方法(因为是特例所以你不需要显式声明)。它将会给这个实例的类

5. 构造函数

赋给实例属性值的通常方法是,在__init__构造函数方法中将它们赋给self,构造函数方法包含了每次创建一个实例的时候Python会自动运行的代码。让我们给自己的类添加一个构造函数:

class Turtle:

   def __init__(self,name) -> None:
       self.name = name

if __name__ == "__main__":
    t = Turtle('joker')
    print(t.name)
  • _init_实在没有什么奇妙之处,只不过当创建一个实例的时候,会自动调用它(__init__)并且它有特殊的第一个参数。尽管它的名字很怪异,它仍然是一个常规的函数.
6. 方法

方法有四种: 1. 实例方法(实例对象持有) ;2. 静态方法 3. 类方法。4. 无绑定实例的——函数

1.实例方法

这里讲实例方法,其他的查看其他小节。

  • 方法只是附加给类并旨在处理那些类的实例的常规函数。实例是方法调用的主体,并且会自动传递给方法的self参数。
class Turtle:

   def summonTheGods(self,name):
       print("{} god is comming".format(name))

if __name__ == "__main__":
    t = Turtle()
    t.summonTheGods('Taiyi')

方法调用的本质:

instance.method(args.. .)
这会自动翻译成以下形式的类方法函数调用:
class.method(instance,args.. .)

第二种方法可以真的使用。第一个参数将实例对象传递进去即可。

2. 函数方法(基本上可以替代类方法和静态方法)
class Commuter:
    def echo(val,val2):
        print(val,val2)

if __name__ == '__main__':
    t = Commuter()
    # t.echo(1,2) 不能被实例调用
    Commuter.echo(1,2)
   

在Python 3.0中(以及随后的Python 3.X版本中),对一个无self方法的调用使得通过类调用有效,但从实例调用失效:

3.静态方法和类方法
class Human:
   
    def echo(v):
        print(v)
    
    def render(v):
        print(v)

    echoVal = staticmethod(echo)
    renderVal = classmethod(render)

if __name__ == '__main__':
    Human.echoVal(111)
    Human.renderVal(222)

再不适用装饰器的前提下,使用内置的staticmethod和classmethod来特殊化一个简单的方法。

**好处:异化。**python3中简单函数就绝大部分功能替代静态方法和类方法。

#装饰器版本
class Human:
   
    @staticmethod
    def echo(v):
        print(v)
    
    @classmethod
    def render(cls,v):
        print(v)


if __name__ == '__main__':
    Human.echo(111)
    Human.render(222)
7. 特殊的类属性
  • 内置的instance.__class__属性提供了一个从实例到创建它的类的链接。类反过来有一个__name__(就像模块一样),还有一个__bases__序列,提供了超类的访问。我们使用这些来打印创建一个实例的类的名字,而不是通过硬编码来做到。
  • 内置的object.__dict__属性提供了一个字典,带有一个键/值对,以便每个属性都附加到一个命名控件对象(包括模块、类和实例)。由于它是字典,因此我们可以获取键的列表、按照键来索引、迭代其键,等等,从而广泛地处理所有的属性。我们使用这些来打印出任何实例的每个属性,而不是在定制显示中硬编码。
8. 伪私有属性

只在方法名前面使用两个下划线符号:对我们的例子来说就是__x。Python自动扩展这样的名称,以包含类的名称,从而使它们变得真正唯一。这一功能通常叫做伪私有类属性.

我们将推迟到第38章再给出属性私有性的一个更完整的解决方案,在那里,我们将使用类装饰器来更加通用地拦截和验证属性。

class Commuter:
    __name="joker"
    age=30
    def echo(self):
        print(self.__dir__())

if __name__ == '__main__':
    t = Commuter()
    t.echo()
    print(t.age)
    print(t.__name) #报错

为啥打印t.__name会报错呢?因为python将两个下划线的变量,在外部访问的时候,自动变更为_Commuter__name,所以看上去不可见,而内部访问不会变。

9. 类的设计
1. OOP
  • 继承是基于Python中的属性查找的(在X.name表达式中)。
  • 多态:在x.method方法中,method的意义取决于X的类型(类)。
  • 封装:方法和运算符实现行为,数据隐藏默认是一种惯例。
2. 函数重载(python中,是不存在的)
class Commuter:

    def echo(self,val):
        print(val)
    def echo(self,val,val2):#同名函数,最后一个生效
       print(val,val2)

if __name__ == '__main__':
    t = Commuter()
    t.echo(1)

六、管理属性

本章包括以下内容:

  • __getattr____setattr__ 方法,把未定义的属性获取和所有的属性赋值指向通用的处理器方法。

  • __getattribute__ 方法,把所有属性获取都指向Python 2.6的新式类和Python 3.0的有类中的一个泛型处理器方法。

  • property内置函数,把特定属性访问定位到get和set处理器函数,也叫做特性(Property)。

  • 描述符协议,把特定属性访问定位到具有任意get和set处理器方法的类的实例。

1. 特性(Property)
class Person:
    name = 1

    def __init__(self,name):
        self.name = name 

p = Person('k')
print(p.name) #'k'
p.name = 'b'
print(p.name) #'b'
del p.name 
print(p.name) #1
del p.name 
print(p.name) #报错:AttributeError: name

但是在python2.6及之前:使用property

class Person:
    def __init__(self,name):
        self._name = name 

    def getName(self):
        return self._name

    def setName(self,value):
        self._name = value 
    
    def delName(self):
        del self._name
    name = property(getName,setName,delName,'name docs')
p = Person('bob,smit')
print(p.name) #'bob,smit'
p.name = 'joker'
print(p.name)  #'joker'
del p.name
print(p.name) #报错
2. 特性@property
class Person:
    
    @property
    def name(self):
        return 'joker'

p = Person()
print(p.name)

等同于:

由于这一映射,证实了内置函数 property 可以充当一个装饰器,来定义一个函数,当获取一个属性的时候自动运行该函数:

class Person:
    def name(self): 
    	name = property(name)

对于Python 2.6,property对象也有 getter 、 setter 和 deleter 方法,这些方法指定相应的特性访问器方法赋值并且返回特性自身的一个副本。

class Person:
    def __init__(self,name='None'):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self,value):
        self._name = value
    
    @name.deleter
    def name(self):
        del self._name

p = Person()
print(p.name)
p.name = '22'
print(p.name)
3. 描述符

描述符提供了拦截属性访问的一种替代方法;它们与前面小节所讨论的特性有很大的关系。实际上,特性是描述符的一种——从技术上讲, property 内置函数只是创建一个特定类型的描述符的一种简化方式,而这种描述符在属性访问时运行方法函数。

从功能上讲,描述符协议允许我们把一个特定属性的get和set操作指向我们提供的一个单独类对象的方法:它们提供了一种方式来插入在访问属性的时候自动运行的代码,并且它们允许我们拦截属性删除并且为属性提供文档(如果愿意的话)。

描述符作为独立的类创建,并且它们就像方法函数一样分配给类属性。和任何其他的类属性一样,它们可以通过子类和实例继承。通过为描述符自身提供一个 self ,以及提供客户类的实例,都可以提供访问拦截方法。因此,它们可以自己保留和使用状态信息,以及主体实例的状态信息。例如,一个描述符可能调用客户类上可用的方法,以及它所定义的特定于描述符的方法。

和特性一样,描述符也管理一个单个的、特定的属性。尽管它不能广泛地捕获所有的属性访问,但它提供了对获取和赋值访问的控制,并且允许我们自由地把简单的数据修改为计算值从而改变一个属性,而不会影响已有的代码。特性实际上只是创建一种特定描述符的方便方法,并且,正如我们所见到的,它们可以直接作为描述符编写。

  • 描述符作为单独的类编写
  • 针对想要拦截的属性访问操作提供特定命名的访问器方法
  • 描述符管理一个单个的、特定的属性
    • owner: 指定了描述符实例要附加到的类
    • instance: 要么是访问的属性所属的实例(用于instance.attr ),
      要么当所访问的属性直接属于类的时候是None(用于class.attr)
class Descriptor:
    """
    owner: 指定了描述符实例要附加到的类
    instance: 要么是访问的属性所属的实例(用于instance.attr ),
            要么当所访问的属性直接属于类的时候是None(用于class.attr)
    """
    def __get__(self,instance,owner):pass # 返回attr value

    def __set__(self,instance,value):pass #返回None

    def __delete__(self,instance):pass #返回None

class Des:
    def __get__(self,instance,owner):
        print(self,instance,owner,sep='\n')

class Sub:
    attr = Des()

x = Sub()
x.attr

输出:

<__main__.Des object at 0x00000260602FBE80>
<__main__.Sub object at 0x00000260602FBE50> #不一样哦,
<class '__main__.Sub'>

在上面的例子中,当获取 x.attr 的时候,Python自动运行 Des类的__get__ 方法

还要注意不要把描述符 __delete__ 方法和通用的 __del__ 方法搞混淆了。调用前者是试图删除所有者类的一个实例上的管理属性名称;后者是一种通用的实例析构器方法,当任何类的一个实例将要进行垃圾回收的时候调用。 __delete__ 与我们将要在本章后面遇到的__delattr__ 泛型属性删除方法关系更近。

1. 只读描述符

和特性不同,使用描述符直接忽略 __set__ 方法不足以让属性成为只读的,因为描述符名称可以赋给一个实例

In [1]: class D:
   ...:     def __get__(*args):print('get')
   ...:

In [2]: class C:
   ...:      a = D()
   ...:

In [3]: X = C()

In [4]: X.a
get

In [5]: C.a
get

In [6]: X.a = 100

In [7]: X.a
Out[7]: 100

In [8]: C.a
get

这就是Python中所有实例属性赋值工作的方式,并且它允许在它们的实例中类选择性地覆盖类级默认值。要让基于描述符的属性成为只读的,捕获描述符类中的赋值并引发一个异常来阻止属性赋值——当要赋值的属性是一个描述符的时候,Python有效地绕过了
常规实例层级的赋值行为,并且把操作指向描述符对象.

class D:
    def __get__(*args):print('get')
    def __set__(*args):raise AttributeError('cannot set')

class C:
    a = D()

x = C()
x.a #'get'
x.a = 100 # 'AttributeError: cannot set'
2. 完整实例
class Name:
    def __get__(self,instance,owner):
        print('fetch')
        return instance._name
    def __set__(self,instance,value):
        print('change')
        instance._name = value

    def __delete__(self,instance):
        print('remove')
        del instance._name

class Person:
    def __init__(self,name):
        self._name = name
    name = Name()

if __name__ == '__main__':
    bob = Person('bob smith')
    print(bob.name)
    bob.name = 'Robert smit'
    print(bob.name)
    del bob.name

输出:

fetch
bob smith
change
fetch
Robert smit
remove
  • self是Name类实例

  • instance 是 Person 类实例

  • owner 是 Person 类,不是实例

还要注意到,当一个描述符类在客户类之外无用的话,将描述符的定义嵌入客户类之中,这在语法上是完全合理的。

class Person:
    class Name:
        def __get__(self,instance,owner):
            print('fetch')
            return instance._name
        def __set__(self,instance,value):
            print('change')
            instance._name = value

        def __delete__(self,instance):
            print('remove')
            del instance._name
    def __init__(self,name):
        self._name = name
    name = Name()

if __name__ == '__main__':
    bob = Person('bob smith')
    print(bob.name)
    bob.name = 'Robert smit'
    print(bob.name)
    del bob.name
3. 计算属性

实际上,描述符也可以用来在每次获取属性的时候计算它们的值。

class DescSquare:
    def __init__(self, start): # 每一个DescSquare都有一个独立的实例状态
        self.value = start
    def __get__(self, instance, owner): #获取时进行计算
        return self.value ** 2
    def __set__(self, instance, value): #设置,这里没进行操作,只是简单的赋值,好处就是能保留传进来的原始值。
        self.value = value 

class Client1:
    x = DescSquare(0)

if __name__ == '__main__':
    c = Client1()
    print(c.x) #0
    c.x = 5
    print(c.x) #25

有没有点像vue的computed,不过python中的计算属性,不是响应式的。

  • 描述符状态用来管理内部用于描述符工作的数据。(如DescSquare)
  • 实例状态记录了和客户类相关的信息,以及可能由客户类创建的信息。(如client1)
4. __getattr____getattribute__

到目前为止,我们已经学习了特性和描述符——管理特定属性的工具。 __getattr____getattribute__ 操作符重载方法提供了拦截类实例的属性获取的另一种方法。

就像特性和描述符一样,它们也允许我们插入当访问属性的时候自动运行的代码。然而,我们将会看到,这两个方法有更广泛的应用。

  • __getattr__ 针对未定义的属性运行——也就是说,属性没有存储在实例上,或者没有从其类之一继承。
  • __getattribute__ 针对每个属性,因此,当使用它的时候,必须小心避免通过把属性访问传递给超类而导致递归循环。

这两个方法是一组属性拦截方法的代表,这些方法还包括 __setattr____delattr__

  • 在Python 2.6中, __getattr__ 将会拦截对__str____repr__ 这样的运算符重载方法的访问,但是,在Python 3.0中不会这样.
  • 此外,在Python 3.0中,针对打印和 + 这样的内置操作显式地调用属性并不会通过 __getattr__ (或其近亲
    __getattribute__ )路由。

七、装饰器

1. 什么是装饰器

装饰是为函数和类指定管理代码的一种方式。装饰器本身的形式是处理其他的可调用对象的可调用的对象(本身是函数或类)

  • 函数装饰器 在函数定义的时候进行名称重绑定,提供一个逻辑层来管理函数和方法或随后对它们的调用。
  • 类装饰器 在类定义的时候进行名称重绑定,提供一个逻辑层来管理类,或管理随后调用它们所创建的示例

简而言之,装饰器提供了一种方法,在函数和类定义语句的末尾插入自动运行代码——对于函数装饰器,在 def 的末尾;对于类装饰器,在 class 的末尾。

对于更为通用的任务,程序员可以编写自己的任意装饰器。例如,函数装饰器可能通过添加跟踪调用、在调试时执行参数验证测试、自动获取和释放线程锁、统计调用函数的次数以进行优化等的代码来扩展函数。你可以想象添加到函数调用中的任何行为,都可以作为定制函数装饰器的备选。

另外一方面,函数装饰器设计用来只增强一个特定函数或方法调用,而不是一个完整的对象接口。类装饰器更好地充当后一种角色——因为它们可以拦截实例创建调用,它们可以用来实现任意的对象接口扩展或管理任务。

很多类装饰器与我们见到的委托编程模式有很大的相似之处。

def tracer(func):
    call = 0
    print('do?')
    def wrapper(*args,**kwargs):
        nonlocal call
        call += 1
        print('call {} to {}'.format(call,func.__name__))
        func(*args,**kwargs)
    return wrapper

@tracer #装饰的修饰在这个位置的时候,就会运行print("do?)语句了。然后返回wrapper.
def spam(a,b,c):
    print(a+b+c)

当主体函数或类定义的时候,装饰器应用一次;在对类或函数的每次调用的时候,不必添加额外的代码(在未来可能必须改变)。

更概括地说,有一种常用的编码模式可以包含这一思想——装饰器返回了一个包装器,包装器把最初的函数保持到一个封闭的作用域中(有没有点像闭包函数)

2. 装饰器管理函数和类

大多数实例都使用包装器来拦截随后对函数和类的调用,但这并非使用装饰器的唯一方法:

  • 函数装饰器 可以用来管理函数对象,而不是随后对它们的调用——例如,把一个函数注册到一个API。然而,我们在这里主要关注更为常见的用法,即调用包装器应用程序
  • 类装饰器 也可以用来直接管理类对象,而不是实例创建调用——例如,用新的方法扩展类。因为这些用法和元类有很大的重合(实际上,都是在类创建过程的最后运行),我们将在下一章看到更多的例子。

换句话说,函数装饰器可以用来管理函数调用和函数对象,类装饰器可以用来管理类实例和类自身。

3. 函数装饰器

装饰的对象是函数

函数装饰器是一种关于函数的运行时声明,函数的定义需要遵守此声明(具体更清晰的理解,见第一小节。)。装饰器在紧挨着定义一个函数或方法的 def 语句之前的一行编写,并且它由 @ 符号以及紧随其后的对于元函数的一个引用组成——这是管理另一个函数的一个函数(或其他的可调用对象)。

@decorator 
def f(arg):
    pass
f(99)

等同如下:

def f(arg):
    pass
f = decorator(f)
f(99)

装饰器自身是一个返回可调用对象的可调用对象。也就是说,它返回了一个对象,当随后装饰的函数通过其最初的名称调用的时候,将会调用这个对象——不管是拦截了随后调用的一个包装器对象,还是最初的函数以某种方式的扩展。

1. 函数装饰函数
def tracer(func):
    call = 0
    def wrapper(*args,**kwargs):
        nonlocal call
        call += 1
        print('call {} to {}'.format(call,func.__name__))
        return func(*args,**kwargs)
    return wrapper

@tracer
def spam(a,b,c):
    print(a+b+c)

if __name__ == '__main__':
    spam(1,2,3)
    spam(3,4,5)

"""
call 1 to spam
6
call 2 to spam
12
"""

在执行spam(1,2,3)时,等同如下:

t = tracer(spam)
t(spam)(1,2,3)
t(spam)(4,5,6)

每次tracer修饰不同函数,都会产生一个封闭的作用域。(比如即修饰spam函数,又修饰其他函数如spam2函数,就会又两个封闭作用域。)

  • nonlocal 语句,允许修改封闭的函数作用域变量,所以它们可以充当针对每次装饰的、可修改的数据。比如上面的call变量。

  • 另一种替代nonlocal的方法,就是使用函数属性。在py3中:

    def tracert(func):
        def wrapper(*args,**kwargs):
            wrapper.counts +=1
            print("call %s to %s"%(wrapper.calls,func.__name__))
            return func(*args,**kwargs)
        wrapper.counts = 0
        return wrapper
    
2. 类装饰函数(装饰器是一个类)
class Tracer:
    def __init__(self,func):
        self.calls = 0
        self.func = func

    def __call__(self,*args,**kwargs):
        self.calls += 1
        print("call {} to {}".format(self.calls,self.func.__name__))
        self.func(*args,**kwargs)

tracer = Tracer

@tracer
def spam(a,b,c):
    print(a+b+c)

if __name__ == '__main__':
    #spam是类的实例。
    spam(1,2,3)
    spam(3,4,5)

"""
call 1 to spam
6
call 2 to spam
12
"""
  • 用这个类装饰的每个函数将创建一个新的类实例,带有自己保存的函数对象和调用计数器。(是在修饰的时候,就创建了,而不是spam调用的时候)
  • 现在,随后调用spam的时候,它确实会调用装饰器所创建的(装饰的时候,就创建了)实例的__call__运算符重载方法。
  • 然后,__call__方法可能运行最初的func。按照这种方式编写代码的时候,每个装饰的函数都会产生一个新的实例来保持状态。
3.类装饰类方法
from functools import wraps
class tracer:
    def __init__(self,func):
        self.calls = 0
        self.func = func

    def __call__(self,*args):
        print(self)
        self.calls += 1
        print("call {} to {}".format(self.calls,self.func.__name__))
        self.func()

class Person:

    @tracer
    def rise(self):
        print('I am rising')

if __name__ == '__main__':
    p = Person()
    p.rise()
    
"""
C:\Users\xy\Desktop\projects\gaodengshuxue\example>python main.py
<__main__.tracer object at 0x00000218D66EBFD0>
call 0 to rise
Traceback (most recent call last):
  File "C:\Users\xy\Desktop\projects\gaodengshuxue\example\main.py", line 21, in <module>
    p.rise()
  File "C:\Users\xy\Desktop\projects\gaodengshuxue\example\main.py", line 11, in __call__
    self.func()
TypeError: Person.rise() missing 1 required positional argument: 'self
"""

上面是失败的!!!

因为调用__call__是传进去的self是tracer的实例对象,但是在__call__最后一行有问题:self.func=》是在tracer实例上的,导致执行p.rise()方法中,rise中self没有指向任何实例。。

解决方法一:使用描述符的__get__方法在调用的时候接受描述符类和主体类实例。

描述符也能够拥有 __set__ 和 __del__ 访问方法,但是,我们在这里不需要它们。现在,由于描述符的 __get__ 方法在调用的时候接收描述符类和主体类实例,因此当我们需要装饰器的状态以及最初的类实例来分派调用的时候,它很适合于装饰方法。

class Tracer:
    def __init__(self,func):
        self.calls = 0
        self.func = func
    def __call__(self, *args, **kwargs):
        self.calls +=1
        print('calls %s to %s'%(self.calls,self.func.__name__))
        return self.func(*args,**kwargs)
    def __get__(self,instance,owner):
        """
            owner: 指定了描述符实例要附加到的类
            instance: 要么是访问的属性所属的实例(用于instance.attr ),
                    要么当所访问的属性直接属于类的时候是None(用于class.attr)
        """
        return Wrapper(self,instance)
class Wrapper:
    def __init__(self,desc,subj):
        """
        desc: Tracer的实例对象
        subj: Person的实例对象。
        """
        self.desc = desc
        self.subj = subj
    def __call__(self, *args, **kwargs):
        return self.desc(self.subj,*args,**kwargs)

tracer = Tracer

@tracer
def spam(a,b,c):
    print(a+b+c)

spam(1,2,3)
spam(a=4,b=5,c=6)

class Person:
    def __init__(self,pay):
        self.pay = pay
    @tracer
    def giveRaise(self,percent):
        self.pay *= (1.0 + percent)
        print(self.pay)
        return self.pay
print('-------------------')
bob = Person(5600)
sue = Person(8600)
bob.giveRaise(0.1)
sue.giveRaise(0.3)
  • spam变量名经过@tracer装饰后,指向了Tracer的实例对象。当继续调用spam()时,执行了__call__方法。函数没有涉及到到__get__方法

  • bob = Person(5600)创建Person对象没错,关键在与bob.giveRaise()的调用:

    • 修饰类方法的时候(还没创建实例前),这个giveRaise就创建一个Tracer的实例。

    • 当在执行bob.giveRaise的时候,先调用了__get__方法,它返回了一个Wrapper的实例对象,并且这个对象,即封装了Tracer实例对象,也封装了Person实例对象。

    • 执行bob.giveRaise的最后一步是,再调用了__call__方法。注意触发的是上一步返回的实例对象的__call__方法,即Wrapper实例的__call__方法。

      def __call__(self, *args, **kwargs):
          """
              desc: Tracer的实例对象
              subj: Person的实例对象。
          """
          return self.desc(self.subj,*args,**kwargs)
      

      这个__call__,触发了Tracer实例对象的__call__方法,传递进去了Person的实例对象,此时才不会报错。

解决方法二: 修改装饰器形式:

def tracer(func):
    calls = 0
    def onCall(*args,**kwargs):
        nonlocal  calls
        calls +=1
        print('call %s to %s'%(calls,func.__name__))
        return func(*args,**kwargs)
    return onCall

@tracer
def spam(a,b,c):
    print(a+b+c)

spam(1,2,3)
spam(a=4,b=5,c=6)

class Person:
    def __init__(self,pay):
        self.pay = pay
    @tracer
    def giveRaise(self,percent):
        self.pay *= (1.0 + percent)
        print(self.pay)
        return self.pay
print('-------------------')
bob = Person(5600)
sue = Person(8600) #两个类实例,其实共享同一个calls变量,但是是不同Person的实例。
bob.giveRaise(0.1)
sue.giveRaise(0.3)

"""
call 1 to spam
6
call 2 to spam
15
-------------------
call 1 to giveRaise
6160.000000000001
call 2 to giveRaise
11180.0
"""

教训: 如果你想要装饰器在简单函数和类方法上都有效,最好使用基于嵌套函数的编码模式,而不是带有调用拦截的类。(能简单就简单点)

4. 类装饰器

装饰的对象是类

@decorator
class C:
    pass

x = C(99)

等同于下面的语法——类自动地传递给装饰器函数,并且装饰器的结果返回来分配给类名:

class C:
	pass
C = decorator(C)
x = C(99)

直接的效果就是,随后调用类名会创建一个实例,该实例会触发装饰器所返回的可调用对象,而不是调用最初的类自身。

尽管先编码,但装饰器的结果是当随后创建一个实例的时候才运行的.

def decorator(cls):
    print("do1?") #在装饰器装饰的时候就执行力了。然后返回Wrapper类。
    class Wrapper:
        def __init__(self,*args):
            print("do2?") #在这一步x = C('china')执行的
            self.wrapped = cls(*args)
        def __getattr__(self,name):
            return getattr(self.wrapped,name)
    return Wrapper

@decorator
class C:
    def __init__(self,name):
        self.name = name

x = C('china')
print(x.name) #china

"""
#附:
#getattr用法
name = 'a'
getattr(A,'name')
'a
"""

装饰器把类的名称重新绑定到另一个类,这个类在一个封闭的作用域中保持了最初的类,并且当调用它的时候,创建并嵌入了最初的类的一个实例。当随后从该实例获取一个属性的时候,包装器的 __getattr__ 拦截了它,并且将其委托给最初的类的嵌入的实例。

此外,每个被装饰的类都创建一个新的作用域,它记住了最初的类

1. 单体类

下面使用实现一个单体类: Spam只被实例化了一次。

class singleton:
    def __init__(self,aClass):
        #aClass是被修饰的类
        print('do?') # 在装饰的时候,就实例化了。然后返回了一个singleton的实例对象给Spam
        self.aClass = aClass
        self.instance = None
    def __call__(self, *args, **kwargs):
        print('1')   # s = Spam('joker')时才执行的。
        if self.instance == None:
            self.instance = self.aClass(*args)
        return self.instance #返回Spam实例对象。

@singleton
class Spam:
    def __init__(self,val):
        self.val = val

if __name__ == "__main__":
    print(Spam) # Spam在被装饰的时候,Spam变量名就指向了Singleton的实例对象。
    s = Spam('joker')
    s2 = Spam('joker2')
    print(s,s2) #都是Spam的同一个实例
2. 跟踪对象接口

上面一个单体示例使用类装饰器来管理一个类的所有实例。类装饰器的另一个常用场景是每个产生实例的接口。类装饰器基本上可以在实例上安装一个包装器逻辑层,来以某种方式管理对其接口的访问。

__getattr__ 运算符重载方法作为包装嵌入的实例的整个对象接口的一种方法,以便实现委托编码模式。我们在前一章介绍的管理的属性中看到过类似的例子。还记得吧,当获取未定义的属性名的时候, __getattr__ 会运行;我们可以使用这个钩子来拦截一个控制器类中的方法调用,并将它们传递给一个嵌入的对象。

为了便于参考,这里给出最初的非装饰器委托示例,它在两个内置类型对象上工作:

class Wrapper:
    def __init__(self,object):
        self.wrapped = object
    def __getattr__(self, attrname):
        t = getattr(self.wrapped,attrname)
        #追踪: append ;对象: <built-in method append of list object at 0x000002496492D700>
        print("追踪:",attrname,";对象:",t)
        return t

if __name__ == '__main__':
    x = Wrapper([1,2,3])
    x.append(4) #x.append 获取到这个<built-in method append of list object at 0x000002496492D700>
    #之后的(4)才是执行这个<built-in method append of list object at 0x000002496492D700> 方法
    print(x.wrapped)

记住: 是x.append触发Wrapper的拦截器,然后该拦截器根据名称返回了这个lis.append的函数体。

类装饰器为编写这种 __getattr__ 技术来包装一个完整接口提供了一个替代的、方便的方法。

def Tracer(aClass):
    print(aClass) #这就是出传递进来的Spam或Person
    class Wrapper:
        def __init__(self,*args,**kwargs):
            print("do?") #装饰器在装饰的时候,没有进入到这里
            self.fetches = 0

            self.wrapped = aClass(*args,**kwargs)
        def __getattr__(self, attrname):
            print("追踪:"+attrname)
            self.fetches +=1
            return getattr(self.wrapped,attrname)
    print('outdo?') #装饰器在装饰的时候,只返回一个Wrapper.
    return Wrapper #等同于将Spam或Person封装在Wrapper中,并返回Wrapper.

@Tracer
class Spam:
    def display(self):
        print('spam!' * 2)


@Tracer
class Person:
    def __init__(self,name,hours,rate):
        self.name = name
        self.hours = hours
        self.rate = rate
    def pay(self):
        return self.hours * self.rate

if __name__ =="__main__":
    s = Spam()
    s.display()

    p = Person("joker",1,1)
    p.pay()
    print(s.fetches) #注意s.fetches不会触发__getattr__拦截器

注意: s.fetches不会触发__getattr__拦截器. 为什么呢,在管理属性那一章节,我们说过:

__getattr__ 针对未定义的属性运行——也就是说,属性没有存储在实例上,或者没有从其类之一继承。所以上面的例子Wrapper实例对象有fetches,但是没有display,还有pay。

3. 类错误:保持多个实例
class Tracer:
    def __init__(self,aClass):
        print("do?") #在装饰的时候就执行了,将Spam类保存到这里。
        self.aClass = aClass

    def __call__(self, *args, **kwargs):
        print('call?') #在s = Spam()的时候就执行了
        # s = Spam(),还没运行到()时,Spam表示Tracer的实例对象,此时Tracer的实例对象在进行()运算,就调用了__call__
        self.wrapped = self.aClass(*args,**kwargs)
        return self

    def __getattr__(self, attrname):
        print("追踪:"+attrname) #s.diplay的时候,还没运行到()的时候,被__getattr__拦截,
        # 然后返回了之前__call__()中实例化的Spam对象的display方法。然后该方法进行()运算。
        return getattr(self.wrapped,attrname)

@Tracer
class Spam:
    def display(self):
        print('Spam'*2)

if __name__ == "__main__":
    s = Spam()
    s.display()

对于一个给定的类的多个实例并不是很有效:每个实例构建调用会触发 __call__这会覆盖前面的实例(装饰的时候,Tracer已经实例化了,所以wrapped会被覆盖。)。直接效果是 Tracer 只保存了一个实例,即最后创建的一个实例。

5. 装饰器的嵌套

为了支持多步骤的扩展,装饰器语法允许我们向一个装饰的函数或方法添加包装器逻辑的多个层。当使用这一功能的时候,每个装饰器必须出现在自己的一行中。这种形式的装饰器语法:

@A
@B
@C
def f(...):
	...

如下这样运行:

def f(...):
	...
f = A(B(C()))

就像对函数一样,多个类装饰器导致了多个嵌套的函数调用,并且可能导致围绕实例创建调用的包装器逻辑的多个层。

@spam
@eggs
class C:
	...
	
x = C()

等同如下代码:

class C:
	...

C = spam(eggs(C))
x = C()

再次,每个装饰器都自由地返回最初的类或者一个插入的包装器对象。有了包装器,当最终请求最初 C 类的一个实例的时候,这一调用会重定向到 spam 和 eggs 装饰器提供的包装层对象,二者可能有任意的不同角色。

6. 装饰器参数

函数装饰器和类装饰器似乎都能接受参数,尽管实际上这些参数传递给了真正返回装饰器的一个可调用对象,而装饰器反过来又返回一个可调用对象。

@decorator(A,B)
def F(arg):
	...

F(99)

等同如下:

def F(arg):
	...

F = decorator(A,B)(F)
F(99)

装饰器参数在装饰时就解析了,并且它们通常用来保持状态信息供随后的调用使用。

可能采用如下的形式:

def decorator(A,B):
	# save or use A,B in there
	
	def actualDecorator(F):
		#save or use function F
		# return a callable: nested def ,calls with __call__,etc.
		return callable
	return acturalDecorator

换句话说,装饰器参数往往意味着可调用对象的3个层级

  • 接受装饰器参数的一个可调用对象,它返回一个可调用对象以作为装饰器,比如A,
  • A装饰器返回一个可调用对象来处理对最初的函数或类的调用。

这3个层级的每一个都可能是一个函数或类,并且可能以作用域或类属性的形式保存了状态。

实例一:

def timer(label=''):
    def decorator(func):
       print('decorator') #在装饰的时候,就执行到这一层了,然后返回wrapper
       def wrapper(*args,**kwargs):
           start = time.time()
           result = func(*args,**kwargs)
           end = time.time() - start
           print("%s: spend %s "%(label,end))
           return result
       return wrapper
    return decorator

@timer('first do')
def listcomp(N):
    print('111')
    return [x*2 for x in range(N)]

listcomp(500000)

相当于listcomp指向了wrapper.

实例二:

import time
def timer(label=''):
   class Timer:
       def __init__(self,func):
           print("do?") #在装饰的时候,就实例化了。
           self.func = func
       def __call__(self, *args, **kwargs):
           start = time.time()
           result = self.func(*args,**kwargs)
           end = time.time() - start
           print("spend:%s"%(end))
           return result
   return Time


@timer('first do')
def listcomp(N):
    print('111')
    return [x*2 for x in range(N)]

t = listcomp(500000) #调用的时候,执行__call__
print(t)
7. 私有属性
8. 一些例子
1. 函数执行计时器
import time

class Timer:
    def __init__(self,func):
        print(func)
        self.func = func
        self.alltime = 0

    def __call__(self,*args,**kargs):
        start = time.time()
        result = self.func(*args,**kargs)
        elapsed = time.time()-start
        self.alltime +=elapsed
        print("%s: %0.5f, %0.5f"%(self.func.__name__,elapsed,self.alltime))
        return result
timer = Timer

@timer
def listcomp(N):
    time.sleep(3)
    return [x*2 for x in range(N)]

listcomp(500000)

"""
<function listcomp at 0x00000281AA193E20>
listcomp: 3.05713, 3.05713
"""

八、异常处理

1. 异常的常见用途
1.1 错误处理

每当在运行时检测到程序错误时,Python就会引发异常。可以在程序代码中捕捉和响应错误,或者忽略已发生的异常。如果忽略错误,Python默认的异常处理行为将启动:停止程序,打印出错消息。如果不想启动这种默认行为,就要写tr y语句来捕捉异常并从异常中恢复:当检测到错误时,Python会跳到try处理器,而程序在try之后会重新继续执行。

1.2 事件通知

异常也可用于发出有效状态的信号,而不需在程序间传递结果标志位,或者刻意对其进行测试。例如,搜索的程序可能在失败时引发异常,而不是返回一个整数结果代码(而且这段代码很有可能不会有–个有效的结果)。

1.3 特殊情况处理

有时,发生了某种很罕见的情况,很难调整代码去处理。通常会在异常处理器中处理这些罕见的情况,从而省去编写应对特殊情况的代码。

1.4 终止行为

正如将要看到的一样,try/finally语句可确保一定会进行需要的结束运算,无论程序中是否有异常。

1.5 非常规控制流程

最后,因为异常是一种高级的“goto”,它可以作为实现非常规的控制流程的基础。例如,虽然反向跟踪(backtracking)并不是语言本身的一部分,但它能够通过Python的异常来实现,此外需要一些辅助逻辑来退回赋值语句。Python中没有“go to”语句(谢天谢地! ),但是,异常有时候可以充当类似的角色。

2. 默认异常处理

如果我们的代码没有刻意捕捉异常,它将会一直向上返回到程序顶层,并启用默认的异常处理器:就是打印标准出错消息。此时,你也许已经熟悉了标准出错消息。这些消息包括引发的异常还有堆栈跟踪:也就是异常发生时激活的程序行和函数清单。

2.1 捕获异常

不过,在有些情况下,这并不是我们想要的。例如,服务器程序一般需要在内部错误发生时依然保持工作。如果你不想要默认的异常行为,就需要把调用包装在try语句内,自行捕捉异常。

>>> try:
...     1/0
... except ZeroDivisionError:
...     print("不能为0")
...
不能为0
2.2 引发异常

**要手动触发异常,直接执行raise语句。**用户触发的异常的捕捉方式和Python引发的异常一样。

>>> a = 0
>>> try:
...     if a ==0:
...     	raise ZeroDivisionError
...     else:
...         print(10/a)
... except TypeError:
...     print("需要为数字")
...
ZeroDivisionError
3. 异常语句
3.1 try/except/else语句
try:
	#其他语句
except 异常1:
	#其他语句
except 异常2:
	#其他语句
except:
	#其他语句
else:
	#其他语句

在这个语句中:

  • try首行底下的代码块代表此语句的主要动作:试着执行的程序代码。
  • Except子句定义try代码块内引发的异常的处理器
  • 而else子句(如果编写了的话)则是提供没发生异常时要执行的处理器。

换句话说,except分句会捕捉try代码块执行时所发生的任何异常,而else子句只在try代码块执行时不发生异常才会执行。

except没有限制,else最好有且至多一个

3.2 try语句分句
分句形式含义
except:捕获所有(其他)异常类型
except name:只捕获特定的异常
except name as name1:异常别名
except name,value:捕获所列的异常和其额外的数据(或实例)
except (name1,name2):捕获任何列出的异常
except (name1,name2),value:捕获任何列出的异常和其额外的数据(或实例)
else:如果没有引发异常,就运行
finally:总是会运行此代码块

空的except尽管方便,也可能捕获和代码无关、意料之外的系统异常。此时python3有个替代方案:捕获一个名为Exception的异常,这个异常忽略了系统退出相关的异常。

try:
	action()
except Exception:
	...
3.3 try/finally语句
try:
	//其他语句
finally:
	//执行语句

try语句的另一种形式是特定的形式,和最终动作有关。如果在try中包含了finally子句,Python一定会在try语句后执行其语句代码块,无论try代码块执行时是否发生了异常。

利用这个变体,Python可先执行tr y首行下的语句代码块。接下来发生的事情,取决于try代码块中是否发生异常。

  • 如果try代码块运行时没有异常发生,Python会跳至执行finally代码块,然后在整个try语句后继续执行下去。
  • 如果try代码块运行时有异常发生,Python依然会回来运行finally代码块,但是接着会把异常向上传递到较高的try语句或顶层默认处理器。程序不会在try语句下继续执行。也就是说,即使发生了异常,finally代码块还是会执行的,和except不同的是,finally不会终止异常,而是在finally代码块执行后,一直处于发生状态。
3.4 统一 try/except/finally语句

我们可以在同一个try语句中混合finally、except以及else子句。也就是说,我们现在可以编写下列形式的语句:

try:
	#do
except Exception1:
    #handle1
except Exception2:
    #handler2
else:
    #handler3
finally:
    #do2
4. raise和raise…from语句

要显式地触发异常,可以使用raise语句,其一般形式相当简单。raise语句的组成是:raise关键字,后面跟着可选的要引发的类或者类的一个实例:

raise IndexError
raise IndexError()

利用raise,可以重新引发异常

try:
	#do
except IndexError:
	print("error")
	raise #将IndexError重新抛出去了。

通过这种方式执行raise时,会重新引发异常,并将其传递给更高层的处理器(或者顶层的默认处理器,它会停止程序,打印标准出错消息)。

rasie exception from otherexception

当使用from的时候,第二个表达式指定了另一个异常类或实例,它会附加到引发异常的_cause_属性。如果引发的异常没有捕获,Python把异常也作为标准出错消息的一部分打印出来:

try:
	1/0
except Exception as E:
	raise TypeError('Bad') from E

最后上层能看到的是TypeError异常。

**当在一个异常处理器内部引发一个异常的时候,隐式地遵从类似的过程:前一个异常附加到新的异常的_context_属性,并且如果该异常未捕获的话,再次显示在标准出错消息中。**这是一个高级的并且多少还有些含糊的扩展,因此,请参阅Python的手册以了解详细内容。

5. with/as环境管理器

简而言之,with/as语句的设计是作为常见try/finally用法模式的替代方案。就像try lfinally语句,with/as语句也是用于定义必须执行的终止或“清理”行为,无论处理步骤中是否发生异常。不过,和try/finally不同的是,with语句支持更丰富的基于对象的协议,可以为代码块定义支持进入和离开动作。

Python以环境管理器强化一些内置工具,例如,自动自行关闭的文件,以及对锁的自动上锁和开锁,程序员也可以用类编写自己的环境管理器。

with expression [as variable]
	with-block

在这里的expression要返回一个对象,从而支持环境管理协议(稍后会谈到这个协议的更多内容)。如果选用的as子句存在时,此对象也可返回一个值,赋值给变量名variable。
注意: variable并非赋值为expression的结果。expression的结果是支持环境协议的对象,而variable则是赋值为其他的东西。然后,expression返回的对象可在with-block开始前,先执行启动程序,并且在该代码块完成后,执行终止程序代码,无论该代码块是否引发异常。
有些内置的Python对象已得到强化,支持了环境管理协议,因此可以用于with语句。例如,文件对象有环境管理器,可在with代码块后自动关闭文件,无论是否引发异常。

with open(r'c:\misc\data.txt') as myfile:
	for line in myfile:
		print(line)

因为myfile对象也支持with语句所使用的环境管理协议。在这个with语句执行后,环境管理机制保证由myfile所引用的文件对象会自动关闭,即使处理该文件时,for循环引发了异常也是如此。

尽管文件对象在垃圾回收时自动关闭,然而,并不总是能够很容易地知道会何时发生。

6. 环境管理协议

要实现环境管理器,使用特殊的方法来接入with语句,该方法属于运算符重载的范畴。用在with语句中对象所需的接口有点复杂,而多数程序员只需知道如何使用现有的环境管理器。不过,对那些可能想写新的环境管理器的工具创造者而言,我们快速浏览其中细节吧。

以下是with语句实际的工作方式。

  • 计算表达式,所得到的对象称为环境管理器,它必须有__enter___exit__方法。
  • 环境管理器的_enter_方法会被调用。如果as子句存在,其返回值会赋值给As子句中的变量,否则,直接丢弃。
  • 代码块中嵌套的代码会执行。
  • 如果with代码块引发异常,__exit__ (type,value,traceback)方法就会被调用(带有异常细节)。这些也是由sys.exc_info返回的相同值(Python手册和本书这部分稍后会做说明)。如果此方法返回值为假,则异常会重新引发。否则,异常会终止。正常情况下异常是应该被重新引发,这样的话才能传递到with语句之外。
  • 如果with代码块没有引发异常,__exit__方法依然会被调用,其type、value以及traceback参数都会以None传递。
class TraceBlock:
    def message(self, arg):
        print("run:",arg)

    def __enter__(self):
        print("start with block")
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        if exc_type is None:
            print("exited normally")
        else:
            print("raise an exception", exc_type)
            return False

with TraceBlock() as tb:
    tb.message("——————————————尝试1——————————")
    print("——————————————reached——————————————")
with TraceBlock() as tb:
    tb.message("-------------尝试2------------")
    raise TypeError
    print("-------------not reached-------------")

输出:

(venv) C:\Users\29700\Desktop\gams>python main.py
start with block
run: ——————————————尝试1——————————
——————————————reached——————————————
exited normally
start with block
run: -------------尝试2------------
raise an exception <class 'TypeError'>
Traceback (most recent call last):
  File "main.py", line 21, in <module>
    raise TypeError
TypeError

环境管理器是有些高级的机制,还不是Python的正式组成部分,所以我们在这里跳过了其他细节(参考Python的标准手册来了解细节。例如,新的contextlib标准模块)

7. 异常对象
  • 字符串异常(新版本好像不行)

    myexc = "My exception string"
    try:
        raise myexc
    except myexc:
        print("捕获")
    
  • 基于类的异常

    • 提供类型分类,对今后的修改有更好的支持。以后增加新异常时,通常不需要在try语句中进行修改。
    • 它们附加了状态信息。异常类提供了存储在try处理器中所使用的环境信息的合理地点:这样的话,可以拥有状态信息以及可调用的方法,并且可以通过实例进行读取。
    • 它们支持继承。基于类的异常允许参与继承层次,从而可以获得并定制共同的行为。例如,继承的显示方法可提供通用的出错消息的外观。
8. 默认打印和状态
I= IndexError("index error")
print(I.args) #('index error',)

class E(Exception):pass

try:
    raise E('spam')
except E as e:
    print(e,e.args) #spam ('spam',)


class Bad(Exception):
    def __str__(self):
        return "bad bad"

try:
    raise Bad
except Bad as e:
    print(e) #bad bad

九、数据类型

1. 字符串

字符串是不可变的序列,并且他们不可以在原地修改。

1. 常见操作
s =  ''  #空字符串
s = "spam's" #双引号和单引号相同
s = "s\np\ta\x00m" #转义序列
s = """...""" #三重引号字符串块
s = r'\temp\new' #Raw字符串
s = b'spam' #python3.0中的字节字符串
s = u'spam' #仅在python2.6中使用Unicode字符串
s1 + s2 #合并
s * 3 #重复
s[i] #索引,获取第i位字符
s[i:j] #分片,还有s[:]表示全部,s[i:]从i位到最后。
len(s) #求长度
"a %s is xxx"%kind #格式化
“a{} parrot”.format(kind) #格式化
s.find('pa') #字符串查找。还有s.rfind()
s.rstrip() #将字符串最右侧的空格移除
s.lstrip() #将字符串最左侧的空格移除
s.strip() #将字符串两侧的空格移除。
s.replace('pa','tj') #替换,全部替换。
s.split(',') #切割,返回list.
s.isdigit() #判断是否是数字字符串,比如 “1231145”字符串,类似正则/^[0-9]$/g
s.lower() #都转为小写,比如A->a,只对[A-Z]有效。
s.upper() #都转为大写
s.capitalize() 将字符串的第一个字母变成大写,其他字母变小写。对于 8 位字节编码需要根据本地环境。
s.casefold() #casefold() 方法返回一个字符串,其中所有字符均为小写。字符集更大。
s.center(10,'aaa') #center()方法以字符串宽度(width)为中心。使用指定的填充字符(fillchar)填充完成。默认填充字符(fillchar)是一个空格。
s.count(subStr) #统计子串在s中出现的次数。
s.encode(encoding='UTF-8',errors='strict') #指定编码格式。
s.endswith(‘ga’)# endswith(‘ga’) 方法用于判断字符串是否以指定后缀结尾
s.exandtabs(tabsize) #将制表符的大小设置为指定的空格数。
s.isalnum() #如果s至少有一个字符并且所有字符都是字母或数字则返回 True,否则返回 False
s.isalpha() #检查文本中的所有字符是否都是字母
s.isascii() #如果字符串为空或字符串中的所有字符都是 ASCII,则返回 True,否则返回 False。ASCII 字符的码位在 U+0000-U+007F 范围内。
s.isdecimal() #isdecimal()方法检查字符串是否只包含十进制字符。这种方法只存在于unicode对象。
s.isidentifier() #判断字符串是否是有效的 Python 标识符,即可用此方法来判断变量名是否合法。''
s.islower() #检测字符串是否由小写字母组成。
s.isupper() #检测字符串是否由大写字母组成。
s.isnumeric() #检查字符串是否仅包含数字字符。此方法仅用于unicode对象上。
s.isprintable() #检查文本中的所有字符是否可打印,比如\n,\t就不是可打印的。
s.isspace() #检查文本中的所有字符是否都是空格:
s.title() #将每个单词的首字母大写:"Welcome to my world" ->'Welcome To My World
"&".join(strList) # '-'.join(["Bill", "Steve", "Elon"]) ---> 'Bill-Steve-Elon'
s.index() #从左开始查索引,s.rindex()从右。
s.swapcase() #将小写字母大写,大写字母小写:

注意 - 与Python 2不同,所有字符串在Python 3中都用Unicode表示

  • format_map

     class Default(dict):
         def __missing__(self, key):
             return key
    
    '{name} was born in {country}'.format_map(Default(name='Guido'))
    #'Guido was born in country'
    
    """
    类似于 str.format(**mapping),不同之处在于 mapping 会被直接使用而不是复制到一个 dict。 适宜使用此方法的一个例子是当 mapping 为 dict 的子类的情况
    """
    
  • ljust和rjust

    txt = "banana"
    x = txt.ljust(20)
    print(x, "-----$")
    #banana               -----$
    
  • maketrans和translate

    #创建一个映射表,并在translate()方法中使用它来将任何“S”字符替换为“P”字符:
    txt = "Hello Sam!";
    mytable = txt.maketrans("S", "P");
    print(txt.translate(mytable));
    
  • partition

    """
    partition 搜索单词 "bananas",并返回包含三个元素的元组:
        1 - “匹配”之前的所有内容
        2 - “匹配”
        3 - “匹配”之后的所有内容
    
    rpartition() 方法搜索指定字符串的最后一次出现,并将该字符串拆分为包含三个元素的元组。
    """
    txt = "I could eat bananas all day, bananas are my favorite fruit"
    x = txt.partition("bananas")
    print(x)
    #('I could eat ', 'bananas', ' all day, bananas are my favorite fruit')
    
  • removesuffix和removeprefix.一个是后,一个是前。

    'MiscTests'.removesuffix('Tests')
    #'Misc'
    'TmpDirMixin'.removesuffix('Tests')
    #'TmpDirMixin'
    
  • splitlines() -将字符串拆分为一个列表,其中每一行都是一个列表项:(类似s.split(‘\n’),但比split分割行好用。)

    txt = "Thank you for your visiting\nWelcome to China"
    x = txt.splitlines()
    print(x)
    #['Thank you for the music', 'Welcome to the jungle']
    s = """
    111111
    222222
    33333
    """
    print(s.splitlines())
    #['', '111111', '222222', '33333']
    
  • startswitch

    txt = "Hello, welcome to my world."
    x = txt.startswith("Hello")
    print(x) #True
    
  • zfill - 用零填充字符串,直到长度为 10 个字符

    txt = "50"
    x = txt.zfill(10)
    print(x)#0000000050
    
  • 字符串和字节串

    就是怎么将‘str’ -> b’str’

    str = 'agag'
    #法一
    bytes(str,encoding="utf8")
    #法二
    str.encode()
    

    转回去类似

    s = b'12315'
    #法一
    str(s,encoding='utf8')#法二:
    s.decode()
    
2. 字典

字典有时又叫做关联数组(associative array)或者是散列表(hash)。它们通过键将一系列值联系起来,这样就可以使用键从字典中取出一项。就像列表那样,同样可以使用索引操作从字典中获取内容。但是索引采取键的形式,而不是相对偏移。

  • 可变
  • 任意嵌套
  • 支持原地修改
  • 无序集合
1. 常见操作
#1. 声明字典的几种方式:
d1 = {}
d2 = dict()
d3 = dict({'name':1})
print(d3)
d4 = dict(name=1,age=2) #{'name': 1, 'age': 2}
print(d4)
d5 = dict.fromkeys(['a','b'])
print(d5) #{'a': None, 'b': None}

keyslist = ['name','age']
valslist = ['joker',30]
d7 = dict(zip(keyslist,valslist)) #{'name': 'joker', 'age': 30}
print(d7)

#2.支持嵌套
d = {'person': {'name':'joker','age':30}}

#3.获取字典值得几种方式。
d = {'person': {'name':'joker','age':30}}
print(d['person'])
print(d['person']['name'])

#4.键存在测试
d = {'person': {'name':'joker','age':30}}
print('person' in d) #True
print('humman' in d) #False

#5. 获取第一层所有键
d.keys()

#6.获取第一层所有值
d.values()

#7. 获取键+值
d = {"name":"joker","age":30}
print(d.items()) #dict_items([('name', 'joker'), ('age', 30)])

#8. 字典浅拷贝
person = {"name":"joker","age":30}
d = {"person":person}
d2 = d.copy()
print(d2)
person['name'] = 'ttt'
print(d2) #{'person': {'name': 'ttt', 'age': 30}}

#9.d.get(key,default)
d = {'name':'joker'}
v = d.get('age')
print(v) #None
v = d.get('age',30)
print(v) #30,设置默认.

#10.合并,浅合并
d1 = {'name':'joker'}
d2 = {'age':30,'obj': {'sex':0}}
d1.update(d2)
print(d1) #{'name': 'joker', 'age': 30, 'obj': {'sex': 0}}
print(d2) #{'age': 30, 'obj': {'sex': 0}}
d2['obj']['sex'] =1
print(d1) #{'name': 'joker', 'age': 30, 'obj': {'sex': 1}}

#11.删除k-v
d1= {'name':'joker','age':30}
d1.pop('age')
print(d1) #{'name': 'joker'}
del d1['name']

#12.长度
len(d1)

#13. list key
list(d.keys())

#14.key的与操作,还有或
d1 = {'name':'joker','age':30}
d2 = {'sex':0,'id':100,'age':20}
print(d1.keys() & d2.keys()) #{'age'}
print(d1.keys() | d2.keys()) #set集合{'age', 'name', 'id', 'sex'}

#14. 字典解析
d = {x:x*2 for x in range(5)}
print(d) #{0: 0, 1: 2, 2: 4, 3: 6, 4: 8}

附录

1. LEGB规则

Python 在查找"名称"时,是按照 LEGB 规则查找的:
Local–>Enclosed–>Global–>Built in

  • Local 指的就是函数或者类的方法内部
  • Enclosed 指的是嵌套函数(一个函数包裹另一个函数,闭包)
  • Global 指的是模块中的全局变量
  • Built in 指的是 Python 为自己保留的特殊名称
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值