[Python]类与面向对象编程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Python]类与面向对象编程相关的知识,希望对你有一定的参考价值。

1. class语句

类通常是由函数、变量和属性组成的集合。使用class语句可以定义类,例如:

class Account(object):
    num_accounts = 0
    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        Account.num_accounts += 1
    def __del__(self):
        Account.num_accounts -= 1
    def deposit(self, amt):
        self.balance = self.balance + amt
    def withdraw(self, amt):
        self.balance = self.balance - amt
    def inquiry(self):
        return self.balance

在类主体执行期间创建的值放在类对象中,这个对象充当着命名空间,例如:

Account.num_accunts
Account.__init__
Account.__del__
Account.deposit
Account.withdraw
Account.inquiry

需要注意的是,class语句本身并不创建该类的任何类型。类仅设置将在以后创建的所有实例都使用的属性。类中定义的函数称为实例方法。类的实例作为第一个参数传递,根据约定,这个参数称为self,但所有合法的标识符都可以使用。类变量是可在类的所有实例之间共享的值。比如上例的num_accounts变量用于跟踪存在多少个Account实例。
?

2. 类实例

类的实例是以函数形式调用类对象来创建的。这种方法将创建一个新实例,而后将该实例传递给类的__init__()方法。__init__()方法的参数包括新创建的实例self和在调用类对象时提供的参数。例如:

a = Account("Guido", 1000.00)
b = Account("Bill", 10.00)

通过将属性分配给self来将其保存到实例中。例如self.name = name表示将name属性保存在衫例中。使用"."运算符可以访问这些属性以及类属性,例如:

a.deposit(100.00)
b.withdraw(50.00)
name = a.name

尽管类会定义命名空间,但它们不会为在方法体内使用的名称限定范围。所以在实现类时,对属性和方法的引用必须是完全限定的。比如之前的例子中使用的是self.balance而非balance。如果希望从一个方法中调用另一个方法,也可以采用这种方式,例如:

class Foo(object):
    def bar(self):
        print("bar!")
    def spam(self):
        bar(self) # 错误,抛出NameError异常
        self.bar()
        Foo.bar(self)

?

3. 继承

继承是一种创建新类的机制。原始类称为基类或超类。新类称为派生类或子类。通过继承创建类时,所创建的类将“继承”其基类定义的属性。派生类可以重新定义属性并添加自己的属性。
在class语句中使用以逗号分隔的基类名称列表来指定继承。如果没有有效的基类,将继承object。继承通常会重新定义现有方法的行为,例如:

import random
class EvilAccount(Account):
    def inquiry(self):
        if andom.randint(0, 4) == 1:
            return self.balance * 1.10
        else:
            return self.balance
c = EvilAccount("George", 1000.00)
c.deposit(10.0)
available = c.inquiry()

如果搜索一个属性时未在实例或实例的类中找到匹配项,搜索将会在基类上进行。这个过程会一直继续下去,直到没有更多的基类可供搜索。子类可以定义自己的__init__()方法。因此,要由派生类调用基类的__init__()方法来对它们进行恰当的初始化。如果基类未定义__init__(),就可以忽略这一步。如果不知道基类是否定义了__init__(),可在不提供任何参数的情况下调用它,因为始终存在一个不执行任何操作的默认__init__()实现。例如:

class EvilAccount(Account):
    def __init__(self, name, balance, evilfactor):
        Account.__init__(self, name, balance)
        self.evilfactor = evilfactor
    def inquiry(self):
        if random.randint(0, 4) == 1:
            return self.balance * self.evilfactor
        else:
            return self.balance

有时,派生类重新实现了方法,但是还想调用原始的实现,可以将实例self作为第一个参数传递,例如:

class MoreEvilAccount(EvilAccount):
    def deposit(self, amount):
        self.withdraw(5.00)
        EvilAccount.deposit(self, amount)

但是这种写法容易引起一些混淆,可以使用另一种方案,用super()函数,例如:

class MoreEvilAccount(EvilAccount):
    def deposit(self, amount):
        self.withdraw(5.00)
        super().deposit(amount)

Python支持多重继承,通过让一个类列出多个基类即可指定多重继承,例如:

class DepositCharge(object):
    fee = 5.00
    def deposit_fee(self):
        print(self.fee)

class WithdrawCharge(object):
    fee = 2.50
    def withdraw_fee(self):
        print(self.fee)

class MostEvilAccount(EvilAccount, DepositCharge, WithdrawCharge):
    def deposit(self, amt):
        self.deposit_fee()
        super().deposit(amt)
    def withdraw(self, amt):
        self.withdraw_fee()
        super().withdraw(amt)

withdraw_fee()实际并未使用在自己的类中初始化的fee值。属性fee是在两个不同的基类中定义的类变量,程序使用了其中一个。要找到使用了多重继承的属性,可以在列表中对所有基类按从“最特殊”的类到“最不特殊”的类。而后在搜索属性时,就会按这个顺序搜索列表,直至找到该属性的第一个定义。对于任何给定的类,通过打印它的__mro__属性即可查看基类的顺序。

?

4. 静态方法和类方法

静态方法是一种普通函数,就位于类定义的命名空间中。要定义静态方法,可使用@staticmethod装饰器,例如:

class Foo(object):
    @staticmethod
    def add(x, y):
        return x + y

要调用静态方法,只需用类名作为它的前缀,例如:

x = Foo.add(3, 4)

类方法是将类本身作为对象进行操作的方法。类方法使用@classmethod装饰器定义,根据约定,类是作为第一个参数(名为cls)传递的,例如:

class Times(object):
    factor = 1
    @classmethod
    def mul(cls, x):
        return cls.factor * x

class TwoTimes(Times):
    factor = 2

x = TwoTimes.mul(4)

?

5. 特性

特性是一种特殊的属性,访问它时会计算它的值,例如:

class Circle(object):
    def __init__(self, radius):
        self.radius = radius
    @property
    def area(self):
        return math.pi * self.radius ** 2
    @property
    def perimeter(self):
        return 2 * math.pi * self.radius

在这个例子中,Circle实例存储了一个实例变量c.radius。c.area和c.perimeter是根据该值计算得来的。@property装饰器支持以简单属性的形式访问后面的方法,无需添加额外的()来调用该方法。方法本身是作为一类特性被隐式处理的,当创建一个实例然后访问实例的方法时,不会返回原始函数对象,会得到绑定方法。绑定方法是一个对象,表示将在对象中调用()运算符时执行的方法调用。这种绑定方法对象是由在后台执行的特性函数静默创建的。使用@staticmethod和@classmethod定义静态方法和类方法时,实际上就指定了使用不同的特性函数。
特性还可以拦截操作,以设置和删除属性。这是通过向特性附加其他setter和deleter方法来实现的,例如:

class Foo(object):
    def __init__(self, name):
        self.__name = name
    @property
    def name(self):
        return self.__name
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("Must be a string!")
        self.__name = value
    @name.deleter
    def name(self):
        raise TypeError("Can‘t delete name")

f = Foo("Guido")
n = f.name
f.name = "Monty" # 调用setter
f.name = 45 # 调用setter(TypeError)
del f.name

?

6. 描述符

使用特性后,对属性的访问将由一系列用户定义的get、set和delete函数控制。这种属性控制方式可以通过描述符对象进一步推广。描述符就是一个表示属性值的对象,通过实现一个或多个特殊的__get__()、__set__()和__delete__()方法,可以将描述符与属性访问机制挂钩,例如:

class TypedProperty(object):
    def __init__(self, name, type, default=None):
        self.name = "_" + name
        self.type = type
        self.default = default if default else type()
    def __get__(self, instance, cls):
        return getattr(instance, self.name, self.default)
    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError("Must be a %s" % self.type)
        setattr(instance, self.name, value)
    def __delete__(self, instance):
        raise AttributeError("Can‘t delete attribute")

class Foo(object):
    name = TypedProperty("name", str)
    num = TypedProperty("num", int, 42)

在这个例子中,类TypedProperty定义了一个描述符,分配属性时它将进行类型检查,例如:

f = Foo()
a = f.name # 隐式调用Foo.name.__get__(f.Foo)
f.name = "Guido" # 调用Foo.name.__set__(f, "Guido")
del f.name # 调用Foo.name.__delete__(f)

?

7. 数据封装和私有属性

默认情况下,类的所有属性和方法都是“公共的”。这意味着对它们的访问没有任何限制。在基类中的所有内容都会被 派生类继承,并可从派生类内进行访问。这可能导致在派生类中定义的对象与在基类中定义的对象之间发生命名空间冲突,为了解决该问题,类中所有以双下划线开头的名称都会变成具有_类名__Foo形式的新名称。例如:

class A(object):
    def __init__(self):
        self.__X = 3 # 变为self._A__X
    def __spam(self): # 变成_A__spam()
        pass
    def bar(self):
        self.__spam() # 只调用A.__spam()

class B(A):
    def __init__(self):
        A.__init__(self)
        self.__X = 37 # 变为self._B__X
    def __spam(self): # 变为_B__spam()
        pass

这种方案似乎隐藏了数据,但并没有严格的机制来实际阻止对类的“私有”属性进行访问。尽管这种变形似乎是一个额外的处理步骤,但变形过程实际上只在定义类时发生一次。而且,名称变形不会在getattr()、hasattr()、setattr()或delattr()等函数中发生,因为在这些函数中,属性名称指定为字符串。对于这些函数,需要显示使用变形名称。

?

8. 对象表示和属性绑定

在类的内部,实例是使用字典来实现的,可以用实例的__dict__属性的形式访问该字典。这个字典包含的数据对每个实例而言都是唯一的。对实例的修改始终会反映到局部__dict__属性中。同样,如果直接对__dict__进行修改,所做的修改也会反映在该属性中。
实例被特殊属性__class__链接回它们的类。在特殊属性__base__中将类链接到它们的基类。只要使用obj.name = value设置属性,就会调用特殊方法obj.__setattr__("name", value)。如果使用del obj.name删除了一个属性,就会调用特殊方法obj.__delattr__("name")。
来查找属性时,将调用特殊方法obj.__getattrribute__("name")。如果搜索过程失败,最终会尝试调用类的__getattr__()方法(如果已定义)来查找该属性。如果这也失败,就会抛出AttributeError异常。

?

9. 运算符重载

用户可以定义Python的所有内置运算符,比如,如果希望向Python添加一种新的数字类型,可以定义一个类并在该类中定义__add__(),例如:

class complex(object):
    def __init__(self, real, imag=0):
        self.real = float(real)
        self.imag = float(imag)
    def __repr__(self):
        return "Complex(%s, %s)" % (self.real, self.imag)
    def __str__(self):
        return "(%g+%gj)" % (self.real, self.imag)
    def __add__(self, other):
        return Complex(self.real + other.real, self.imag + other.imag)
    def __sub__(self, other):
        return Complex(self.real - other.real, self.imag - other.imag)

在这个例子中,__repr__()方法创建一个字符串,可以计算该字符串来重新创建对象。__str__()方法创建具有良好输出格式的字符串。__add__()和__sub__()实现数学运算。
?

10. 抽象基类

要定义抽象基类,需要使用abc模块。该模块定义一个元类(ABCMeta)和一组装饰器,可以按如下方式使用:

from abc import ABCMeta, abstractmethod, abstractproperty
class Foo:
    __metaclass__ = ABCMeta
    @abstractmethod
    def spam(self, a, b):
        pass
    @abstractproperty
    def name(self):
        pass

要定义抽象类,需要将其元类如上所示设置为ABCMeta。因为抽象类的实现离不开元类。在抽象类中,@abstractmethod和@abstractproperty装饰器指定Foo的子类必须实现一个方法或特性。抽象类不能直接实例化。这一限制也适用于派生类,如果派生类没有实现一个或多个抽象方法,那么尝试创建派生类实例将会失败。
抽象基类支持对已经存在的类进行注册,使其属于该基类。这是用register()方法完成的,例如:

class Grok(object):
    def spam(self, a, b):
        print("Grok.spam")

Foo.register(Grok)

?

10. 元类

在Python中定义类时,类定义本身将成为一个对象。例如:

class Foo(object): pass
isinstance(Foo, object) # True

类对象的这种创建方式是由一种名为元类的特殊对象控制的。即元类就是知道如何创建和管理类的对象。如果查看Foo的类型,将会发现它的类型为type。
使用class语句定义新类时,类主体将作为其自己的私有字典内的一系列语句来执行。语句的执行与正常代码执行过程相同,只是会在私有成员上发生名称变形。最后,类的名称、基类列表和字典将传递给元类的解构函数,以创建相应的类对象。类创建的最后一步,也就是调用元类type()的步骤,可以自定义。
类可以显式地指定其元类,这通过在基类元组中提供metaclass关键字参数来实现,例如:

class Foo(metaclass=type)
    __metaclass__ = type
    ...

如果没有显示指定元类, class语句将检查基类元组中的第一个条目。在这种情况下,元类与第一个基类的类型相同。如果没有指定基类,class语句将检查全局变量__metaclass__是否存在。如果找到了该变量,将使用它来创建类。如果没有找到任何__metaclass__值,Python将使用默认的元类(type())。
?

10. 类装饰器

类装饰器是一种函数,它接受类作为输入并返回类作为输出,例如:

registry = { }
def register(cls):
    registry[cls.__clsid__] = cls
    return cls

要使用该函数,可以在类定义前将它用作装饰器,例如:

class Foo(object):
    __clsid__ = "123-456"
    def bar(self):
        pass

等同的方式如下:

class Foo(object):
    __clsid__ = "123-456"
    def bar(self):
        pass
register(Foo)

以上是关于[Python]类与面向对象编程的主要内容,如果未能解决你的问题,请参考以下文章

python之基础篇——类与面向对象

第10天:Python 类与对象

[Python]类与面向对象编程

Python入门自学进阶——5--类与对象

课时37:类与对象:面向对象编程

Java类与对象