类是将相似数据和函数聚合在一起的方式。类基本上是一个作用域,其中执行各种代码(尤其是函数定义),该作用域中的局部变量成为类的属性,并且成为该类构造的任何对象的属性。由类构造的对象被称为该类的实例。

概览

在 Python 中,类的基本结构如下:

import math
class MyComplex:
  """一个复数"""       # 类的文档字符串
  classvar = 0.0               # 一个类属性,而不是实例属性
  def phase(self):             # 一个方法
    return math.atan2(self.imaginary, self.real)
  def __init__(self):          # 构造函数
    """构造函数"""
    self.real = 0.0            # 一个实例属性
    self.imaginary = 0.0
c1 = MyComplex()
c1.real = 3.14                 # 没有访问保护
c1.imaginary = 2.71
phase = c1.phase()             # 方法调用
c1.undeclared = 9.99           # 添加一个实例属性
del c1.undeclared              # 删除一个实例属性

print(vars(c1))                # 属性以字典形式展示
vars(c1)["undeclared2"] = 7.77 # 写入访问属性
print(c1.undeclared2)          # 输出 7.77

MyComplex.classvar = 1         # 访问类属性
print(c1.classvar == 1)        # 输出 True; 类属性访问,而不是实例属性
print("classvar" in vars(c1))  # 输出 False
c1.classvar = -1               # 实例属性覆盖类属性
MyComplex.classvar = 2         # 访问类属性
print(c1.classvar == -1)       # 输出 True; 实例属性访问
print("classvar" in vars(c1))  # 输出 True

class MyComplex2(MyComplex):   # 类的继承
  def __init__(self, re = 0, im = 0):
    self.real = re             # 一个带有默认参数的构造函数
    self.imaginary = im
  def phase(self):
    print("派生类的 phase 方法")
    return MyComplex.phase(self) # 调用基类的方法

c3 = MyComplex2()
c4 = MyComplex2(1, 1)
c4.phase()                     # 调用派生类中的方法

class Record: pass             # 将类作为记录/结构体,具有任意属性
record = Record()
record.name = "Joe"
record.surname = "Hoe"

定义一个类

定义类的语法如下:

class ClassName:
    "这里是关于你的类的解释"
    pass

在类定义中,使用大写字母开头是惯例,但并不是语言要求的。通常,最好至少添加一个简短的解释,说明你的类是做什么的。代码中的 pass 语句只是告诉 Python 解释器继续执行而不做任何事。你可以在添加第一个语句时删除它。

实例构造

类是一个可调用的对象,当被调用时会构造类的一个实例。假设我们创建了一个名为 Foo 的类:

class Foo:
    "Foo 是我们的新玩具。"
    pass

要构造 Foo 类的一个实例,可以这样调用类对象:

f = Foo()

这会构造一个 Foo 类的实例,并在变量 f 中创建对它的引用。

类成员

为了访问类实例的成员,使用以下语法:<class instance>.<member>。也可以使用 <class name>.<member> 访问类定义中的成员。

方法

方法是类中的一个函数。方法的第一个参数(方法必须至少接受一个参数)始终是调用该函数的类实例。例如:

class Foo:
    def setx(self, x):
        self.x = x
    def bar(self):
        print(self.x)

如果执行这段代码,什么也不会发生,直到构造 Foo 类的一个实例,并且调用该实例的 bar 方法时,才会发生输出。

为什么方法需要一个参数?

在普通函数中,如果你设置一个变量,如 test = 23,你无法访问 test 变量。输入 test 会说它没有定义。类函数中也存在类似的问题,除非它们使用 self 变量。

基本上,在上面的例子中,如果我们删除 self.x函数 bar 将无法做任何事,因为它无法访问 xsetx() 中的 x 将消失。self 参数将变量保存在类的“共享变量”数据库中。

为什么使用 self

你不一定需要使用 self,但是使用 self 是一种规范。

调用方法

调用方法的方式与调用函数相似,不同之处在于你不需要将实例作为第一个参数传递,而是将方法作为实例的一个属性来使用。

f = Foo()
f.setx(5)
f.bar()

这将输出:

5

你也可以在任意对象上调用方法,将其作为类定义的属性,而不是类的实例:

Foo.setx(f, 5)
Foo.bar(f)

这将产生相同的输出。

动态类结构

如上面的 setx 方法所示,Python 类的成员在运行时可以变化,不仅是它们的值,甚至可以改变类的定义。我们甚至可以在运行上面的代码后删除 f.x

del f.x
f.bar()

输出:

Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 5, in bar
AttributeError: Foo instance has no attribute 'x'

另一个效果是我们可以在程序执行过程中改变 Foo 类的定义。下面的代码中,我们在 Foo 类定义中创建了一个名为 y 的成员。如果我们创建一个新的 Foo 实例,它将包含这个新成员:

Foo.y = 10
g = Foo()
print(g.y)  # 输出 10

查看类的字典

所有这一切的核心是一个字典,可以通过 vars(ClassName) 访问:

print(vars(g))  # 输出 {}

开始时,输出为空。虽然我们看到 g 具有 y 成员,但为什么它不在成员字典中呢?如果你记得,我们是把 y 放在了类定义 Foo 中,而不是放在 g 中。

print(vars(Foo))  # 输出 {'y': 10, 'bar': <function bar at 0x4d6a3c>, '__module__': '__main__', 'setx': <function setx at 0x4d6a04>, '__doc__': None}

这就是 Foo 类定义的所有成员。当 Python 检查 g.member 时,它首先检查 gvars 字典中的 member,然后检查 Foo。如果我们为 g 创建一个新成员,它将被添加到 g 的字典中,但不会影响 Foo

g.setx(5)
print(vars(g))  # 输出 {'x': 5}

请注意,如果我们现在为 g.y 赋值,这并不是给 Foo.y 赋值。Foo.y 仍然是 10,但 g.y 现在将覆盖 Foo.y

g.y = 9
print(vars(g))  # 输出 {'y': 9, 'x': 5}
print(vars(Foo))  # 输出 {'y': 10, 'bar': <function bar at 0x4d6a3c>, '__module__': '__main__', 'setx': <function setx at 0x4d6a04>, '__doc__': None}

确认类实例属性

如果我们检查值:

>>> g.y
9
>>> Foo.y
10

请注意,f.y 也会是 10,因为 Python 在 vars(f) 中找不到 y,所以它会从 vars(Foo) 中获取 y 的值。

一些人可能还注意到,Foo 中的方法与 xy 一起出现在类的字典中。如果你记得在 lambda 函数的章节中,我们可以像对待变量一样对待函数。这意味着我们可以在运行时像给类分配变量一样给类分配方法。如果你这样做,记住当我们调用类实例的方法时,传递给方法的第一个参数将始终是类实例本身。

更改类字典

我们还可以使用类的 __dict__ 成员访问类的成员字典。

>>> g.__dict__
{'y': 9, 'x': 5}

如果我们添加、删除或更改 g.__dict__ 中的键值对,这与我们直接更改 g 的成员一样:

>>> g.__dict__['z'] = -4
>>> g.z
-4

为什么使用类?

类的特殊之处在于,一旦创建了实例,实例就与其他所有实例独立。我可以有两个实例,每个实例有不同的 x 值,它们不会相互影响:

f = Foo()
f.setx(324)
f.boo()
g = Foo()
g.setx(100)
g.boo()

f.boo()g.boo() 将输出不同的值。

新式类

新式类在 Python 2.2 中引入。新式类是以内建类作为基类的类,通常是 object。在低级别上,旧类和新类的主要区别在于它们的类型。旧类实例的类型是 instance,而新式类的实例会返回与 x.__class__ 相同的类型。这使得用户定义的类与内建类平起平坐。在 Python 3 中,旧/经典类将被淘汰。因此,所有的开发都应该使用新式类。新式类还增加了 Java 程序员熟悉的属性和静态方法等结构。

经典类

>>> class ClassicFoo:
...     def __init__(self):
...         pass

新式类

>>> class NewStyleFoo(object):
...     def __init__(self):
...         pass

属性

属性是带有 getter 和 setter 方法的成员。

>>> class SpamWithProperties(object):
...     def __init__(self):
...         self.__egg = "MyEgg"
...     def get_egg(self):
...         return self.__egg
...     def set_egg(self, egg):
...         self.__egg = egg
...     egg = property(get_egg, set_egg)
>>> sp = SpamWithProperties()
>>> sp.egg
'MyEgg'
>>> sp.egg = "Eggs With Spam"
>>> sp.egg
'Eggs With Spam'

从 Python 2.6 开始,也可以使用 @property 装饰器

>>> class SpamWithProperties(object):
...     def __init__(self):
...         self.__egg = "MyEgg"
...     @property
...     def egg(self):
...         return self.__egg
...     @egg.setter
...     def egg(self, egg):
...         self.__egg = egg

静态方法

Python 中的静态方法就像 C++ 或 Java 中的静态方法一样。静态方法没有 self 参数,也不需要在使用它们之前实例化类。它们可以通过 staticmethod() 来定义:

>>> class StaticSpam(object):
...     def StaticNoSpam():
...         print("You can't have the spam, spam, eggs and spam without any spam... that's disgusting")
...     NoSpam = staticmethod(StaticNoSpam)
>>> StaticSpam.NoSpam()
You can't have the spam, spam, eggs and spam without any spam... that's disgusting

它们也可以使用函数装饰器 @staticmethod 来定义:

>>> class StaticSpam(object):
...     @staticmethod
...     def StaticNoSpam():
...         print("You can't have the spam, spam, eggs and spam without any spam... that's disgusting")

继承

像所有面向对象语言一样,Python 支持继承。继承是一个简单的概念,通过继承,一个类可以扩展另一个类的功能,或者在 Python 中,可以继承多个类。使用以下格式:

class ClassName(BaseClass1, BaseClass2, BaseClass3,...):
    ...

ClassName 是派生类,即从基类派生出来的类。派生类将继承基类的所有成员。如果派生类和基类都定义了一个方法,派生类中的方法会覆盖基类中的方法。如果要使用基类中定义的方法,需要通过类的属性调用该方法,如前面的 Foo.setx(f,5)

>>> class Foo:
...     def bar(self):
...         print("I'm doing Foo.bar()")
...     x = 10
...
>>> class Bar(Foo):
...     def bar(self):
...         print("I'm doing Bar.bar()")
...         Foo.bar(self)
...     y = 9
...
>>> g = Bar()
>>> Bar.bar(g)
I'm doing Bar.bar()
I'm doing Foo.bar()
>>> g.y
9
>>> g.x
10

我们可以通过查看类字典来了解底层的工作原理。

>>> vars(g)
{}
>>> vars(Bar)
{'y': 9, '__module__': '__main__', 'bar': <function bar at 0x4d6a3c>, '__doc__': None}
>>> vars(Foo)
{'x': 10, '__module__': '__main__', 'bar': <function bar at 0x4d6994>, '__doc__': None}

当我们调用 g.x 时,它首先会查看 vars(g) 字典,通常是这样做的。正如上面所示,它接着会查看 vars(Bar),因为 gBar 的实例。然而,得益于继承,Python 会检查 vars(Foo),如果在 vars(Bar) 中找不到 x

多重继承

如继承部分所示,一个类可以从多个类派生:

class ClassName(BaseClass1, BaseClass2, BaseClass3):
    pass

多重继承的一个棘手问题是方法解析:当调用一个方法时,如果方法名称在多个基类或它们的基类中都有定义,应该调用哪个基类的方法?

方法解析顺序(MRO)取决于类是旧式类还是新式类。对于旧式类,派生类是从左到右考虑的,并且会在考虑右边之前检查基类的基类。因此,上述代码中,BaseClass1 会首先被考虑,如果没有找到方法,就会考虑 BaseClass1 的基类。如果仍然没有找到,接着考虑 BaseClass2,然后是它的基类,依此类推。对于新式类,请参见 Python 官方文档。

特殊方法

有许多方法具有保留名称,用于特殊目的,如模拟数值或容器操作等。所有这些方法名都以两个下划线开头和结尾。约定是,以下划线开头的方法被认为是类内的“私有”方法。

初始化和删除

__init__

__init__ 是用于构造类实例的特殊方法。__init__() 在实例返回之前调用(不需要手动返回实例)。例如:

class A:
    def __init__(self):
        print('A.__init__()')
a = A()

输出:

A.__init__()

__init__() 可以接受参数,在这种情况下,创建实例时必须传递参数。例如:

class Foo:
    def __init__(self, printme):
        print(printme)
foo = Foo('Hi!')

输出:

Hi!

以下是使用 __init__() 和不使用 __init__() 的区别:

class Foo:
    def __init__(self, x):
        print(x)
foo = Foo('Hi!')

class Foo2:
    def setx(self, x):
        print(x)
f = Foo2()
Foo2.setx(f, 'Hi!')

输出:

Hi!
Hi!

__del__

类似地,__del__ 在实例被销毁时调用,例如,当实例不再被引用时。

__enter____exit__

这两个方法也充当构造函数和析构函数,但它们只在使用 with 语句时执行。例如:

class ConstructorsDestructors:
    def __init__(self):
        print('init')

    def __del__(self):
        print('del')

    def __enter__(self):
        print('enter')

    def __exit__(self, exc_type, exc_value, traceback):
        print('exit')

with ConstructorsDestructors():
    pass

输出:

init
enter
exit
del

__new__

__new__元类构造器。

表示

__str__

__str__ 用于将对象转换为字符串,通常在打印语句或 str() 转换函数中使用。如果需要自定义格式,可以重写 __str__。通常,__str__ 返回对象内容的格式化版本。这个方法通常不用于执行代码。

例如:

class Bar:
    def __init__(self, iamthis):
        self.iamthis = iamthis
    def __str__(self):
        return self.iamthis
bar = Bar('apple')
print(bar)

输出:

apple

__repr__

__repr____str__() 类似。如果 __str__ 不存在,但 __repr__ 存在,那么打印时会使用 __repr__ 的输出。__repr__ 用于返回对象的字符串表示。通常,__repr__ 可以执行以返回原始对象。

例如:

class Bar:
    def __init__(self, iamthis):
        self.iamthis = iamthis
    def __repr__(self):
        return "Bar('%s')" % self.iamthis
bar = Bar('apple')
bar

输出:

Bar('apple')

字符串表示重写函数

函数 操作符
__str__ str(A)
__repr__ repr(A)
__unicode__ unicode(x) (仅 2.x)

属性

__setattr__

__setattr__ 是负责设置类属性的函数。它接受变量的名称和值。每个类都有一个默认的 __setattr__,它会简单地设置变量的值,但我们可以覆盖它。

>>> class Unchangable:
...    def __setattr__(self, name, value):
...        print("Nice try")
...
>>> u = Unchangable()
>>> u.x = 9
Nice try
>>> u.x
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: Unchangable instance has no attribute 'x'

__getattr__

__setattr__ 类似,不同的是,这个函数在我们尝试访问类成员时调用,默认会返回成员的值。

>>> class HiddenMembers:
...     def __getattr__(self, name):
...         return "You don't get to see " + name
...
>>> h = HiddenMembers()
>>> h.anything
"You don't get to see anything"

删除属性

函数用于删除属性。

>>> class Permanent:
...     def __delattr__(self, name):
...         print(name, "cannot be deleted")
...
>>> p = Permanent()
>>> p.x = 9
>>> del p.x
x cannot be deleted
>>> p.x
9

属性重写函数

函数 间接形式 直接形式
__getattr__ getattr(A, B) A.B
__setattr__ setattr(A, B, C) A.B = C
__delattr__ delattr(A, B) del A.B

操作符重载

操作符重载允许我们使用内建的 Python 语法和操作符来调用我们定义的函数

二元操作符

如果一个类有 __add__ 方法,我们可以使用 + 操作符来相加类的实例。这将调用 __add__ 方法,传递两个类的实例作为参数,返回值将是相加的结果。

>>> class FakeNumber:
...     n = 5
...     def __add__(A, B):
...         return A.n + B.n
...
>>> c = FakeNumber()
>>> d = FakeNumber()
>>> d.n = 7
>>> c + d
12

要重载增强赋值操作符,只需在正常的二元操作符前添加 'i',即对于 += 使用 __iadd__ 而不是 __add__。该函数将接收一个参数,即增强赋值操作符右边的对象。返回值将赋值给操作符左边的对象。

>>> c.__imul__ = lambda B: B.n - 6
>>> c *= d
>>> c
1

值得注意的是,如果增强赋值操作符函数没有直接设置,增强赋值操作符也会使用正常的操作符函数。这将按预期工作,__add__ 被用于 +=,以此类推。

>>> c = FakeNumber()
>>> c += d
>>> c
12

二元操作符重写函数

函数 操作符
__add__ A + B
__sub__ A - B
__mul__ A * B
__truediv__ A / B
__floordiv__ A // B
__mod__ A % B
__pow__ A ** B
__and__ A & B
__or__ `A
__xor__ A ^ B
__eq__ A == B
__ne__ A != B
__gt__ A > B
__lt__ A < B
__ge__ A >= B
__le__ A <= B
__lshift__ A << B
__rshift__ A >> B
__contains__ A in B
  A not in B

一元操作符

一元操作符只会传递类的实例。

>>> FakeNumber.__neg__ = lambda A: A.n + 6
>>> -d
13

一元操作符重写函数

函数 操作符
__pos__ +A
__neg__ -A
__inv__ ~A
__abs__ abs(A)
__len__ len(A)

项目操作符

在 Python 中,我们还可以重载索引和切片操作符。这允许我们对自己的对象使用 class[i]class[a:b] 语法。

最简单的项操作符是 __getitem__,它接受类的实例作为参数,以及索引的值。

>>> class FakeList:
...     def __getitem__(self, index):
...         return index * 2
...
>>> f = FakeList()
>>> f['a']
'aa'

我们还可以为将值分配给项的语法定义一个函数。此函数的参数包括要分配的值,以及来自 __getitem__ 的参数。

>>> class FakeList:
...     def __setitem__(self, index, value):
...         self.string = index + " is now " + value
...
>>> f = FakeList()
>>> f['a'] = 'gone'
>>> f.string
'a is now gone'

我们可以对切片做同样的事情。每种语法有不同的参数列表。

>>> class FakeList:
...     def __getslice__(self, start, end):
...         return str(start) + " to " + str(end)
...
>>> f = FakeList()
>>> f[1:4]
'1 to 4'

请记住,在切片语法中,开始和结束参数可以为空。在这里,Python 为开始和结束提供了默认

值,如下所示:

>>> f[:]
'0 to 2147483647'

请注意,切片的结束默认值是 32 位系统中最大可能的有符号整数,具体取决于系统和 C 编译器。

__setslice__ 的参数是 (self, start, end, value),我们还可以为删除项和切片定义操作符。

__delitem__ 的参数是 (self, index)
__delslice__ 的参数是 (self, start, end)
注意,这些与 __getitem____getslice__ 的参数是相同的。

项目操作符重写函数

函数 操作符
__getitem__ C[i]
__setitem__ C[i] = v
__delitem__ del C[i]
__getslice__ C[s:e]
__setslice__ C[s:e] = v
__delslice__ del C[s:e]

其他重载

函数 操作符
__cmp__ cmp(x, y)
__hash__ hash(x)
__nonzero__ bool(x)
__call__ f(x)
__iter__ iter(x)
__reversed__ reversed(x)
__divmod__ divmod(x, y)
__int__ int(x)
__long__ long(x)
__float__ float(x)
__complex__ complex(x)
__hex__ hex(x)
__oct__ oct(x)
__index__  
__copy__ copy.copy(x)
__deepcopy__ copy.deepcopy(x)
__sizeof__ sys.getsizeof(x)
__trunc__ math.trunc(x)
__format__ format(x, ...)

编程实践

Python 类的灵活性意味着类可以采用多种不同的行为。然而,为了易于理解,最好谨慎使用 Python 提供的许多工具。尽量在类定义中声明所有方法,并始终使用 <class>.<member> 语法,而不是 __dict__,除非绝对必要。可以参考 C++ 和 Java 中的类,了解大多数程序员期望的类的表现形式。

封装

由于 Python 类中的所有成员都可以通过类外的函数或方法访问,因此除了重载 __getattr____setattr____delattr__,就没有办法强制执行封装。然而,通常做法是类或模块的创建者信任用户只使用预定的接口,而不去限制用户对模块工作原理的访问。如果使用类或模块的非预期部分,请记住,这些部分可能会在以后的版本中发生变化,甚至可能导致错误或未定义的行为,因为封装是私有的。

文档字符串

在定义类时,使用字符串文字来记录类的文档是一个约定。这个字符串将被放入类定义的 __doc__ 属性中。

>>> class Documented:
...     """This is a docstring"""
...     def explode(self):
...         """
...         This method is documented, too! The coder is really serious about
...         making this class usable by others who don't know the code as well
...         as he does.
...
...         """
...         print("boom")
>>> d = Documented()
>>> d.__doc__
'This is a docstring'

文档字符串是记录代码的非常有用的方式。即使你从未编写过一行单独的文档(并且让我们承认,这对许多开发人员来说是最低优先级的事情),在类中包含有用的文档字符串将大大提升它们的可用性。

有许多工具可以将 Python 代码中的文档字符串转化为可读的 API 文档,例如 EpyDoc。

不仅仅是类定义需要文档化。类中的每个方法也应该有自己的文档字符串。注意,上面 Documented 类中的 explode 方法的文档字符串较长,跨越了几行。它的格式符合 Python 创始人 Guido van Rossum 在 PEP 8 中提出的风格建议。

在运行时添加方法

添加到类

在运行时向类添加方法是相当容易的。假设我们有一个叫做 Spam 的类和一个函数 cook。我们希望能够在所有 Spam 类的实例上使用 cook 函数

class Spam:
  def __init__(self):
    self.myeggs = 5

def cook(self):
  print("cooking %s eggs" % self.myeggs)

Spam.cook = cook   #向类 Spam 添加函数
eggs = Spam()      #现在创建一个 Spam 类的新实例
eggs.cook()        #准备做饭!

这将输出:

cooking 5 eggs
添加到类的实例

向已经创建的类实例添加方法稍微复杂一些。假设我们再次拥有一个名为 Spam 的类,并且已经创建了一个 eggs 实例。但是后来我们注意到我们想做饭,但我们不想创建新实例,而是使用已经创建的实例:

class Spam:
  def __init__(self):
    self.myeggs = 5

eggs = Spam()

def cook(self):
  print("cooking %s eggs" % self.myeggs)

import types
f = types.MethodType(cook, eggs, Spam)
eggs.cook = f

eggs.cook()

现在我们可以做饭了,最后一句将输出:

cooking 5 eggs
使用函数

我们还可以编写一个函数,使为类实例添加方法的过程更加简便。

def attach_method(fxn, instance, myclass):
  f = types.MethodType(fxn, instance, myclass)
  setattr(instance, fxn.__name__, f)

现在,我们只需调用 attach_method,传入要附加的函数、要附加到的实例和该实例所属的类。我们可以像这样调用:

attach_method(cook, eggs, Spam)
最后修改: 2025年01月31日 星期五 00:42