Skip to content

Python 学习笔记 9. 类

🏷️ Python Python 学习笔记

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

9.1. 名称和对象

对象具有个性,多个名称(在多个作用域内)可以绑定到同一个对象。这在其他语言中称为别名。在某些方面(如参数传递),别名表现的像指针。

9.2. Python 作用域和命名空间

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

不同命名空间中的名称之间绝对没有关系。

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

  • 包含内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。
  • 模块的全局命名空间在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出。
  • 一个函数的本地命名空间在这个函数被调用时创建,并在函数返回或抛出一个不在函数内部处理的错误时被删除。

一个 作用域 是一个命名空间可直接访问的 Python 程序的文本区域。这里的 “可直接访问” 意味着对名称的 非限定引用 会尝试在命名空间中查找名称。

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

上面摘抄过来的查找过程完全看不懂。

简单来说就是会从最接近的作用域查找变量,直至全局作用域,但这是在查找的各个作用域中没有对其做赋值操作的前提下

python
>>> def scope_test():
...     def print_glbal():
...         print("Print global variable:", spam)
...     print_glbal()
...
>>> spam = "global spam"
>>> scope_test()
Print global variable: global spam

变量未定义直接赋值时,不会执行作用域的查询,会把变量直接当做当前作用域的变量。

对比上面的示例,下面的示例在 scope_test() 的作用域内对同名的变量 spam 做了赋值操作,此时在 全局作用域scope_test()局部作用域 中同时各存在一个名为 spam 的变量。

python
>>> def scope_test():
...     def print_nonlocal():
...         print("Print nonlocal variable:", spam)
...     spam = "nonlocal spam"
...     print_nonlocal()
...
>>> spam = "global spam"
>>> scope_test()
Print nonlocal variable: nonlocal spam

如果在 scope_test() 局部作用域下再创建一个局部作用域并对同名的 spam 变量赋值会怎样?

python
>>> def scope_test():
...     def print_nonlocal():
...         print("Print nonlocal variable:", spam)
...     def print_local():
...         spam = "local spam"
...         print("Print local variable:", spam)
...     spam = "nonlocal spam"
...     print_nonlocal()
...     print_local()
...
>>> spam = "global spam"
>>> scope_test()
Print nonlocal variable: nonlocal spam
Print local variable: local spam

print_local() 作用域中使用的是它自己的局部作用域中的 spam 变量。

在这里如果调整 print_local() 方法调用的顺序又会怎样呢?

python
>>> def scope_test():
...     def print_nonlocal():
...         print("Print nonlocal variable:", spam)
...     def print_local():
...         print("Print local variable:", spam)
...         spam = "local spam"
...     spam = "nonlocal spam"
...     print_nonlocal()
...     print_local()
...
>>> spam = "global spam"
>>> scope_test()
Print nonlocal variable: nonlocal spam
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in scope_test
  File "<stdin>", line 5, in print_local
UnboundLocalError: local variable 'spam' referenced before assignment

报了 UnboundLocalError 异常,可见在 print_local() 方法中变量未赋值之前,spam 已经被认定为是自己局部作用域中的变量了,在赋值之前使用该变量不会去外层查找同名变量。

前面的几个例子中的变量都是 非限定引用 ,那反过来说就是 Python 支持 限定引用
使用 nonlocalglobal 关键字可以指定变量的作用域。

  • nonlocal 语句会使得所列出的名称指向之前在最近的包含作用域中绑定的除全局变量以外的变量。
  • global 语句是作用于整个当前代码块的声明。它意味着所列出的标识符将被解读为全局变量。
python
>>> def scope_test():
...     def print_nonlocal():
...         print("Before local assignment:", spam)
...     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"
...     print_nonlocal()
...     do_local()
...     print("After local assignment:", spam)
...     do_nonlocal()
...     print("After nonlocal assignment:", spam)
...     do_global()
...     print("After global assignment:", spam)
...
>>> try:
...     print("In global scope:", spam)
... except:
...     print("No spam defined in global scope.")
...
No spam defined in global scope.
>>> scope_test()
Before local assignment: test spam
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

从上面的示例可以看出,Python 中的变的作用域跟其它常用的编程语言(C#、Java、JavaScript 等)都有很大的差异。

9.3. 初探类

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

9.3.1. 类定义语法

最简单的类定义看起来像这样:

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

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

9.3.2. 类对象

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

python
>>> class MyClass:
...     """A simple example class"""
...     i = 12345
...     def f(self):
...         return 'hello world'
...

属性引用:

python
>>> MyClass.i
12345
>>> MyClass.f
<function MyClass.f at 0x0000000001E28E18>
>>> MyClass.__doc__
'A simple example class'

实例化:

python
>>> x = MyClass()
>>> x
<__main__.MyClass object at 0x0000000001E2C9E8>

上例中的 MyClass 类中的 i 属性是所有实例共享的,有点类似于 C# 中的静态变量。如果要实现每个实例拥有独立的属性值,应使用如下写法:

python
>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> y = Complex(2.0, -3.5)
>>> x.r, x.i
(3.0, -4.5)
>>> y.r, y.i
(2.0, -3.5)

9.3.3. 实例对象

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

数据属性 不需要声明;像局部变量一样,它们将在第一次被赋值时产生。

方法 是“从属于”对象的函数。实例对象的有效方法名称依赖于其所属的类。根据定义,一个类中所有是函数对象的属性都是定义了其实例的相应方法。
上面 MyClass 的例子中, x.f方法对象,而 MyClass.f 则是 函数对象

9.3.4. 方法对象

方法的特殊之处就在于 实例对象会作为函数的第一个参数被传入

方法定义 def f(self) 虽然带有一个 self 参数,但调用时直接调用即可。

python
x.f()

也可以保存起来以后再调用。

python
xf = x.f
print(xf())

9.3.5. 类和实例变量

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

python
>>> 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')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

上例中 kind 就是类变量,被所有示例共享; name 就是示例变量,仅在各自的示例中使用。

9.4. 补充说明

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

python
>>> class Warehouse:
...     purpose = 'storage'
...     region = 'west'
...
>>> w1 = Warehouse()
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w1.purpose, w1.region)
storage west
>>> print(w2.purpose, w2.region)
storage east

w2 在对 region 属性赋值时,并不会修改 类属性 Warehouse.region 的值,而是会在 w2 实例上新增一个同名属性。

通过修改 类属性 Warehouse.region 的值来验证:

python
>>> Warehouse.region = "south"
>>> print(w1.purpose, w1.region)
storage south
>>> print(w2.purpose, w2.region)
storage east

修改后 w1.regionWarehouse.region 的值一致,而 w2.region 值没变。这说明 w1.region 指向的仍然是类属性,而 w2.region 已经变成了 实例属性。

9.5. 继承

python
class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

派生类可能会重载其基类的方法。

有一种方式可以简单地直接调用基类方法:即调用 BaseClassName.methodname(self, arguments)

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

  • isinstance()

    使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.__class__int 或某个派生自 int 的类时为 True。

  • issubclass()

    使用 issubclass() 来检查类的继承关系: issubclass(bool, int)True ,因为 boolint 的子类。但是,issubclass(float, int)False ,因为 float 不是 int 的子类。

9.5.1. 多重继承

搜索顺序:深度优先、从左至右,不会在同一个类中搜索两次。

python
class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

9.6. 私有变量

名称改写:任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam ,其中 classname 为去除了前缀下划线的当前类名称。

python
>>> 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)
...     __update = update   # private copy of original update() method
...

请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。

python
>>> x = MappingSubclass([""])
>>> x.items_list
['']
>>> x.update(["a"])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: update() missing 1 required positional argument: 'values'
>>> x.update("a","b")
>>> x.items_list
['', ('a', 'b')]

通过上面的调用示例可以看出 x.update 方法调用的是子类 MappingSubclass 中的 update 方法。

python
>>> x.__update
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MappingSubclass' object has no attribute '__update'
>>> x._MappingSubclass__update
<bound method MappingSubclass.update of <__main__.MappingSubclass object at 0x0000000001E36BA8>>
>>> x._Mapping__update
<bound method Mapping.update of <__main__.MappingSubclass object at 0x0000000001E36BA8>>

通过上面的调用可以看出 __update 方法已经被分别改名为 _MappingSubclass__update_Mapping__update 了。

9.7. 杂项说明

定义一个类似于 结构(struct)的类型。

python
>>> class Employee:
...     pass
...
>>> john = Employee()  # Create an empty employee record
>>> # Fill the fields of the record
... john.name = 'John Doe'
>>> john.dept = 'computer lab'
>>> john.salary = 1000

9.8. 迭代器

大多数容器对象都可以使用 for 语句

python
>>> for element in [1, 2, 3]:
...     print(element)
...
1
2
3
>>> for element in (1, 2, 3):
...     print(element)
...
1
2
3
>>> for key in {'one':1, 'two':2}:
...     print(key)
...
one
two
>>> for char in "123":
...     print(char)
...
1
2
3
>>> for line in open("workfile"):
...     print(line, end='')
...
This is the first line of the file.
Second line of the file

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

python
>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x0000000001DE3E80>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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

python
>>> class Reverse:
...     """Iterator for looping over a sequence backwards."""
...     def __init__(self, data):
...         self.data = data
...         self.index = len(data)
...     def __iter__(self):
...         return self
...     def __next__(self):
...         if self.index == 0:
...             raise StopIteration
...         self.index = self.index - 1
...         return self.data[self.index]
...
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x0000000001E410B8>
>>> for char in rev:
...     print(char)
...
m
a
p
s
>>>

9.9. 生成器

Generator 是一个用于创建迭代器的简单而强大的工具。

它们的写法类似标准的函数,但当它们要返回数据时会使用 yield 语句。每次对生成器调用 next() 时,它会从上次离开位置恢复执行(它会记住上次执行语句时的所有数据值)。

显示如何非常容易地创建生成器的示例如下:

python
>>> def reverse(data):
...     for index in range(len(data)-1, -1, -1):
...         yield data[index]
...
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g
>>>

9.10. 生成器表达式

某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但 外层 为 圆括号 而非方括号。

python
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260
>>> sum(i*i for i in range(10))                 # sum of squares
285
>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']