[Python] 08 - Classes --> Objects

时间:2023-03-09 10:04:31
[Python] 08 - Classes --> Objects

故事背景


一、阶级关系

1. Programs are composed of modules.
2. Modules contain statements.
3. Statements contain expressions.
4. Expressions create and process objects.

二、教学大纲

  • <Think Python>

[Python] 08 - Classes --> Objects

  • 菜鸟教程

Goto: http://www.runoob.com/python3/python3-class.html

参考资源:廖雪峰,Python面向对象编程

参考资源:廖雪峰,Python面向对象高级编程

参考资源:错误、调试和测试

  • 面向对象的三大特点

数据封装(Encapsulation )、继承(inheritance )和多态(polymorphism)。

Encapsulation


类的定义

  • 构造方法

赋值的的过程,就自动完成了类内的变量定义。

#!/usr/bin/python3

class Complex:
def __init__(self, realpart, imagpart):
self.r = realpart
self.i = imagpart
x = Complex(3.0, -4.5)
print(x.r, x.i) # 输出结果:3.0 -4.5

self 参数

类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的 第一个参数名称:self

注意:self 的名字并不是规定死的,也可以使用 this,但是最好还是按照约定使用 self。

self 记录了 “类的地址”,“类的属性” 等各种类的属性。

class Test:
def prt(self):
print(self)        # address
print(self.__class__)  # name t = Test()
t.prt()

执行结果:

<__main__.Test instance at 0x100771878>
__main__.Test

权限控制

  • “私有” 属性 & 方法

加上 “双下划线” 定义属性为私有属性。

#!/usr/bin/python3
class people:
  name = ''
  age = 0
  __weight = 0 def __init__(self, n, a, w):
  self.name = n
  self.age = a
  self.__weight= w def speak(self):
  print("weight is {}".format(self.__weight)) def __talk(self):
  print("age is {}".format(self.age)) p = people('runoob', 10, 30) #####################
# private attribute
##################### print(p.name)
# print(p.__weight) # <-- this is err case.
print(p._people__weight) #####################
# private function
##################### p.speak()
# p.__talk() # <-- this is err case.
p._people__talk()

“私有化” 原理

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

>>> bart._Student__name
'Bart Simpson'

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

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

  • 限制滥用 “对象成员”

__slot__关键字

由于'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。

注意:使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用。

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'

为何用?内存环保

Ref: 10. __slots__ Magic

动态语言意味着“浪费空间”换取“灵活性”。显然,减少不必要的灵活性可以 reduce memory of RAM。添加__slots__后带来的内存环保效果如下:

The code will reduce the burden on your RAM. Some people have seen almost 40% to 50% reduction in RAM usage by using this technique.

class MyClass(object):
__slots__ = ['name', 'identifier']
  def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
self.set_up()
# ...
  • 成员变量 Set & Get

过去的策略

分别定义set, get函数操作。

装饰器策略

将函数当做属性来使用:

(a) 只有@property表示只读。

(b) 同时有@property和@x.setter表示可读可写。

(c) 同时有@property和@x.setter和@x.deleter表示可读可写可删除。

class student(object):

    def __init__(self,id):
self.__id=id @property # 只读
def score(self):
return self._score @score.setter # 只写
def score(self, value):
# 设置前的checker
if not isinstance(value,int):
raise ValueError('score must be an integer!')
if value<0 or value>100:
raise ValueError('score must between 0 and 100')
# 开始设置
self._score=value @property # 只读
def get_id(self):
return self.__id s=student('') s.score=100 # 写
print(s.score) # 读
print(s.__dict__)
print (s.get_id) # 只读
#s.get_id=456 #只能读,不可写: AttributeError: can't set attribute

经典方式

# 相较于”经典方式“,以下是新式简便方式,新式类(继承自object类)
class A(object):

    def __init__(self):
self.__name=None def getName(self):
return self.__name def setName(self,value):
self.__name=value def delName(self):
del self.__name name=property(getName, setName, delName) a=A()
print(a.name) # 读
a.name='python' # 写
print(a.name) # 读
del a.name # 删除

类方法

  • @staticmethod 与 @classmethod

类方法的 3种定义方式

尤其是第一种,比较tricky。

class Person(object):

    # [第壹种方法] 不加任何参数直接定义,也是类方法
def Work():
print(" I am working!")

# [第贰种方法] 加装饰器方法
@classmethod
def Think(cls, b):     # 类方法Think必须要带至少1个参数,第一个参数默认为类名,后面可以引用。
cls.Eat(b)     # 在类方法Think中,调用类方法Eat类方法。
cls.Work()     # 在类方法Think中,调用Work类方法。
print(b,",I am Thinking!")

# [第叁种方法] 先定义类方法,至少1个参数,第一个默认为类名
def Eat(cls, b):
print(b+",I am eating")
Eat=classmethod(Eat)    # 通过内建函数classmethod()来创建类方法。

类的三种方法

如下展示了三种方法:普通类成员函数、静态方法、类方法。

#
# class one.
#
class Person(object): grade=6 def __init__(self):
self.name = "king"
self.age=20 # ----------------------------------------------------------------------- def sayHi(self):
print ('Hello, your name is?',self.name) def sayAge(self):
print( 'My age is %d years old.'%self.age) # ----------------------------------------------------------------------- @staticmethod # 静态方法
def sayName():
print ("<staticmethod> my name is king") # ----------------------------------------------------------------------- @classmethod # 类方法
def classMethod(cls):
print('<classMethod> class grade:',cls.grade)

该类的用法如下:

p = Person()                    # 实例化对象
print('p.grade:',p.grade) # 实例对象调用类变量 p.grade=9
# 类和对象都可以调用 @classmethod,但这里类的grade不会改变,体现了独立性
p.classMethod()
Person().classMethod()

# __init__的变量不属于类
# 但对象找不到变量,就会去找类里的
Person.age
Person().age
Person.__dict__
Person().__dict__

# 概念辨析:类对象、实例对象
Person().grade   # 类对象调用类变量
p.sayHi()   # 实例对象调用类成员函数
Person().sayAge()   # 类对象调用类成员函数 # 静态变量任意调用
m=Person()
m.sayName()   # 多个实例皆可共享此静态方法
Person().sayName()   # 类对象调用静态方法

继承区别

子类的实例继承了父类的static_method静态方法,调用该方法,还是调用的父类的方法和类属性。

子类的实例继承了父类的class_method类方法,调用该方法,调用的是子类的方法和子类的类属性。

Ref: python类的实例方法、静态方法和类方法区别及其应用场景

class Foo(object):
X = 1
Y = 14 @staticmethod
def averag(*mixes): # "父类中的静态方法"
return sum(mixes) / len(mixes) @staticmethod
def static_method(): # "父类中的静态方法"
print "父类中的静态方法"
return Foo.averag(Foo.X, Foo.Y) @classmethod
def class_method(cls): # 父类中的类方法
print "父类中的类方法"
return cls.averag(cls.X, cls.Y) class Son(Foo):
X = 3
Y = 5 @staticmethod
def averag(*mixes): # "子类中重载了父类的静态方法"
print "子类中重载了父类的静态方法"
print "666 ",mixes
return sum(mixes) / 3 p = Son()
print "result of p.averag(1,5)"
print (p.averag(1,5))
print "result of p.static_method()"
print(p.static_method())
print "result of p.class_method()"
print(p.class_method())

code mode

  • cls参数 - Factory method

构造函数的重载

Ref: class method vs static method in Python 

We generally use class method to create factory methods. Factory methods return class object ( similar to a constructor ) for different use cases.

We generally use static methods to create utility functions.

为何这里提到了”工厂方法“?因为工厂方法需要返回类,而类方法是'天生的”具备cls参数。

实例中通过两种策略返回类,等价于:不用“函数重载”,实现多个版本的”构建函数“。

Goto: 为什么 Python 不支持函数重载?而其他语言大都支持?

#############################
# cls: factory method
############################# from datetime import date
class Person: def __init__(self, name, age):
self.name = name
self.age = age @classmethod
def fromBirthYear(cls, name, birthYear):
return cls(name, date.today().year - birthYear)
    # 模拟上面的类方法,但第一个参数在使用时,省不掉
def fromBirthYear2(cls, name, birthYear):
return cls(name, date.today().year - birthYear)
    #---------------------------------------------------
def display(self):
print(self.name + "'s age is: " + str(self.age)) # (1) 常规方式返回一个实例对象
person = Person('Adam', 19)
person.display() # (2) 类方法返回一个实例对象
person1 = Person.fromBirthYear('John', 1985)
person1.display() # (3) 模拟类方法的使用,只是不美观而已
person2 = Person.fromBirthYear2(Person, 'Jeff', 1987)
person2.display()

Python实现工厂方法

Goto: [Design Patterns] 01. Creational Patterns - Abstract Factory

重要区别:"抽象工厂"的工厂是类;"工厂方法"的工厂是方法。

类的字典属性

  • 查询 “对象” de 属性&方法

>>> # var public >>> hasattr(p, 'name')
True
>>> # var private>>> hasattr(p, '__weight')
False
>>> hasattr(p, '_people__weight')
True
>>> # func public>>> hasattr(p, 'speak')
True
>>> fnSpeak = getattr(p, 'speak') # <-- func pointer.
>>> fnSpeak()
weight is 30
>>> # func private>>> hasattr(p, '__talk')
False
>>> getattr(p, '__talk', 404)  # 如果不存在,返回默认值404
404
>>> hasattr(p, '_people__talk')
True
>>> fnTalk = getattr(p, '_people__talk')  # <-- func pointer
>>> fnTalk()
age is 10

查询 “类” de 属性&方法

类貌似不能直接调用方法。因为方法需要self,而 self是实例化后成为对象,才会有的东西。

对象属性和类属性虽然不是一个东西,但类的属性会成为对象的 ”备份“,如下所示。

>>> class Student(object):
... name = 'Student' >>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student >>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student >>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
  • 设置 “对象” de 属性&方法

这里,主要关注 class 与 object 之间的趣味关系。

实例化后,两者就操作独立了。

>>> getattr(people, 'age')
0
>>> p = people('p hao', 20, 100)
>>> p2 = people('p2 hao', 30, 200)
>>> p.age
20
>>> p2.age
30
>>> setattr(people, 'age', 88)
>>> getattr(people, 'age')
88
>>> p.age
20
>>> p2.age
30
>>> delattr(people, 'age')
>>> hasattr(people, 'age')
False
>>> getattr(people, 'age')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'people' has no attribute 'age'

>>> p.age
20
>>> p2.age
30
  • “字典属性” 原理

如果我们在Class没有定义这3个方法,那么系统会用Python自带的内部函数;

如果我们在类里面自定义了这3个方法,那么python会先调用我们自己定义的这3个函数。

self.__dict__

Ref: 到底干了什么?

这里要小心“无限循环”!需要通过最为本质的操作方法:__dict__操作。

class Cat:

    class_level = '贵族'

    def __init__(self,name,type,speed,age):
self.name = name
self.type = type
self.speed = speed
self.age = age def run(self):
print('%s岁的%s%s正在以%s的速度奔跑' % (self.age, self.type, self.name, self.speed))

def __getattr__(self, item):
print('你找的属性不存在') def __setattr__(self, key, value):
print('你在设置属性')
# self.key=value   # !!! 这种方法不行,会产生无限递归了,因为他本身self.key=value也会触发__setattr__
self.__dict__[key] = value def __delattr__(self, item):
print('你在删除属性')
# del self.item   # !!! 无限递归了,和上面的__setattr__原理一样
self.__dict__.pop(item) xiaohua = Cat('小花','波斯猫','10m/s',10)

可见,obj 中没有给出函数相关信息。

>>> people.__dict__
mappingproxy({'__module__': '__main__', 'name': '', 'age': 0, '_people__weight': 0, '__init__': <function people.__init__ at 0x7f1659e8b488>, 'speak': <function people.speak at 0x7f1659e8b510>, '_people__talk': <function people.__talk at 0x7f1659e8b598>, '__dict__': <attribute '__dict__' of 'people' objects>, '__weakref__': <attribute '__weakref__' of 'people' objects>, '__doc__': None}) >>> p3 = people('p hao', 20, 100)
>>> p3.__dict__
{'name': 'p hao', 'age': 20, '_people__weight': 100}

常见字典属性

(1) 简单的若干属性

类的专有方法:

__init__ : 构造函数,在生成对象时调用
__del__ : 析构函数,释放对象时使用
__repr__ : 打印,转换
__setitem__ : 按照索引赋值
__getitem__ : 按照索引获取值
__len__ : 获得长度
__cmp__ : 比较运算
__call__: 函数调用
__add__ : 加运算
__sub__ : 减运算
__mul__ : 乘运算
__div__ : 除运算
__mod__ : 求余运算
__pow__ : 乘方

其他generic的方法:

'__module__': '__main__',  # 当导入到其他的脚本文件的时候,此时__name__的名字其实是导入模块的名字,不是’__main__’, main代码里面的就不执行了。
'__dict__': <attribute '__dict__' of 'people' objects>,
'__weakref__': <attribute '__weakref__' of 'people' objects>,
'__doc__': None})

(2) __str__, __repr__ 的区别

Ref: 廖雪峰 - 定制类

__repr__() 方法是类的实例化对象用来做“自我介绍”的方法,默认情况下,它会返回当前对象的“类名+object at+内存地址”,而如果对该方法进行重写,可以为其制作自定义的自我描述信息。

(a) 两者的笼统区别在于:__str__,用户友好;__repr__,程序员友好。

print('hello'.__str__())
hello print('hello'.__repr__())
'hello' from datetime import datetime as dt print(dt.today().__str__())
2018-11-30 14:26:38.881492 # 返回的是处理后的str数据 print(dt.today().__repr__())
datetime.datetime(2018, 11, 30, 14, 26, 48, 580276) # 返回的是原始函数调用的数据方法

(b) 为什么要用?打印类的效果更为美观。

class Chain(object):

    def __init__(self, path=''):
self._path = path def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path)) def __str__(self):
return self._path __repr__ = __str__ c = Chain('hello world')
print(c) # call __str__
hello world c # call __repr__
hello world

(c) 进一步配合使用实现:链式调用。

# (1) try to find this attribute --> __getattr__
# (2) go to the next Chain by 'return', and print attribute name --> __repr__
Chain().status.user.timeline.list

(3) __call__ 将类的用法 "函数化"

函数后的 ”小括号“ 触发了__call__。

class Student(object):
def __init__(self, name):
self.name = name def __call__(self):
print('My name is %s.' % self.name)

>>> s = Student('Michael')
>>> s()
My name is Michael.

那么,怎么判断一个变量是对象还是函数呢?通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
  • 运算符重载

(a) 类的加法

关键字:__add__

#!/usr/bin/python3

class Vector:
def __init__(self, a, b):
self.a = a
self.b = b def __str__(self):
return 'Vector (%d, %d)' % (self.a, self.b) def __add__(self, other):
return Vector(self.a + other.a, self.b + other.b) v1 = Vector(2,10)
v2 = Vector(5,-2)
print (v1 + v2)

执行结果:

Vector(7,8)

 

(b) Iterable类

定义一个可遍历的类。或者具备generator特性的类,如下。

class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己 def __next__(self): # (1) 计算下一个值
self.a, self.b = self.b, self.a + self.b
# (2) 注意 "循环退出条件"
if self.a > 100000:
raise StopIteration()
return self.a # 返回下一个值 # Fib()返回的是自己本身;迭代的则是__next__提供的结果。
>>> for n in Fib():  
... print(n)
...
1
1
2
3
5
...
46368
75025

(c) 数组模式

本质上是通过重新计算得出结果。

class Fib(object):
def __getitem__(self, idx):
a, b = 1, 1
for x in range(idx):
a, b = b, a + b
return a >>> f = Fib()
>>> f[]
1
>>> f[]
1
>>> f[]
2
>>> f[]
3
>>> f[]
89
>>> f[]
573147844013817084101

(d) 切片模式

class Fib(object):
def __getitem__(self, n): if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L >>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。

最后,还有一个__delitem__()方法,用于删除某个元素。

Inheritance


多继承

  • 构造函数

可以通过直接 ”调用父类构造函数" 来实现。

parent.__init__(self, ...)

还有下面这个方式,至少不用再涉及到"父类”的名字,貌似好一些。

super(子类,self).__init__(参数1,参数2,....)
  • 多继承歧义

(1) 菱形:近三层的路径二义性

钻石继承时,c++中采用虚函数解决;python 怎么办?使用 super(子类, self) 方式。

如下中,Father.__init__是被(后者)Son_2触发的。

class Father(object):
def __init__(self, name, *args, **kwargs):
self.name = name
print("我是父类__init__")

class Son_1(Father):
def __init__(self, name, age, *args, **kwargs):
print("我是Son_1的__init__")
super(Son_1, self).__init__(name, *args, **kwargs)
self.age = age class Son_2(Father):
def __init__(self, name, gender, *args, **kwargs):
print("我是Son_2的__init__")
self.gender = gender
super(Son_2, self).__init__(name, *args, **kwargs)

class GrandSon(Son_1, Son_2):
def __init__(self, name, age, gender):
print("我是GrandSon的__init__")
super(GrandSon, self).__init__(name, age, gender) def say_hello(self):
print(self.name, self.age, self.gender)

grand_son = GrandSon("老王", 24, "男")

(2) 倒三角:近两层的调用歧义

若是两个父类中有相同的方法名,而在子类使用时未指定,python 从左至右 搜索 即方法在子类中未找到时,从左到右查找父类中是否包含方法。

class Father(object):
def __init__(self, name, *args, **kwargs):
self.name = name
print("我是父类__init__") def say_hello(self):
print("i'm father.") class Son_1(Father):
def __init__(self, name, age, *args, **kwargs):
print("我是Son_1的__init__")
super(Son_1, self).__init__(name, *args, **kwargs)
self.age = 100 def say_hello(self):
print("i'm son_1.") def fn_son_1(self):
print("only son 1 can do it, age = {}".format(self.age) ) class Son_2():
def __init__(self, name, gender, *args, **kwargs):
print("我是Son_2的__init__")
self.gender = gender
# super(Son_2, self).__init__(name, *args, **kwargs) def say_hello(self):
print("i'm son_2.") class GrandSon(Son_2, Son_1):
def __init__(self, name, age, gender):
print("我是GrandSon的__init__")
# super(GrandSon, self).__init__(name, age, gender)
Son_2.__init__(self, name, age, gender)
Son_1.__init__(self, name, age, gender) grand_son = GrandSon("老王", 24, "男") grand_son.fn_son_1()

Notice: GrandSon在这里是不宜使用super的,这种“自动化模式”会导致“只调用Son_2的__init__。

所以,这里还是使用了传统的 父类.__init__,这样,grand_son.fn_son_1()就可以正常执行了。

Polymorphism


家长代表权

  • "父类" 作为参数

该参数也可以兼容处理其子类。

class Father(object):
def __init__(self, name, *args, **kwargs):
self.name = name
print("我是父类__init__") def say_hello(self):
print("i'm father.") class Son_1(Father):
def __init__(self, name, age, *args, **kwargs):
print("我是Son_1的__init__")
super(Son_1, self).__init__(name, *args, **kwargs)
self.age = 100 def say_hello(self):
print("i'm son_1.") def fn_son_1(self):
print("only son 1 can do it, age = {}".format(self.age) ) class Son_2(Father):
def __init__(self, name, gender, *args, **kwargs):
print("我是Son_2的__init__")
self.gender = gender
super(Son_2, self).__init__(name, *args, **kwargs) def say_hello(self):
print("i'm son_2.") son_1 = Son_1("son 1", 24)
son_2 = Son_2("son 2", 'male')

设计一个函数,一并兼容处理son_1, son_2,怎么办?

>>> def say_something(father):
... father.say_hello()

>>> say_something(son_1)
i'm son_1.
>>> say_something(son_2)
i'm son_2.
  • "类父类" 作为参数

动态语言支持“鸭子类型”,具备“必要的”方法就能凑活的用了。

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

class Timer(object):
def run(self):
print('Start...')

End.