自定义类

自定义类型可以帮助我们更好地实现模块化编程
作者

HPDell

1 面向对象的程序设计

Object-oriented Programming

1.1 为什么需要类和对象

很多时候我们要描述的东西是比较复杂的。 比如,要描述一个小区,我们可能需要以下信息:

{
    'name': '小区名称',
    'location': (112.234345, 30.103932),
    'developer': '开发商',
    'avg_price': 12000,
    'buildings': ['#1', '#2', '#3'],
    'is_selling': True,
    'properties': ['1-101', '1-102', '1-201']
}

而且可能还有一些以小区为单位的操作,比如:总户数 count_properties() 。 这些函数是和“小区”这个类型的变量强绑定的,不能应用于其他类型上。 此时使用内置类型就无法满足我们的需要。

1.2 类和对象

对象

Object,一般翻译为“对象”,也被称为“实例”、“实体”。 具有一定关联的变量和函数的集合,用于整体化地描述某个物体、概念或资源, 从而将零散的代码整合起来。

类描述一个对象拥有哪些变量和函数。 必须拥有一个 __init__() 函数来初始化变量。

class Community:
    def __init__(self):
        self.title = "小区名"
comm = Community()
print(comm.title)

1.3 四个概念

  1. 抽象:将某种物体或概念的特点抽象为一组变量。
  2. 封装:将该种物体或概念的能力包装为一组函数。
  3. 继承:一个类(基类)进行扩展和更改,得到其他类(子类)。
  4. 多态:子类种来自基类的函数可以有和基类不同的行为。

Python 中能用到的所有的变量,都是经过抽象和封装的类对象。

1.4 案例:猫、家猫、虎

    1. 花纹、皮毛、尾巴、年龄
    2. 叫、进食
  1. 家猫
    1. 主人、疫苗、宠物证
    2. 喵喵叫、缠人、蹲坐
    1. 野生或饲养、活动区、标识
    2. 咆哮、捕猎

1.5 优缺点

  1. 抽象和封装可以将代码按照实体组合起来,并赋予其实际含义,可以更好地组织和理解代码。
  2. 继承有助于少写代码,减少出错的可能;多态又保持了一定的灵活性。

但是:

  1. 抽象和封装要合适,不适当的封装会造成困难,过度封装则会增加维护难度
  2. 基类太多有的时候会导致代码容易出错,也容易造成冲突

2 编写类

声明类包含哪些变量和函数的过程。

2.1 构造函数

在构造函数 __init__() 中定义类包含哪些变量。

class Property:
    def __init__(self):
        self.price = 20000
        self.size = 100
        self.bedroom = 2
        self.bathroom = 1
        self.decorated = True
        self.community = Community()
重要

类的所有函数1都必须包含一个 self 参数,而且必须是第一个参数。


构造函数除了定义变量外,还有一个重要作用是传递变量的初始值。

class Appartment:
    def __init__(self, price, size, bedroom, bathroom, decorated, community):
        self.price = price
        self.size = size
        self.bedroom = bedroom
        self.bathroom = bathroom
        self.decorated = decorated
        self.community = community

appartment1 = Appartment(20000, 100, 2, 1, True, comm)
print(appartment1.price)

2.2 成员变量

  • 成员变量是只属于类对象的变量,其他地方无法获取。
  • 同一个类的不同对象的同一个变量,也是不同值的。
  • 在类对象后面使用 .变量名 的方式使用成员变量,也可以修改值。
appartment1.price = 15000
print(appartment1.price)

2.3 成员函数(方法)

成员函数的编写方法和普通函数一样,但是可以通过 self 调用成员变量,而普通函数是不可以使用 self 的。

class Appartment:
    def __init__(self, price, bedroom, bathroom):
        self.price = price
        self.bedroom = bedroom
        self.bathroom = bathroom
    
    def info(self):
        print(f"{self.bedroom}b{self.bathroom}b appartment, {self.price} per week.")

appartment1 = Appartment(400, 1, 1)
appartment1.info()
  • 在类对象后面使用 .函数名(实参) 的方式调用成员函数。
  • 调用成员函数的时候,不需要明写 self 参数。

成员函数也可以有自己的参数,使用自己的参数时无需使用 self

class RentalAppartment:
    def __init__(self, price, bedroom, bathroom):
        self.price_per_week = price
        self.bedroom = bedroom
        self.bathroom = bathroom
    
    def total_rent(self, weeks):
        return self.price_per_week * weeks

appart1 = RentalAppartment(400, 1, 1)
appart1.total_rent(52)
提示

成员函数参数也可以有默认值。

2.4 继承和派生

通过扩展一个类(基类)得到另一个类(子类、派生类)的过程。

class 派生类(基类):
    // 类定义
提示
  1. 基类可以不止有一个,但最好只有一个。
  2. 子类中包含基类的所有变量和函数。
  3. 子类中的函数可以使用 super() 表示基类。
  4. 如果子类和基类有同样的变量或函数,子类中优先使用自己的。

例如

class Animal:
    def __init__(self, name):
        self.name = name
        self.appetited = 0
        self.starve = 0

    def eat(self, volume):
        self.starve += volume / self.appetited
        self.starve = min([self.starve, 1])
        return self.starve
class Cat(Animal):
    def __init__(self, name):
        super().__init__(name)
        self.appetited = 30
    
    def sound(self):
        print("Meow!")

Cat 类型的变量就可以使用 Animal 的所有成员变量和成员函数。

tcell = Cat("Tcell")
print("Starve level: {0:.2f}%".format(tcell.starve * 100))
print("after eat: {0:.2f}%".format(tcell.eat(20) * 100))
Starve level: 0.00%
after eat: 66.67%
tcell.sound()
Meow!

3 内置类

Python 的一些内置类提供了很多方法。

3.1 字符串

"Python".lower()
'python'
"Python".upper()
'PYTHON'
"Python".swapcase()
'pYTHON'
"python".capitalize()
'Python'
"Python".startswith("Py")
True
"Python".endswith("on")
True
"Python".find("th")
2
"Python,Java,C++".split(",")
['Python', 'Java', 'C++']
"Python,Java,C++".replace(",", " | ")
'Python | Java | C++'
", ".join(["Python", "Java", "C++"])
'Python, Java, C++'
"  Python. ".strip()
'Python.'
"Result: {0:.2f}".format(1/3)
'Result: 0.33'
"Result: {res:.2f}".format(res=1/3)
'Result: 0.33'

3.2 列表

a = [1,2,3]
a.append(4)
a
[1, 2, 3, 4]
a.extend([4,5,6])
a
[1, 2, 3, 4, 4, 5, 6]
print(a.pop())
a
6
[1, 2, 3, 4, 4, 5]
a.reverse()
a
[5, 4, 4, 3, 2, 1]
a.insert(2, 9)
a
[5, 4, 9, 4, 3, 2, 1]
a.remove(4)
a
[5, 9, 4, 3, 2, 1]
print(a.pop(2))
a
4
[5, 9, 3, 2, 1]
a.clear()
a
[]

3.3 字典

d = {
    'name': '小区名称',
    'developer': '开发商'
}
d.items()
dict_items([('name', '小区名称'), ('developer', '开发商')])
d.keys()
dict_keys(['name', 'developer'])
d.values()
dict_values(['小区名称', '开发商'])
for key, value in d.items():
    print(f"{key}={value}")
name=小区名称
developer=开发商
d.get('name', '默认名称')
'小区名称'
d.get('city', '郑州')
'郑州'
d.update({'city': '郑州'})
d
{'name': '小区名称', 'developer': '开发商', 'city': '郑州'}
print(d.pop('city'))
d
郑州
{'name': '小区名称', 'developer': '开发商'}
{}.fromkeys(['bed', 'bath'], 2)
{'bed': 2, 'bath': 2}

4 异常

这是一类特殊的类型,用于表示程序遇到了异常情况。

4.1 什么情况下会遇到异常

凡是代码无法正确执行的时候,就会抛出一个异常。

  • 除数为 0 的时候,抛出 ZeroDivisionError
  • 访问的列表元素超出了列表长度,抛出 IndexError
  • 参数的类型不对,抛出 TypeError
  • 使用的变量或函数找不到,抛出 NameError
  • 找不到指定文件,抛出 FileNotFoundError
  • 网页不存在(404),抛出 HTTPError
  • 网页超时无法打开,抛出 ConnectTimeoutError

4.2 raise 语句抛出异常

在一些情况下,如果认为当前的情况是一种异常情况,可以使用 raise 手动抛出一个异常。 这表示我们不对当前情况做任何处理,而是交给更上一层的代码块处理。

raise ValueError("Some message")
raise ValueError(("Some message", 2))

异常的构造函数通常都有一个参数,用来保存一些信息,这些信息被放在了 args 成员变量中。

提示

异常虽然有很多种类型,但不同类型之间在定义上没有太大的区别。 使用不同异常类型的目的是指示这个异常的性质。 比如当除数是 0 时,使用 ZeroDivisionError 就比 ValueError 更清晰的指出了异常的本质。


在写网络爬虫的时候,正常情况下会获得一个字典,其中有一个键名(字段)是 data,对应的键值表示要获取的数据,可以从中提取出我们需要的信息。 但是有的时候可能会发生一些错误,此时这个字段就消失了。 这时需要抛出一个 ValueError 的异常,表示获得的结果不对。

def get_data(response):
    if "data" not in response:
        raise ValueError("Field 'data' not found in response")
    data = response["data"]
    return len(data)
response1 = {'status': 200, 'data': [{'name': '碧桂园'}, {'name': '万达'}]}
get_data(response1)
2
response2 = {'status': 404, 'message': 'Page not found.'}
get_data(response2)
ValueError: Field 'data' not found in response

4.3 try-except 语句捕获异常

格式

try:
    # Some code
except:
    # Deal with exception
try:
    # Some code
except SomeException:
    # Deal with exception
except AnotherException as e:
    # Deal with exception
    # using some info in e
提示
  • 如果不指定异常类型,则捕获所有类型的异常
  • 如果指定异常类型 A,也会捕获派生自 A 的类型的异常
  • 异常类型是从上至下匹配的

循环使用 get_data 函数处理下面的返回值列表,并处理的异常。 当遇到异常时,输出一句话,显示是在处理哪个 response 时发生的异常。

response_list = [response1, response2]
for i, res in enumerate(response_list):
    try:
        get_data(res)
    except ValueError as e:
        print(f"Value error raised at response {i}: {e.args[0]}")
Value error raised at response 1: Field 'data' not found in response

5 下节

模块

  • 安装模块
  • 常用模块

脚注

  1. 静态函数和类函数除外,暂时不考虑这种特殊情况。↩︎