Python 编程/类
类是将类似数据和函数聚合在一起的一种方式。类基本上是一个范围,在该范围内执行各种代码(尤其是函数定义),并且该范围的局部变量成为类的属性,以及该类构建的任何对象的属性。由类构建的对象称为该类的实例。
一瞥 Python 中的类
import math
class MyComplex:
"""A complex number""" # Class documentation
classvar = 0.0 # A class attribute, not an instance one
def phase(self): # A method
return math.atan2(self.imaginary, self.real)
def __init__(self): # A constructor
"""A constructor"""
self.real = 0.0 # An instance attribute
self.imaginary = 0.0
c1 = MyComplex()
c1.real = 3.14 # No access protection
c1.imaginary = 2.71
phase = c1.phase() # Method call
c1.undeclared = 9.99 # Add an instance attribute
del c1.undeclared # Delete an instance attribute
print(vars(c1)) # Attributes as a dictionary
vars(c1)["undeclared2"] = 7.77 # Write access to an attribute
print(c1.undeclared2) # 7.77, indeed
MyComplex.classvar = 1 # Class attribute access
print(c1.classvar == 1) # True; class attribute access, not an instance one
print("classvar" in vars(c1)) # False
c1.classvar = -1 # An instance attribute overshadowing the class one
MyComplex.classvar = 2 # Class attribute access
print(c1.classvar == -1) # True; instance attribute access
print("classvar" in vars(c1)) # True
class MyComplex2(MyComplex): # Class derivation or inheritance
def __init__(self, re = 0, im = 0):
self.real = re # A constructor with multiple arguments with defaults
self.imaginary = im
def phase(self):
print("Derived phase")
return MyComplex.phase(self) # Call to a base class; "super"
c3 = MyComplex2()
c4 = MyComplex2(1, 1)
c4.phase() # Call to the method in the derived class
class Record: pass # Class as a record/struct with arbitrary attributes
record = Record()
record.name = "Joe"
record.surname = "Hoe"
要定义类,请使用以下格式
class ClassName:
"Here is an explanation about your class"
pass
此类定义中的大写是约定,但语言不要求。通常最好至少添加一个简短的说明,说明你的类应该做什么。上面的代码中的 pass 语句只是告诉 Python 解释器继续执行,什么也不做。你可以在添加第一个语句后将其删除。
该类是一个可调用对象,当被调用时,它会构建该类的实例。假设我们创建一个类 Foo。
class Foo:
"Foo is our new toy."
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 将无法执行任何操作,因为它无法访问 x。setx() 中的 x 会消失。self 参数将变量保存到类的“共享变量”数据库中。
你不需要使用 self。但是,使用 self 是一个规范。
调用方法与调用函数非常相似,但是不要像形式参数列表所暗示的那样将实例作为第一个参数传递,而是将函数用作实例的属性。
>>> f = Foo()
>>> f.setx(5)
>>> f.bar()
这将输出
5
可以通过使用它作为定义类的属性而不是该类的实例,在任意对象上调用方法,如下所示
>>> Foo.setx(f,5)
>>> Foo.bar(f)
这将产生相同的输出。
正如 setx 方法所示,Python 类的成员可以在运行时更改,而不仅仅是它们的价值,这与 C++ 或 Java 等语言中的类不同。我们甚至可以在运行上面的代码后删除 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 类的定义。在下面的代码中,我们创建了一个名为 y 的 Foo 类定义的成员。如果我们再创建 Foo 的新实例,它现在将拥有这个新成员。
>>> Foo.y = 10
>>> g = Foo()
>>> g.y
10
这一切的核心是一个 字典,可以通过“vars(ClassName)”访问
>>> vars(g)
{}
起初,这个输出毫无意义。我们刚刚看到 g 拥有成员 y,那么为什么它不在成员字典中?但是,如果你还记得,我们将 y 放入了类定义 Foo 中,而不是 g 中。
>>> vars(Foo)
{'y': 10, 'bar': <function bar at 0x4d6a3c>, '__module__': '__main__',
'setx': <function setx at 0x4d6a04>, '__doc__': None}
在那里,我们拥有 Foo 类定义的所有成员。当 Python 检查 g.member 时,它首先在 g 的 vars 字典中检查“member”,然后检查 Foo。如果我们创建 g 的新成员,它将被添加到 g 的字典中,但不会被添加到 Foo 的字典中。
>>> g.setx(5)
>>> vars(g)
{'x': 5}
请注意,如果我们现在为 g.y 分配一个值,我们不是将该值分配给 Foo.y。Foo.y 将仍然是 10,但 g.y 现在将覆盖 Foo.y
>>> g.y = 9
>>> vars(g)
{'y': 9, 'x': 5}
>>> 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 中的方法出现在类字典中,与 x 和 y 位于同一位置。如果你还记得关于 lambda 函数 的部分,我们可以像变量一样对待函数。这意味着我们可以像分配变量一样在运行时将方法分配给类。但是,如果你这样做,请记住,如果我们调用类实例的方法,传递给该方法的第一个参数将始终是类实例本身。
我们还可以使用类的 __dict__ 成员访问类的成员字典。
>>> g.__dict__
{'y': 9, 'x': 5}
如果我们从 g.__dict__ 中添加、删除或更改键值对,这与我们对 g 的成员进行了那些更改的效果相同。
>>> g.__dict__['z'] = -4
>>> g.z
-4
类很特别,因为一旦创建了一个实例,该实例就独立于所有其他实例。我可以有两个实例,每个实例都有不同的 x 值,它们不会影响彼此的 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 have the spam, spam, eggs and spam without any spam... that's disgusting")
... NoSpam = staticmethod(StaticNoSpam)
>>> StaticSpam.NoSpam()
You can't have 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 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 0x4d6a04>,
'__doc__': None}
>>> vars(Foo)
{'x': 10, '__module__': '__main__', 'bar': <function bar at 0x4d6994>,
'__doc__': None}
当我们调用 g.x 时,它首先像往常一样在 vars(g) 字典中查找。与上面一样,它接下来检查 vars(Bar),因为 g 是 Bar 的实例。然而,由于继承,如果它没有在 vars(Bar) 中找到 x,Python 将检查 vars(Foo)。
如#继承节所示,一个类可以从多个类派生
class ClassName(BaseClass1, BaseClass2, BaseClass3):
pass
多重继承中一个棘手的地方是方法解析:在方法调用时,如果方法名在多个基类或其基类中可用,那么应该调用哪个基类方法。
方法解析顺序取决于类是旧式类还是新式类。对于旧式类,派生类是从左到右考虑的,基类的基类在移至右侧之前被考虑。因此,在上面,BaseClass1 首先被考虑,如果在那里没有找到方法,就会考虑 BaseClass1 的基类。如果失败,则考虑 BaseClass2,然后是其基类,依此类推。对于新式类,请参阅 Python 在线文档。
链接
- 9.5.1. 多重继承,docs.python.org
- Python 2.3 方法解析顺序,python.org
有许多方法具有保留名称,这些名称用于特殊目的,例如模拟数值或容器操作等等。所有这些名称都以两个下划线开头和结尾。约定俗成,以单个下划线开头的函数是它们被引入的范围内的 "私有" 的。
这些目的之一是构造一个实例,它的特殊名称是 "__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__";例如,当它不再被引用时。
这些方法也是构造函数和析构函数,但它们只在使用 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
元类 构造函数。
通过覆盖 __str__ 可以覆盖将对象转换为字符串(如使用 print 语句或使用 str() 转换函数)的行为。通常,__str__ 返回对象内容的格式化版本。这通常不会是可执行的代码。 例如 class Bar:
def __init__ (self, iamthis):
self.iamthis = iamthis
def __str__ (self):
return self.iamthis
bar = Bar('apple')
print(bar)
输出 apple 这个函数很像 __str__()。如果 __str__ 不存在,但这个函数存在,则使用该函数的输出作为打印的替代。__repr__ 用于以字符串形式返回对象的表示形式。通常,它可以被执行以获取回原始对象。 例如 class Bar:
def __init__ (self, iamthis):
self.iamthis = iamthis
def __repr__(self):
return "Bar('%s')" % self.iamthis
bar = Bar('apple')
bar
输出(注意区别:可能没有必要将其放在打印语句中,但在 Python 2.7 中确实需要这样做) Bar('apple') |
|
这是负责设置类属性的函数。 它被提供变量被赋值的名称和值。 当然,每个类都带有一个默认的 __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' 类似于 __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
|
|
运算符重载允许我们使用内置的 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
|
|
一元运算符将只传递它们被调用的类的实例。 >>> FakeNumber.__neg__ = lambda A : A.n + 6
>>> -d
13
|
|
在 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 编译器而异。
我们还有用于删除项目和切片的运算符。
请注意,这些与 __getitem__ 和 __getslice__ 相同。 |
|
|
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 #add the function to the class Spam
eggs = Spam() #NOW create a new instance of Spam
eggs.cook() #and we are ready to cook!
这将输出
cooking 5 eggs
向已创建的类实例添加方法有点棘手。再次假设我们有一个名为 Spam 的类,并且我们已经创建了 eggs。但后来我们注意到我们想烹饪这些 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()
现在我们可以烹饪我们的 eggs,最后一条语句将输出
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)
注意,在 add_method 函数中,我们不能写instance.fxn = f因为这会向实例添加一个名为 fxn 的函数。
- 9. 类,docs.python.org
- 2. 内置函数 # vars,docs.python.org