Python语法基础-函数,类以及调试处理
1. 函数的定义
python中函数有两种:
- python自带的函数
- 用户定义函数
返回多个值
原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便
1.1函数的参数
参数 | 含义 | 输入 |
---|---|---|
位置参数 | def power(x,n) |
实际参数 |
默认参数 | def power(x,n=2) |
实际+默认参数(需要改变时) |
可变参数 | def power(*args) |
传入任意个参数,内部组装成tuple |
关键字参数 | def person(name, age, **kw) |
传入带参数名的参数,组装成dict |
命名关键字参数 | def person(name,age,*, city, job) |
限制关键字参数的名字(必须传入参数名) |
- 顺序: 必选参数<—默认参数<—可变参数<—命名关键字参数<—关键字参数
# 关键字参数
def person(name, age, **kw):
print(\'name:\', name, \'age:\', age, \'other:\', kw)
person(\'hao\', 20) # name: Michael age: 30 other: {}
person(\'hao\', 20, gener = \'M\', job = \'Engineer\') # name: Adam age: 45 other: {\'gender\': \'M\', \'job\': \'Engineer\'}
extra = {\'city\': \'Beijing\', \'job\': \'Engineer\'}
person(\'Jack\', 24, **extra)
# 命名关键字参数
def person(name, age, *, city=\'Beijing\', job):
print(name, age, city, job)
person(\'Jack\', 24, job = \'123\')
person(\'Jack\', 24, city = \'Beijing\', job = \'Engineer\')
# Combination
# 可变 + 关键字参数
def f1(a, b, c=0, *args, **kw):
print(\'a =\', a, \'b =\', b, \'c =\', c, \'args =\', args, \'kw =\', kw)
f1(1, 2, 3, \'a\', \'b\') # a = 1 b = 2 c = 3 args = (\'a\', \'b\') kw = {\'x\': 99}
f1(1, 2, 3, \'a\', \'b\', x=99) # a = 1 b = 2 c = 0 d = 99 kw = {\'ext\': None}
# 默认参数 + 命名关键字参数 + 关键字参数
def f2(a, b, c=0, *, d, **kw):
print(\'a =\', a, \'b =\', b, \'c =\', c, \'d =\', d, \'kw =\', kw)
f2(1, 2, d=99, ext=None) # a = 1 b = 2 c = 0 d = 99 kw = {\'ext\': None}
2. 面向对象编程
- 面向过程: 根据业务逻辑从上到下写代码
- 面向对象: 对数据与函数绑定到一起,进行封装,这样更快速的开发过程,减少代码重复使用
数据封装、继承和多态是面向对象的三大特点
2.1. 类(抽象概念)和对象(具体概念)
玩具模具(类)-》 火车玩具,飞机玩具..(对象)
类的组成结构
- 类名:狗
- 类的属性:一组数据(狗的颜色,性别…)
- 类的方法: 运行进行操作的方法行为(行为,会叫,会咬人…)-> 用函数设计
类的组成 | 特性 | 例子 | 例子 |
---|---|---|---|
类名 | 名称 | 狗 | 人 |
类的属性 | 一组数据 | 狗的颜色,性别 | 身高,年龄 |
类的方法 | 运行进行操作的方法行为 | 行为,会叫,会咬人 | 跑,打架 |
- 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
- 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
- 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
- 继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。
(1). 定义类
# 定义类
class Dog(object):
# 定义初始化方法
def __init__(self,weight,color):
"""
self: python会把对象的地址自动传给self,不要自己传
weight, color: 接收外部属性
"""
# 定义属性
self.weight = weight
self.color = color
# 魔法方法: 当只打印Dog对象的时候,就可以打印这里的东西
def __str__(self):
msg = "dog weight" + self.weight + "color" + self.color
return "哈哈哈"
def getweight(self):
return self.weight
def getcolor(self):
return self.color
def setweight(self):
self.weight = 100
def setcolor(self):
self.color = "green"
# 定义方法
def bark(self):
"""
self: python解释器默认把调用该class的对象地址给它
"""
print("666")
def run(self):
print("777")
# 创建对象
huskey = Dog(5, \'Black\') # 创建一个哈士奇
keji = Dog(10, \'Green\')
huskey.bark() # 哈士奇叫
huskey.run() # 哈士奇跑
huskey.weight = 100 # 哈士奇属性
huskey.color = \'green\'
-
self
表示自己,表示调用类的对象本身 - python中类似
__***__
的方法,是魔法方法,有特殊用途
(2). 类的数据封装
面向对象编程的一个重要特点就是数据封装,比如在上面例子中,Dog
类的每个实例都有weight和color,我们可以直接在Dog
类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Dog
类本身是关联起来的,我们称之为类的方法:
class Dog(object):
def __init__(self, weight, color):
self.weight = weight
self.color = color
def print_dog(self):
print(\'%s: %s\' % (self.weight, self.color))
我们从外部看Dog
类,就只需要知道,创建实例需要给出weight
和color
,而如何打印,都是在Dog
类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
(3). 访问限制
- 从前面
Dog
类的定义来看,外部代码还是可以自由地修改一个实例的weight
、color
属性,我们可以设置内部属性不被外部访问:
class Dog(object):
# 加下划线
def __init__(self, weight, color):
self.__weight = weight
self.__color = color
def print_dog(self):
print(\'%s: %s\' % (self.__weight, __self.color))
huskey = Dog(60, \'green\')
print(huskey.__name) # 无法再访问属性,会出错
- 但是如果外部代码需要获取
weight
、color
属性呢?-> 添加get_weight
、get_color
方法
class Dog(object):
...
def get_weight(self):
return self.__weight
def get_color(self):
return self.__color
- 如果又要允许外部代码修改
weight
、color
怎么办?->添加set_weight
、set_color
方法
class Dog(object):
...
def set_weight(self,weight):
self.__weight = weight
def set_color(self):
self.__color = color
-
为什么要费这么大周折呢?之前不是可以直接husky.weight=10直接改吗
因为在方法中,可以对参数做检查,避免传入无效的参数:
class Dog(object):
...
def set_weight(self,weight):
if 0<=weight<=100:
self.__weight = weight
else:
raise ValueError(\'Bad weight\')
(4). 获取对象信息
# type():判断对象类型
type([1,2,3])
type(\'abc\') == str
type(huskey)
# isinstance(): 一个对象是否属于某种类型
isinstance(h, husky)
isinstance(h, cat)
# dir() 获取一个对象的所有属性和方法
dir(\'ABC\')
dir(\'huskey\')
2.2. 继承和多态
继承就是:父类和子类之间的交互关系
(1)为什么要用继承?
最大的好处是子类获得了父类的全部功能
class Animal(object):
def run(self):
print(\'Animal is running...\')
# Dog继承父类Animal,并且自动拥有run属性,可以在Dog对象中直接调用
class Dog(Animal):
pass
# 子类可以自己增加一些方法(eat),同时也可以重写父类的方法(run)---》这就是多态
class Cat(Animal):
def run(self):
print(\'Dog is running...\')
def eat(self):
print(\'Eating meat...\')
(2)什么是多态?
在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是反过来就不行了
C是dog,C也是animal
D是animal,但你不能说D也是Dog
# 首先创建对象并查看对象类型
a = list() # a是list类型
isinstance(a, list) # true
b = Animal() # b是Animal类型
isinstance(b, Animal) # true
c = Dog() # c是Dog类型
isinstance(c, Dog) # true
# C既是Dog也是Animal
isinstance(c, Animal) # true
# 反过来就错了
isinstance(b, Dog) # false
(3)那么为题来了。这样设置成多态有什么好处呢?
-
任何依赖父类作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
-
当我们需要传入
Dog
、Cat
、Tortoise
……时,我们只需要接收Animal
类型就可以了,因为Dog、Cat、Tortoise
……都是Animal
类型,然后,按照Animal
类型进行操作即可。由于Animal
类型有run()
方法,因此,传入的任意类型,只要是Animal
类或者子类,就会自动调用实际类型的run()
方法,这就是多态的意思 -
对于一个变量,我们只需要知道它是
Animal
类型,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal、Dog、Cat
还是Tortoise
对象上,由运行时该对象的确切类型决定
def run_twice(animal):
animal.run()
run_twice(Animal()) # 调用animal的run
run_twice(Dog()) # 调用dog的run
run_twice(Cat()) # 调用cat的run
3. 面向对象高级编程
(1). __slots__
的使用
动态绑定允许我们在程序运行的过程中动态给class加上功能, 但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student
实例添加name
和age
属性。
在class
中定义一个特殊的__slots__
变量,来限制该class
实例能添加的属性:
class Student(object):
__slots__ = (\'name\', \'age\') # 用tuple定义允许绑定的属性名称
s = Student() # 创建新的实例
s.name = \'Michael\' # 绑定属性\'name\'
s.score = 99 # 绑定属性\'score\'-->出错
__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的
(2). @property
的使用
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改,解决方案:
- 通过
set_score
设置成绩,在通过get_score
获取成绩,在set_score
中检查参数(已经讲过) - 使用内置的
@property
装饰器,既可以检查参数,又可以类似属性那样访问类的变量
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
class Student(object):
# @property将get_Score方法变成属性
@property
def score(self):
return self._score
# @score.setter将set_score方法变成属性赋值
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError(\'score must be an integer!\')
if value < 0 or value > 100:
raise ValueError(\'score must between 0 ~ 100!\')
self._score = value
s = Student()
s.score = 60 # OK,实际转化为s.set_score(60)
s.score # OK,实际转化为s.get_score()
s.score = 9999 # error
(3). 多重继承MixLn的设计
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系
class Animal(object):
pass
class Bird(Animal):
pass
class Parrot(Bird):
pass
# 此时我们需要加入额外的功能fly
# 先定义好fly的类
class Flyable(object):
def fly(self):
print(\'Flying...\')
# 同时继承两个类
class Parrot(Bird, Flyable):
pass
(4). 定制个性化类
1): __str__
和 __repr__
让打印的object更漂亮:
-
__str__
: 用print打印 -
__repr__
: 直接输入对象名
class Student(object):
# 加下划线
def __init__(self, name):
self.__name = name
def __str__(self):
msg = \'Student name is\' + self.__name
return msg
__repr__ = __str__
s = Student()
print(s) # 打印出 Student name is.....
s # 效果跟上面的一样
2): __iter__
将类定义成类似list或者tuple那种,可以用于for循环的作用
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b
def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己
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
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 100000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值
for i in Fib():
print(n) # 1,1,2,3,5,.......,75025
# 因为__getitem__的作用,可以index某个值
f = Fib()
f[0] # 1
# 也可以切片
print(f[0:5]) # 1,1,2,3,5
3): __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.
- 实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象
4) 枚举类Enum
定义常量
from enum import Enum, unique
# unique保证没有重复值
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
# 访问枚举类
print(Weekday.Mon) # Weekday.Mon
print(Weekday.Mon.value) # 1
print(Weekday(1))) # # Weekday.Mon
5) type()
动态创建类Class
type()
函数既可以返回一个对象的类型,又可以创建出新的类型
type()和Class()的功能是一样的
要创建一个class对象,type()函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。
def fn(self, name=\'world\'): # 先定义函数
print("hello", name)
Hello = type(\'Hello\', (object,), dict(hello=fn)) # 创建Hello class
4. 错误处理
(4.1).try-catch->有错误后就结束了不执行以后的
try:
print(\'try...\')
r = 10 / int(\'a\')
print(\'result:\', r)
except ValueError as e:
print(\'ValueError:\', e)
except ZeroDivisionError as e:
print(\'ZeroDivisionError:\', e)
finally:
print(\'finally...\')
print(\'END\')
try...
except: division by string
finally...
END
用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块,至此,执行完毕。
(4.2).logging->有错误了打印完信息后继续执行
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar(\'0\')
except Exception as e:
logging.exception(e)
main()
print(\'END\')
# 结果
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar(\'0\')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END # 继续执行了
- 通过配置,
logging
还可以把错误记录到日志文件里,方便事后排查
(4.3).自定义抛出raise错误
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError(\'invalid value: %s\' % s)
return 10 / n
foo(\'0\')
# 结果
Traceback (most recent call last):
File "err_throw.py", line 11, in <module>
foo(\'0\')
File "err_throw.py", line 8, in foo
raise FooError(\'invalid value: %s\' % s)
__main__.FooError: invalid value: 0
5. 调试
方法 | 优点 | 缺点 |
---|---|---|
print方法 | 简单使用 | 重复多 |
assert | 简单使用 | 重复多 |
logging | 不会抛出错误,可以输入文档 | 重复多 |
pdb | python内置调试器 | 重复多 |
VS Code | 强强强 |
(5.1).print方法(略过)
(5.2).断言assert
凡是用print()
来辅助查看的地方,都可以用断言(assert)
来替代:
def foo(s):
n = int(s)
assert n != 0, \'n is zero!\'
return 10 / n
def main():
foo(\'0\')
# 结果
Traceback (most recent call last):
...
AssertionError: n is zero!
assert
的意思是,表达式n != 0
应该是True
,否则,根据程序运行的逻辑,后面的代码肯定会出错。如果断言失败,assert
语句本身就会抛出AssertionError
(5.3).logging
把print()
替换为logging
是第3种方式,和assert
比,logging
不会抛出错误,而且可以输出到文件:
import logging
logging.basicConfig(level=logging.INFO)
s = \'0\'
n = int(s)
logging.info(\'n = %d\' % n)
print(10 / n)
# 结果
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero
logging可以指定不同信息的级别
- debug
- info
- warning
- error
(5.4).pdb.set_trace()
我们只需要import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点
import pdb
s = \'0\'
n = int(s)
# 暂停并进入pdb调试环境
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)
(5.5). IDE
直接在代码中设置断掉调试
A–>B;
A–>C;
B–>D;
C–>D;