前言: 提供了把数据和功能绑定在一起的方法 支持面向对象中的多种特性: 基础机制支持多个基类;派生类能够覆盖基类的方法;类的方法能够调用基类中的同名方法 此外,还具有Python的动态特性,运行时创建,创建后可修改

9.1.名称和对象

别名:对象之间相互独立,多个名称(甚至是多个作用域内的多个名称)可以绑定到同一对象。这在其他语言中通常被称为别名,这种现象主要是有益于可变对象,比如列表,,字典等等

9.2.Python的作用域和命名空间

命名空间:namespace

名称到对象的映射 实现:大多数靠Python字典实现 不同命名空间中的名称之间绝对没有关系

属性:在点之后的名称是属性(好奇怪的定义)

模块属性和模块中定义的全局名称之间存在直接的映射:它们共享相同的命名空间 属性可以是只读或者只写的,后一种情况可以对属性进行赋值,也可以通过del来进行删除

命名空间的创建时间和生命周期(感觉有点复杂)

命名空间是在不同时刻创建的,且拥有不同的生命周期。内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在读取模块定义时创建;通常,模块的命名空间也会持续到解释器退出。从脚本文件读取或交互式读取的,由解释器顶层调用执行的语句是 __main__ 模块调用的一部分,也拥有自己的全局命名空间。内置名称实际上也在模块里,即 builtins 。

函数的局部命名空间在函数被调用时被创建,并在函数返回或抛出未在函数内被处理的异常时,被删除。(实际上,用“遗忘”来描述实际发生的情况会更好一些。)当然,每次递归调用都有自己的局部命名空间。

一个命名空间的 作用域 是 Python 代码中的一段文本区域,从这个区域可直接访问该命名空间。“可直接访问”的意思是,该文本区域内的名称在被非限定引用时,查找名称的范围,是包括该命名空间在内的。

作用域虽然是被静态确定的,但会被动态使用。执行期间的任何时刻,都会有 3 或 4 个“命名空间可直接访问”的嵌套作用域:

  • 最内层作用域,包含局部名称,并首先在其中进行搜索
  • 那些外层闭包函数的作用域,包含“非局部、非全局”的名称,从最靠内层的那个作用域开始,逐层向外搜索。
  • 倒数第二层作用域,包含当前模块的全局名称
  • 最外层(最后搜索)的作用域,是内置名称的命名空间

如果一个名称被声明为全局,则所有引用和赋值都将直接指向“倒数第二层作用域”,即包含模块的全局名称的作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用 nonlocal 语句;如果未使用 nonlocal 声明,这些变量将为只读(尝试写入这样的变量将在最内层作用域中创建一个 新的 局部变量,而使得同名的外部变量保持不变)。

通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。在函数之外,局部作用域引用与全局作用域一致的命名空间:模块的命名空间。 类定义在局部命名空间内再放置另一个命名空间。

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

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

global 语句用于表明特定变量在全局作用域里,并应在全局作用域中重新绑定;nonlocal 语句表明特定变量在外层作用域中,并应在外层作用域中重新绑定。

9.2.1.作用域和命名空间

一个经典的例子便于理解global和nonlocal对于变量绑定的影响

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)

示例代码的输出是:

1
2
3
4
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

注意,局部 赋值(这是默认状态)不会改变 scope_test 对 spam 的绑定。 nonlocal 赋值会改变 scope_test 对 spam 的绑定,而 global 赋值会改变模块层级的绑定。

而且,global 赋值前没有 spam 的绑定。

本人的理解:经验证是正确的,但是表述没有很准确,即函数名啥的写的比较粗略等等 首先 spam = “test spam"创建在scope_test作用域的spam,然后dolocal创建属于dolocal的spam然后do_nonlocal更新属于它外围u函数也就是socpetest的spam,然后doglobal创建全局的spam,在scope——test的每次输出都是输出属于scopetest的spam,但是最后一句 scope_test() print(“In global scope:”, spam) 会输出属于global的spam 此处要理解的关键就是各个作用域中的spam和 nonlocal与global对于绑定造成的影响 nonlocal是在外围函数中创建或者更新,比如此处的scope_test就是do_local等三个函数的外围函数 global则是在全局做更新或创建

9.3.初探类

9.3.1.类的定义

class ClassName:

9.3.2 Class对象

支持两种操作:

1.属性引用

属性引用 语法:obj.name ____doc____为文档字符串属性

2.实例化

当一个类定义了 __init__() 方法时,类的实例化会自动为新创建的类实例唤起 __init__()。 因此在这个例子中,可以通过以下语句获得一个已初始化的新实例: 当然,__init__() 方法还有一些参数用于实现更高的灵活性。 在这种情况下,提供给类实例化运算符的参数 将被传递给 __init__()。 例如

9.3.3.实例对象

唯一操作为属性引用,引用的对象包括两个方面:函数和数据属性

9.3.4.方法对象

方法对象可被储存卡里 例如:xf = x.f,print(xf) 方法的特殊之处就在于实例对象会作为函数的第一个参数被传入

9.3.5.实例对象和类

正确设计实例变量和类变量

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Dog:

    kind = 'canine'         # 类变量被所有实例所共享

    def __init__(self, name):
        self.name = name    # 实例变量为每个实例所独有

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # 被所有的 Dog 实例所共享
'canine'
>>> e.kind                  # 被所有的 Dog 实例所共享
'canine'
>>> d.name                  # 为 d 所独有
'Fido'
>>> e.name                  # 为 e 所独有
'Buddy'

正如在 名称和对象 中所讨论的,共享数据可能在涉及 mutable 对象如列表和字典时导致令人惊讶的结果。 例如,以下代码中的 tricks 列表不应被用作类变量因为所有 Dog 实例将共享一个单独的列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Dog:

    tricks = []             # 类变量的错误用法

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

    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')
>>> d.tricks                # 非预期地被所有的 Dog 实例所共享
['roll over', 'play dead']

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # 为每个 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')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4.补充说明

1.同样的属性名称同时出现在类对象和实例对象时。优先选择实例对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Warehouse:
   purpose = 'storage'
   region = 'west'

w1 = Warehouse()
print(w1.purpose, w1.region)
storage west
//相当与给w2创建了一个实例属性region,要与属于warehouse的region属性区别开
w2 = Warehouse()
w2.region = 'east'
print(w2.purpose, w2.region)
storage east

方法的第一个参数常常被命名为 self。 这也不过就是一个约定: self 这一名称在 Python 中绝对没有特殊含义。 但是要注意,不遵循此约定会使得你的代码对其他 Python 程序员来说缺乏可读性,而且也可以想像一个 类浏览器 程序的编写可能会依赖于这样的约定。

9.5. 继承

派生类的语法:class DeriveClassName(BaseClassName): 若基类位于另一个module, 语法为:class DerivedClassName(modname.BaseClassName):

关于重写: Python 中所有的方法实际上都是 virtual 方法 如果想要调用基类的方法而非派生类的方法:BaseClassName.methodname(self, arguments)

两个有关继承机制的内置函数: isinstancel():isinstance(obj,int) 当obj为int或者int的子类时为true; issubclass(a,b):判断a是不是b的子类

9.5.1.多重继承

语法:class DerivedClassName(Base1, Base2, Base3): 多个基类访问顺序:有点复杂,自己看对于多数目的来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左到右的,当层次结构存在重叠时不会在同一个类中搜索两次。 因此,如果某个属性在 DerivedClassName 中找不到,就会在 Base1 中搜索它,然后(递归地)在 Base1 的基类中搜索,如果在那里也找不到,就将在 Base2 中搜索,依此类推。

真实情况比这个更复杂一些;方法解析顺序会动态改变以支持对 super() 的协同调用。 这种方式在某些其他多重继承型语言中被称为后续方法调用,它比单继承型语言中的 super 调用更强大。

动态调整顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个上级类可通过多条路径被最底层的类所访问)。 例如,所有类都是继承自 object,因此任何多重继承的情况都提供了一条以上的路径可以通向 object。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化,保留每个类所指定的从左至右的顺序,只调用每个上级类一次,并且保持单调性(即一个类可以被子类化而不影响其父类的优先顺序)。 总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 要了解更多细节,请参阅 Python 2.3 方法解析顺序。

9.6.私有变量

意识到:仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在 约定为:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 名称改写:任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为去除了前缀下划线的当前类名称。

9.7.杂项说明

使用datalclasses:将一些带名称的数据项捆绑在一起 from dataclasses import dataclass

1
2
3
4
5
@dataclass
class Employee:
    name: str
    dept: str
    salary: int

9.8.迭代器

for语句: 原理:在容器对象调用iter()生成一个带有–next–()方法的迭代器对象,准一访问到末尾引发StopIteration异常来中止 通过next()函数调用–next–()

9.9.生成器

一个创建迭代器的工具,返回数据时会使用yield语句

yield与return的区别:yield不会终结函数而是暂停函数,这正好符合生成器的需要 几个特性: 但生成器的写法更为紧凑,因为它会自动创建 __iter__() 和 __next__() 方法。 另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。 当生成器终结时,它们还会自动引发 StopIteration。

9.10.生成器表达式(懒得解释了自己看)

某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。

示例:

Copy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sum(i*i for i in range(10))                 # sum of squares
285

xvec = [10, 20, 30]
yvec = [7, 5, 3]
sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

unique_words = set(word for line in page  for word in line.split())

valedictorian = max((student.gpa, student.name) for student in graduates)

data = 'golf'
list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']