python 面向对象编程

本文详细介绍了Python的面向对象编程,包括类与对象的概念、类的方法和字段、访问限制(私有变量和方法)、继承与多态。还讨论了如何获取对象信息、动态绑定属性和方法、使用@property装饰器进行属性管理、多重继承、定制类以及枚举类和元类的使用。文章深入浅出地展示了Python面向对象编程的各个方面。

类与对象

class Student(object):
    pass

stu = Student
s = Student()

print (stu)
print (Student)
print (s)

结果是:

<class '__main__.Student'>
<class '__main__.Student'>
<__main__.Student object at 0x000002E28CE33E10>

## 0x000002E28CE33E10是内存地址

类的方法和字段

类的方法

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

类的属性

  • 定义在类内部方法外部(类属性)
  • 定义在方法内部(对象属性)
    • 定义在 _ self _ 方法中
    • 定义在其他方法中
    • 对实例变量绑定任何数据
class Student(object):

    sex = "man"
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        self.grade = 1
        print('%s: %s' % (self.name, self.score))


print (Student.sex)

s = Student("zhangsan",95)
print (s.name)
print (s.sex)
#print (s.grade) -- 这个时候访问grade会出错

s.print_score()
print (s.grade)

s.count = "China"
print (s.count)

类的访问限制

私有变量

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以 __开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问

私有方法

方法前加两个下划线__

class Student(object):
    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def __print_score(self):
        print('%s: %s' % (self.__name, self.__score))

s = Student("zhangsan",95)
s.__print_score()


# Traceback (most recent call last):
#   File 
# "C:/Users/disheng.zeng/Develop/Python/Scrapy/purePython1/main.py", 
# line 27, in <module>
#     s.__print_score()
# AttributeError: 'Student' object has no attribute '__print_score'

私有变量和特殊变量

在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name____score__这样的变量名。

有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

>>> bart._Student__name
'Bart Simpson'

但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。

总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。

继承和多态

  • 继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

  • 动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。

class Animal(object):
    def run(self):
        print ("animal running...")

class Dog(Animal):
    def run(self):
        print ("dog running...")

class Car(object):
    def run(self):
        print ("car running...")

def showrun(animal):
    animal.run()

showrun(Animal())
showrun(Dog())
showrun(Car())

静态语言 vs 动态语言

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:

  • 本质上来讲是因为python在方法参数中不指定变量类型

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

获取对象信息

使用type()

判断变量类型

这里写图片描述

判断变量是否是函数

这里写图片描述

使用isinstance()

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。

  • 判断类类型
  • 判断继承关系
  • 判断普通类型
  • 判断一个变量是否是某些类型中的一种

这里写图片描述

使用dir()

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

这里写图片描述

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

>>> len('ABC')
3
>>> 'ABC'.__len__()
3

我们自己写的类,如果也想用len(myObj)的话,就自己写一个len()方法:

>>> class MyDog(object):
...     def __len__(self):
...         return 100
...
>>> dog = MyDog()
>>> len(dog)
100

剩下的都是普通属性或方法,比如lower()返回小写的字符串:

>>> 'ABC'.lower()
'abc'

反射

仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:

>>> class MyObject(object):
...     def __init__(self):
...         self.x = 9
...     def power(self):
...         return self.x * self.x
...
>>> obj = MyObject()

紧接着,可以测试该对象的属性:

>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19

如果试图获取不存在的属性,会抛出AttributeError的错误:

>>> getattr(obj, 'z') # 获取属性'z'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
可以传入一个default参数,如果属性不存在,就返回默认值:

>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404

也可以获得对象的方法:

>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:

sum = obj.x + obj.y

就不要写:

sum = getattr(obj, 'x') + getattr(obj, 'y')

一个正确的用法的例子如下:

def readImage(fp):
    if hasattr(fp, 'read'):
        return readData(fp)
    return None

假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。

请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能。

给类或者实例动态绑定属性或者方法

绑定属性

class Student(object):
    pass

>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael

绑定方法

  • 如果是类绑定方法,则所有对象共享
  • 如果是对象绑定方法,则不能共享

特别注意:

绑定的方法第一个参数一定要是self

>>> def set_age(self, age): # 定义一个函数作为实例方法
...     self.age = age
...
>>> from types import MethodType
>>> s = Student()
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
### Student.set_age = MethodType(set_age, Student) # 给类绑定一个方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25
  • 给一个实例绑定的方法,对另一个实例是不起作用的:
  • 为了给所有实例都绑定方法,可以给class绑定方法:

__slots__

但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。

为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的slots变量,来限制该class实例能添加的属性:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

然后,我们试试:

>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

问题:

  • __slot__ 的继承问题

使用@property装饰器

用法

  • 加上@property,把一个getter方法变成属性
  • @score.setter,负责把一个setter方法变成属性赋值
class Actress():
  def __init__(self):
    self._name = 'TianXin'
    self._age = 20

  @property
  def age(self):
    return self._age

  @age.setter
  def age(self, age):
    if age > 30:
      raise ValueError
    self._age = age

Actress中有一个属性age,它有gettersetter两个方法,而具体gettersetter之中的实现,外部调用是不管的,也就是说外部不知道getter到的age是来自_age,还是来自_name

实例:快速进行代码重构

参考 http://www.jb51.net/article/86970.htm

可以这类内部进行属性值的getter和setter具体操作的变动,而不需要修改外部调用方

多重继承

class Bat(Mammal, Flyable):
    pass

通过多重继承,一个子类就可以同时获得多个父类的所有功能。

继承:主线与MixIn

  • 由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。

  • 只允许单一继承的语言(如Java)不能使用MixIn的设计。

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

Python自带的很多库也使用了MixIn。举个例子,Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。

比如,编写一个多进程模式的TCP服务,定义如下:

class MyTCPServer(TCPServer, ForkingMixIn):
    pass

编写一个多线程模式的UDP服务,定义如下:

class MyUDPServer(UDPServer, ThreadingMixIn):
    pass

如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn:

class MyTCPServer(TCPServer, CoroutineMixIn):
    pass

这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。

定制类

官方定制文档

__str__()__repr__()

打印类

这里写图片描述

方法区别使用
__str__()返回用户看到的字符串对应的print()方法
__repr__()返回程序开发者看到的字符串为调试服务的

这里写图片描述
通常__str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

__iter__()__next__()

迭代器

如果一个类想被用于for ... in循环,类似listtuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

class Fib(object):
    def __init__(self):
        self.a = 0
        self.b = 1        # 初始化两个计算器

    def __iter__(self):
        return self       # 实例本身就是迭代对象,故返回自己

    def __next__(self):
        self.a , self.b = self.b ,self.a + self.b #计算下一个值
        if self.a > 1000:                         #退出循环的条件
            raise StopIteration()
        return self.a                            #返回下一个值


for n in Fib():
    print (n)

# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34
# 55
# 89
# 144
# 233
# 377
# 610
# 987

__getitem__()

下标访问

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a

切片访问

__getattr__()

动态化处理处理类的所有属性和方法

只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。

class Student(object):
    def __getattr__(self,attr):
        if attr == 'score':      # 对字段的处理
            return 99
        if attr == 'age':        # 对方法的处理
            return lambda:25

s = Student()
print (s.score)
print (s.age())
print (s.name)
print (s.grade())


# 99
# 25
# None

# Traceback (most recent call last):
#   File "C:/Users/disheng.zeng/Develop/Python/Scrapy/purePython1/main.py", line 20, in <module>
#     print (s.grade())
# TypeError: 'NoneType' object is not callable

注意到任意调用如s.name都会返回None,这是因为我们定义的__getattr__默认返回就是None

__call__()

class Student(object):
    def __init__(self,name):
        self.name = name

s = Student("Joy")
s()

# 报错
# TypeError: 'Student' object is not callable

任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。

class Student(object):
    def __init__(self,name):
        self.name = name

    def __call__(self):
        print ("My name is %s" % self.name)

s = Student("Joy")
s()

# My name is Joy

__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。

那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

使用枚举类

使用Enum

from enum import Enum

Mon = Enum("Month",("Jan","Feb","Mar","Apr"))

print (Mon.Jan)
print (Mon.Jan.value)
for name ,member in Mon.__members__.items():
    print (name,"=>",member,",",member.value)

# Month.Jan
# 1

# Jan => Month.Jan , 1
# Feb => Month.Feb , 2
# Mar => Month.Mar , 3
# Apr => Month.Apr , 4

自定义枚举类

@unique装饰器可以帮助我们检查保证没有重复值。

@unique
class Month(Enum):
    Jan = 1
    Feb = 2
    Jun = 6
    Aug = 8


print (Month.Jan)
print (Month.Jan.value)
print (Month['Jan'])
print (Month(6))
print (Month(6).value)

# Month.Jan
# 1
# Month.Jan
# Month.Jun
# 6

可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。

使用元类

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

比方说我们要定义一个Studentclass,就写一个student.py模块:

class Student(object):
    def name(self,name="Mirceal"):
        print ("Hello, %s" % name)

s = Student()
s.name()

print (type(Student))
print (type(s))


# Hello, Mirceal
# <class 'type'>
# <class '__main__.Student'>

当Python解释器载入student模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Student的class对象

type()函数可以查看一个类型或变量的类型,Student是一个class,它的类型就是type,而s是一个实例,它的类型就是class Student

我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Student类,而无需通过class Student(object)...的定义:

def fn(self, name="Mirceal"):
    print("Hello, %s" % name)

Student = type("Student",(object,),dict(name = fn))

s = Student()
s.name()

# Hello, Mirceal
# <class 'type'>
# <class '__main__.Student'>

要创建一个class对象,type()函数依次传入3个参数:

  • class的名称;
  • 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  • class的方法名称与函数绑定,这里我们把函数fn绑定到方法名Student上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

metaclass

参考:
https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431866385235335917b66049448ab14a499afd5b24db000

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值