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 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 类的定义。在下面的代码中,我们在 Foo 类的定义中创建一个名为 y 的成员。如果我们然后创建一个 Foo 的新实例,它现在将具有此新成员。
>>> Foo.y = 10
>>> g = Foo()
>>> g.y
10
这一切的核心是一个 字典,可以通过“vars(ClassName)”访问
>>> vars(g)
{}
最初,此输出没有意义。我们刚刚看到 g 具有成员 y,那么为什么它不在成员字典中?但是,如果你还记得,我们是在类定义 Foo 中,而不是在 g 中添加 y。
>>> 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
元类 构造函数。
将对象转换为字符串,如使用 print 语句或使用 str() 转换函数,可以通过覆盖 __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
输出(注意区别:可能没有必要将其放在 print 中,但在 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 类中的所有 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。但我们注意到,我们想要烹饪这些鸡蛋,但我们不想创建新的实例,而是使用已经创建的实例。
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)
注意,在函数add_method中,我们不能写instance.fxn = f因为这会将一个名为fxn的函数添加到实例中。
- 9. 类,docs.python.org
- 2. 内置函数 # vars,docs.python.org