Contents

Python教程-类

本系列为作者在官网学习python时做的笔记,详见python官网

python标准库官方文档,查阅标准库相关的内容

python语言参考,查看python语法,该文档是简洁的,但试图做到准确和完整。非必要的内建对象类型和内建函数、模块的语义描述在 Python 标准库 中。

标准库和语言参考涵盖了python的所有内容,他们是互补的,如果在语言参考中没有找到需要的内容那么就一定在便准库文档中。

类提供了一种组合数据和功能的方法。 创建一个新类意味着创建一个新的对象 类型,从而允许创建一个该类型的新 实例 。 每个类的实例可以拥有保存自己状态的属性。 一个类的实例也可以有改变自己状态的(定义在类中的)方法。

Python 的类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖它基类的任何方法,一个方法可以调用基类中相同名称的的方法。对象可以包含任意数量和类型的数据。和模块一样,类也拥有 Python 天然的动态特性:它们在运行时创建,可以在创建后修改。

名称和对象

多个名称(在多个作用域内)可以绑定到同一个对象。 这在其他语言中称为别名。 因为别名在某些方面表现得像指针。

Python 作用域和命名空间

namespace (命名空间)是一个从名字到对象的映射。

把任何跟在一个点号之后的名称都称为 属性

属性可以是只读或者可写的。如果为后者,那么对属性的赋值是可行的。可写的属性同样可以用 del 语句删除。例如, del modname.the_answer 将会从名为 modname 的对象中移除 the_answer 属性。

在不同时刻创建的命名空间拥有不同的生存期:

  • 包含内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。
  • 模块的全局命名空间在模块定义被读入时创建;模块命名空间也会持续到解释器退出。
  • 被解释器的顶层调用执行的语句,从一个脚本文件读取或交互式地读取,被认为是__main__模块调用的一部分,因此它们拥有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块称作 builtins 。)

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

一个 作用域 是一个命名空间可直接访问的 Python 程序的文本区域。

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

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

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

通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。 在函数以外,局部作用域将引用与全局作用域相一致的命名空间:模块的命名空间。 类定义将在局部命名空间内再放置另一个命名空间。任何地方的全局命名空间都是模块(py文件)命名空间包括包的__init__.py文件。

作用域是按字面文本来确定的:在一个模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。 另一方面,实际的名称搜索是在运行时动态完成的

如果不存在生效的 global 或 nonlocal 语句 对名称的赋值总是会进入最内层作用域。 删除也是如此:语句 del x 会从局部作用域所引用的命名空间中移除对 x 的绑定。 事实上,所有引入新名称的操作都是使用局部作用域,但是有个例外import 语句和函数定义会在局部作用域中绑定模块或函数名称。

global 语句可被用来表明特定变量生存于全局作用域(py模块最外层)并且应当在其中被重新绑定;nonlocal 语句表明特定变量生存于外层作用域中并且应当在其中被重新绑定(从当前语句所在作用域往外找到的第一个)。可以del绑定的外部变量。

作用域和命名空间示例

 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

初探类

类引入了一些新语法,三种新对象类型和一些新语义。

类定义语法

1
2
3
4
5
6
class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

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

当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域。

当(从结尾处)正常离开类定义时,将创建一个 类对象。 这基本上是一个包围在类定义所创建命名空间内容周围的包装器;原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称 (在这个示例中为 ClassName)。

类对象

类对象支持两种操作:属性引用和实例化。

属性引用 : obj.name。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称:

1
2
3
4
5
6
class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

MyClass.i 和 MyClass.f 就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来更改 MyClass.i 的值。 __doc__ 属性返回所属类的文档字符串: “A simple example class”。

类的 实例化 :x = MyClass()创建类的新 实例 并将此对象分配给局部变量 x。

类定义包含一个名为 __init__() 的特殊方法:

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

当一个类定义了 __init__() 方法时,类的实例化操作会自动为新创建的类实例发起调用 __init__()

提供给类实例化的参数将被传递给 __init__()

1
2
3
4
5
6
7
8
>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

实例对象

实例对象理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。

数据属性 对应于 C++ 中的“数据成员”。 数据属性不需要声明;像局部变量一样,它们将在第一次被赋值时产生

另一类实例属性引用称为 方法。 方法是“从属于”对象的函数。

实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类中所有是函数对象的属性都是定义了其实例的相应方法。 因为 MyClass.f 是一个函数,所以x.f 是有效的方法引用,而 x.i 不是方法,因为 MyClass.i 不是一个函数。

方法对象

调用方法:x.f()

x.f 是一个方法对象,它可以被保存起来以后再调用:

1
2
3
xf = x.f
while True:
    print(xf())

方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 调用 x.f() 其实就相当于 MyClass.f(x)。

其实原理就是当在实例本地找不到数据或者函数时会去类对象里面找,如果是函数会把实例自身作为参数self传给函数。所以你修改类对象就可以修改类定义:

类和实例变量

实例变量用于每个实例的唯一数据,类变量用于类的所有实例共享的属性和方法,共享数据可能在涉及 mutable 对象例如列表和字典的时候导致令人惊讶的结果:

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

    tricks = []             # mistaken use of a class variable

    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                # unexpectedly shared by all dogs
['roll over', 'play dead']

正确的类设计应该使用实例变量即定义在__init__函数里面的self属性:

 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 = []    # 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')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

补充说明

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

在 Python 中没有任何东西能强制隐藏数据 — 它是完全基于约定的

方法的第一个参数常常被命名为 self。 这也不过就是一个约定: self 这一名称在 Python 中绝对没有特殊含义。

任何一个作为类属性的函数都为该类的实例定义了一个相应方法。 函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的。 例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

甚至你可以在定义完类之后修改类的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class a:
    def hello(self):
        print("hello world")

b=a()

def hhhhh(self):
    print("hello world2")

a.hello2=hhhhh

b.hello2()#输出hello world2

方法可以通过使用 self 参数的方法属性调用其他方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)

对象的 object.__class__ 属性指向类对象

类对象也可以进行传值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def f():
    class a:
        def hello(self):
            print("hello world")
    return a

b=f()

c=b()

print(c.__class__)#输出<class '__main__.f.<locals>.a'>

继承

1
2
3
4
5
6
class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

名称 BaseClassName 必须定义于包含派生类定义的作用域中。 也允许用其他任意表达式代替基类名称所在的位置。例如,当基类定义在另一个模块中的时候:

1
class DerivedClassName(modname.BaseClassName):

派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。

派生类的实例化没有任何特殊之处: DerivedClassName() 会创建该类的一个新实例。

方法引用将按以下方式解析:搜索相应的类属性,如有必要将按基类继承链逐步向下查找,如果产生了一个函数对象则方法引用就生效。

派生类可能会重载其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。 (对 C++ 程序员的提示:Python 中所有的方法实际上都是 virtual 方法。)

在派生类中的重载方法实际上可能想要扩展而非简单地替换同名的基类方法。 有一种方式可以简单地直接调用基类方法:即调用 BaseClassName.methodname(self, arguments)。

Python有两个内置函数可被用于继承机制:

  • 使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.__class__ 为 int 或某个派生自 int 的类时为 True。
  • 使用 issubclass() 来检查类的继承关系: issubclass(bool, int) 为 True,因为 bool 是 int 的子类。 但是,issubclass(float, int) 为 False,因为 float 不是 int 的子类。

多重继承

Python 也支持多重继承。 带有多个基类的类定义语句如下所示:

1
2
3
4
5
6
class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

可以认为搜索从父类所继承属性的操作是深度优先、从左至右的,当层次结构中存在重叠时不会在同一个类中搜索两次。 因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果在那里未找到,再到 Base2 中搜索,依此类推。

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

Python 3 可以使用直接使用 super().xxx 代替 super(Class, self).xxx

class.mro()将返回方法搜索顺序表,关于mro相关的C3算法见官网文档以及mozillazg大佬的解析,大佬还解析了super关键字

多继承中调用指定父类的方法可以使用class.method(self,param)格式

私有变量

那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。

由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为去除了前缀下划线的当前类名称。 这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。

名称改写有助于让子类重载方法而不破坏类内方法调用。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

上面的示例即使在 MappingSubclass 引入了一个 __update 标识符的情况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update 而在 MappingSubclass 类中被替换为 _MappingSubclass__update。

杂项说明

实例方法对象也具有属性: m.__self__ 就是带有 m() 方法的实例对象,而 m.__func__ 则是该方法所对应的函数对象。

迭代器

到目前为止,您可能已经注意到大多数容器对象都可以使用 for 语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

for 语句会在容器对象上调用 iter()。 该函数返回一个具有 next() 方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,next() 将引发 StopIteration 异常来通知终止 for 循环。 可以使用 next() 内置函数来调用 next() 方法;这个例子显示了它的运作方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

看过迭代器协议的幕后机制,给你的类添加迭代器行为就很容易了。 定义一个 iter() 方法来返回一个带有 next() 方法的对象。 如果类已定义了 next(),则 iter() 可以简单地返回 self:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        self.index = len(self.data)
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

生成器

生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield 语句。 每次在生成器上调用 next() 时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。 一个显示如何非常容易地创建生成器的示例如下:

1
2
3
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
1
2
3
4
5
6
7
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

可以用生成器来完成的操作同样可以用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 iter() 和 next() 方法。

另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。 这使得该函数相比使用 self.index 和 self.data 这种实例变量的方式更易编写且更为清晰。

除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。

生成器表达式

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

示例:

 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']
 |