在Python中,通过class
关键字定义一个类,比如我们需要定义一个人的类。按照 Python 的编程习惯,类名以大写字母开头。因此可以这样定义:
class Person: pass
注意,在这个Person
类的定义里面,并没有继承任何类,除了这样定义以外,还可以有以下两种定义方式。
class Person(): pass
class Person(object): pass
这三种情况有什么区别呢?在Python3中,是没有区别的,但是在Python2中,则有一定的区别。
在Python2中,对于第一种定义的方法,Person
类只有有限的几个内建函数'__doc__', '__module__', 'name'
,而对于第二种、第三种定义的方法,则会继承Python object对象的更多的内建函数,可以更便捷的操作对象。这是Python2版本的差异。在Python3中,我们只需要知道这三种方式都可以定义一个类即可。
定义了类之后,就可以对类进行实例化了,实例化是指,把抽象的类,赋予实物的过程。比如,定义好Person这个类后,就可以实例化多个Person出来了。 创建实例使用类名+()
,类似函数调用的形式创建:
class Person(object): pass
xiaohong = Person()
xiaoming = Person()
请练习定义一个动物类,并创建出两个实例dog, cat,打印实例,再比较两个实例是否相等。
要打印实例,直接使用print语句;
要比较两个实例是否相等,用==操作符。
参考答案class Animal(object):
pass
dog = Animal()
cat = Animal()
print(dog) print(cat) print(dog == cat)
虽然前面我们已经通过Person类创建出xiaoming、xiaohong等实例,但是这些实例看上去并没有任何区别。在现实世界中,一个人拥有名字、性别、年龄等等的信息,在Python中,可以通过以下的方式赋予实例这些属性,并且把这些属性打印出来。
xiaohong.name = 'xiaohong'
xiaohong.sex = 'girl'
xiaohong.age = 13
print(xiaohong.name)
print(xiaohong.sex)
print(xiaohong.age)
除此以外,这些属性也可以和普通变量一样进行运算。比如xiaohong长大了一岁:
xiaohong.age = xiaohong.age + 1
参考答案请定义一个动物类,并创建出两个实例dog, cat,分别赋予不同的名字和年龄并打印出来。
class Animal(object):
pass
dog = Animal()
cat = Animal()
dog.name = 'wangwang'
dog.age = 1
cat.name = 'mimi'
cat.age = 3
通过前面的方式定义一个实例的属性非常方便,但也有一些问题。 首先,如果定义属性的过程中使用了不同的属性名字,比如性别,前者使用了sex
,后者使用了gender
,那对于一个类的不同实例,存储一个信息就用了两个不同的属性,在后面将会难以维护。 其次,名字、性别、年龄等等,都是人的基本信息,在抽象一个类的时候,理应包含这些信息。 在定义 Person 类时,可以为Person类添加一个特殊的__init__()
方法,当创建实例时,__init__()
方法被自动调用,我们就能在此为每个实例都统一加上以下属性:
class Person(object):
def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age
需要注意的是,__init__()
方法的第一个参数必须是 self
(也可以用别的名字,但建议使用习惯用法),后续参数则可以自由指定,和定义函数没有任何区别。 定义类后,就可以相应的实例化对象了,需要注意的是,在实例化的时候,需要提供除self
以外的所有参数。
xiaoming = Person('Xiao Ming', 'boy', 13)
xiaohong = Person('Xiao Hong', 'girl', 14)
而访问这些属性的方式和之前的一样:
print(xiaohong.name)
print(xiaohong.sex)
print(xiaohong.age)
# 但当访问不存在的属性时,依然会报错
print(xiaohong.birth)
要特别注意的是,初学者定义__init__()
方法常常忘记了 self 参数,比如如下的定义:
class Person(object):
def __init__(name, sex, age):
pass
这种情况下,如果还是如下实例化,将会报错。
xiaoming = Person('Xiao Ming', 'boy', 13)
xiaohong = Person('Xiao Hong', 'girl', 14)
Traceback (most recent call last):
File "" , line 1, in <module>
TypeError: __init__() takes 3 positional arguments but 4 were given
参考答案请定义一个动物类,抽象出名字、年龄两个属性,并实例化两个实例
dog
,cat
。
class Animal(object):
def __init__(self, name, age):
self.name = name
self.age = age
dog = Animal('wangwang', 1)
cat = Animal('mimi', 3)
print(dog.name)
print(dog.age)
print(cat.name)
print(cat.age)
类和实例对象是有区别的,类是抽象,是模板,而实例则是根据类创建的对象,比如类:动物,只是一个抽象,并没有动物的详细信息,而猫、狗等,则是具体的动物,是类的对象。
在前面,实例对象绑定的属性只属于这个实例,绑定在一个实例上的属性不会影响其它实例;同样的,类也可以绑定属性,但是类的属性不属于任何一个对象,而是属于这个类。如果在类上绑定一个属性,则所有实例都可以访问类的属性,并且,所有实例访问的类属性都是同一个!也就是说,实例属性每个实例各自拥有,互相独立,而类属性有且只有一份。
定义类属性可以直接在 class 中定义,比如在前面的Animal类中,加入地域的类属性:
class Animal(object):
localtion = 'Asia'
def __init__(self, name, age):
self.name = name
self.age = age
在上面的代码中,localtion就是属于Animal这个类的类属性,此后,通过Animal()实例化的所有对象,都可以访问到localtion,并且得到唯一的结果。
dog = Animal('wangwang', 1)
cat = Animal('mimi', 3)
print(dog.localtion) # ==> Asia
print(cat.localtion) # ==> Asia
# 类属性,也可以通过类名直接访问
print(Animal.localtion) # ==> Asia
类属性也是可以动态添加和修改的,需要注意的是,因为类属性只有一份,所以改变了,所有实例可以访问到的类属性都会变更:
Animal.localtion = 'Africa'
print(cat.localtion) # ==>Africa
print(dog.localtion) # ==>Africa
参考答案请给 Animal类添加一个类属性 count,每创建一个实例,count 属性就加 1,这样就可以统计出一共创建了多少个 Animal的实例。
class Animal(object):
count = 0
def __init__(self, name, age):
self.name = name
self.age = age
Animal.count += 1
dog = Animal('wangwang', 1)
print(Animal.count)
cat = Animal('mimi', 3)
print(Animal.count)
pig = Animal('panpan', 1)
print(Animal.count)
可以看到,属性可以分为类属性和实例属性,那么问题就来了,如果类属性和实例属性名字相同时,会怎么样,这就涉及Python中类属性和实例属性的优先级的问题了。
我们可以做一个实验,在前面类定义的基础上,在实例属性中,也初始化一个localtion的属性。
class Animal(object):
localtion = 'Asia'
def __init__(self, name, age, localtion):
self.name = name
self.age = age
self.localtion = localtion
接着我们初始化两个实例,并把localtion打印出来。
dog = Animal('wangwang', 1, 'GuangDong')
cat = Animal('mimi', 3, 'ChongQing')
print(dog.localtion) # ==> GuangDong
print(cat.localtion) # ==> ChongQing
print(Animal.localtion) # ==> Asia
可见,在类属性和实例属性同时存在的情况下,实例属性的优先级是要高于类属性的,在操作实例的时候,优先是操作实例的属性。
另外,当实例没有和类同名的时候,通过实例对象,依然可以访问到类属性。
class Animal(object):
localtion = 'Asia'
def __init__(self, name, age):
self.name = name
self.age = age
cat = Animal('mimi', 3)
print(cat.localtion) # ==> Asia
那通过实例,可不可以修改类属性呢?我们来尝试一下:
cat.localtion = 'Africa'
print(Animal.localtion) # ==> Asia
这里依然打印了Asia,可见通过实例是无法修改类的属性的,事实上,通过实例方法修改类属性,只是给实例绑定了一个对应的实例属性:
# 新增的实例属性
print(cat.localtion) # ==> Africa
因此,需要特别注意,尽量不要通过实例来修改类属性,否则很容易引发意想不到的错误。
参考答案请把上节的 Animal类属性 count 改为 __count,再试试能否从实例和类访问该属性。
class Animal(object):
__count = 0
def __init__(self, name):
Animal.__count = Animal.__count + 1
self.name = name
print(Animal.__count)
p1 = Animal('Cat')
p2 = Animal('Dog')
print(Animal.__count)
并不是所有的属性都可以被外部访问的,这种不能被外部访问的属性称为私有属性。私有属性是以双下划线'__'
开头的属性。
# 类私有属性
class Animal(object):
__localtion = 'Asia'
print(Animal.__localtion)
Traceback (most recent call last):
File "" , line 1, in <module>
AttributeError: type object 'Animal' has no attribute '__localtion'
# 实例私有属性
class Animal(object):
def __init__(self, name, age, localtion):
self.name = name
self.age = age
self.__localtion = localtion
dog = Animal('wangwang', 1, 'GuangDong')
print(dog.name) # ==> wangwang
print(dog.age) # ==> 1
print(dog.__localtion)
Traceback (most recent call last):
File "" , line 1, in <module>
AttributeError: 'Animal' object has no attribute '__localtion'
在外部访问私有属性将会抛出异常,提示没有这个属性。 虽然私有属性无法从外部访问,但是,从类的内部是可以访问的。私有属性是为了保护类或实例属性不被外部污染而设计的。
参考答案请给Animal类的__init__方法中添加name和age参数,并把age绑定到__age属性上,看看外部是否能访问到。
class Animal(object):
def __init__(self, name, age):
self.name = name
self.__age = age
cat = Animal('Kitty', '3')
print(cat.name)
print(cat.__age)
上面提到,私有属性没有办法从外部访问,只能在类的内部操作;那如果外部需要操作私有属性怎么办?这个时候可以通过定义类或者实例的方法来操作私有属性,本节课先来介绍实例方法。
实例的方法指的就是在类中定义的函数,实例方法的第一个参数永远都是self,self是一个引用,指向调用该方法的实例对象本身,除此以外,其他参数和普通函数是完全一样的。
class Person(object):
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
在上面的定义,name是实例的私有属性,从外部是无法访问的,而get_name(self) 就是一个实例方法,在实例方法里面是可以操作私有属性的,注意,它的第一个参数是self。
另外,__init__(self, name)
其实也可看做是一个特殊的实例方法。
通过定义get_name(self)方法,在外部就可以通过这个方法访问私有属性了。
p = Person('Alice')
print(p.get_name()) # ==> Alice
注意,在外部调用实例方法时,是不需要显式传递self参数的。
另外,通过定义实例方法来操作私有属性的这种方法是推荐的,这种数据封装的形式除了能保护内部数据一致性外,还可以简化外部调用的难度。 当然,实例方法并不仅仅是为私有属性服务的,我们可以把和类的实例有关的操作都抽象成实例方法,比如:打印实例的详细信息等等。
class Animal(object):
def __init__(self, name, age, localtion):
self.name = name
self.age = age
self.localtion = localtion
def get_info(self):
return 'name = {}, age = {}, localtion = {}'.format(self.name, self.age, self.localtion)
dog = Animal('wangwang', 1, 'GuangDong')
print(dog.get_info())
参考答案把Animal类的age、name、localtion定义成私有属性,并定义对应的方法修改和获取他们的值。
class Animal(object):
def __init__(self, name, age, localtion):
self.__name = name
self.__age = age
self.__localtion = localtion
def set_name(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_age(self, age):
self.__age = age
def get_age(self):
return self.__age
def set_localtion(self, localtion):
self.__localtion =localtion
def get_localtion(self):
return self.__localtion
在上面,为了操作实例对象的私有属性,我们定义了实例方法;同样的,如果需要需要操作类的私有属性,则应该定义类的方法。
默认的,在class中定义的全部是实例方法,实例方法第一个参数 self 是实例本身。
要在class中定义类方法,需要这么写:
class Animal(object):
__localtion = 'Asia'
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def set_localtion(cls, localtion):
cls.__localtion = localtion
@classmethod
def get_localtion(cls):
return cls.__localtion
print(Animal.get_localtion()) # ==> Asia
Animal.set_localtion('Afica')
print(Animal.get_localtion()) # ==> Africa
和实例方法不同的是,这里有两点需要特别注意:
因为是在类上调用,而非实例上调用,因此类方法无法获得任何实例变量,只能获得类的引用。
参考答案如果将类属性count改为私有属性__count,则外部无法获取__count,但是可以通过一个类方法获取,请编写类方法获得__count值。
注意类方法需要添加 @classmethod
class Animal(object):
__localtion = 'Asia'
__count = 0
def __init__(self, name, age):
self.name = name
self.age = age
Animal.__count += 1
@classmethod
def get_count(cls):
return cls.__count
dog = Animal('wangwang', 1)
cat = Animal('mimi', 3)
pig = Animal('panpan', 1)
count = Animal.get_count()
print(count)
2020-10-302020-10-30 11:42:23阅读 1660
对人类的抽象可以定义为Person类,而学生、老师等,也都是人类,所以,在Python当中,如果定义学生Student的类,可以继承Person类。
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
接着定义Student类,在定义Student类的时候,由于继承了Person类,所以Student类自动拥有name、gender属性,因此,在定义Student类的时候,只需要把额外的属性加上即可。
class Student(Person):
def __init__(self, name, gender, score):
super(Student, self).__init__(name, gender)
self.score = score
student = Student('Alice', 'girl', 100)
print(student.name) # ==> Alice
print(student.gender) # ==> girl
print(student.score) # ==> 100
在定义继承类的时候,有几点是需要注意的:
__init__()
方法,需要调用super(Student, self).__init__(name, gender)
,来初始化从父类继承过来的属性参考答案请参考Student类,编写Teacher类,老师拥有任教某个科目的属性。
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Teacher(Person):
def __init__(self, name, gender, course):
super(Teacher,self).__init__(name,gender)
self.course = course
teacher = Teacher('Alice', 'Female', 'English')
print(teacher.name)
print(teacher.gender)
print(teacher.course)
随着我们学习步伐的前进,我们的程序会出现越来越多的类型,有我们自己定义的类,也有Python自有的str、list、dict等,他们的本质都是都是Python中的一种数据类型,这时有必要去判断数据的类型,通过函数isinstance()可以判断一个变量的类型。
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Student(Person):
def __init__(self, name, gender, score):
super(Student, self).__init__(name, gender)
self.score = score
class Teacher(Person):
def __init__(self, name, gender, course):
super(Teacher, self).__init__(name, gender)
self.course = course
p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')
当我们拿到变量 p、s、t 时,可以使用 isinstance 判断类型:
>>> isinstance(p, Person)
True # p是Person类型
>>> isinstance(p, Student)
False # p不是Student类型
>>> isinstance(p, Teacher)
False # p不是Teacher类型
这说明在继承链上,一个父类的实例不能是子类类型,因为子类比父类多了一些属性和方法。 我们再考察 s:
>>> isinstance(s, Person)
True # s是Person类型
>>> isinstance(s, Student)
True # s是Student类型
>>> isinstance(s, Teacher)
False # s不是Teacher类型
s 是Student类型,不是Teacher类型,这很容易理解。但是,s 也是Person类型,因为Student继承自Person,虽然它比Person多了一些属性和方法,但是,把 s 看成Person的实例也是可以的。
这说明在一条继承链上,一个实例可以看成它本身的类型,也可以看成它父类的类型。
isinstance也可以用于Python自有数据类型的判断。
s = 'this is a string.'
n = 10
isinstance(s, int) # ==> False
isinstance(n, str) # ==> False
参考答案请根据继承链的类型转换,依次思考 t 是否是 Person,Student,Teacher,object 类型,并使用isinstance()判断来验证您的答案。
使用isinstance()来进行判断
使用print()函数打印输出判断结果
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Student(Person):
def __init__(self, name, gender, score):
super(Student, self).__init__(name, gender)
self.score = score
class Teacher(Person):
def __init__(self, name, gender, course):
super(Teacher, self).__init__(name, gender)
self.course = course
p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')
isinstance(t, Person)
isinstance(t, Student)
isinstance(t, Teacher)
isinstance(t, object)
类具有继承关系,并且子类类型可以向上转型看做父类类型,如果我们从 Person 派生出 Student和Teacher ,并都写了一个who() 方法:
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def who(self):
return 'I am a Person, my name is %s' % self.name
class Student(Person):
def __init__(self, name, gender, score):
super(Student, self).__init__(name, gender)
self.score = score
def who(self):
return 'I am a Student, my name is %s' % self.name
class Teacher(Person):
def __init__(self, name, gender, course):
super(Teacher, self).__init__(name, gender)
self.course = course
def who(self):
return 'I am a Teacher, my name is %s' % self.name
接着,我们分别把不同类型的who()函数结果打印出来:
p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')
运行结果:
I am a Person, my name is Tim
I am a Student, my name is Bob
I am a Teacher, my name is Alice
这种行为称为多态。从定义上来讲,Student和Teacher都拥有来自父类Person继承的who()方法,以及自己定义的who()方法。但是在实际调用的时候,会首先查找自身的定义,如果自身有定义,则优先使用自己定义的函数;如果没有定义,则顺着继承链向上找。
class Boss(Person):
def __init__(self, name, gender,company):
super(Boss, self).__init__(name, gender)
self.company = company
b = Boss('Bob', 'Male', 'Alibaba')
b.who() # ==> I am a Person, my name is Bob
在Boss的定义类,没有定义who方法,所以会顺着继承链向上找到父类的who方法并且调用。
除了从一个父类继承外,Python允许从多个父类继承,称为多重继承。多重继承和单继承没有特别大的差异,只是在括号内加入多个需要继承的类的名字即可。
class A(object):
def __init__(self, a):
print ('init A...')
self.a = a
class B(A):
def __init__(self, a):
super(B, self).__init__(a)
print ('init B...')
class C(A):
def __init__(self, a):
super(C, self).__init__(a)
print ('init C...')
class D(B, C):
def __init__(self, a):
super(D, self).__init__(a)
print ('init D...')
多重继承的继承链就不是一棵树了,它像这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kqbi9LT5-1651453818300)(https://ask.qcloudimg.com/http-save/yehe-1217896/lty3oeb5zi.png?imageView2/2/w/1620)]
从上图可知,A类被继承了连词,那么A的__init__()
方法,是否会被调用两次呢?
d = D('d')
init A...
init C...
init B...
init D...
实践证明,在多重继承里,A虽然被继承了两次,但是__init__()
的方法只调用一次。
多重继承的目的是从两种继承树中分别选择并继承出子类,以便组合功能使用。
举个例子,Python的网络服务器有TCPServer、UDPServer、UnixStreamServer、UnixDatagramServer,而服务器运行模式有 多进程ForkingMixin 和 多线程ThreadingMixin两种。
要创建多进程模式的 TCPServer:
class MyTCPServer(TCPServer, ForkingMixin)
pass
要创建多线程模式的 UDPServer:
class MyUDPServer(UDPServer, ThreadingMixin):
pass
如果没有多重继承,要实现上述所有可能的组合需要 4x2=8 个子类。
参考答案已知类Student、Teacher继承Person类,技能类BasketballMixin、FootballMixin继承SkillMixin类,请通过多重继承,分别定义“会打篮球的学生”和“会踢足球的老师”。
class Person(object):
pass
class Student(Person):
pass
class Teacher(Person):
pass
class SkillMixin(object):
pass
class BasketballMixin(SkillMixin):
def skill(self):
return 'basketball'
class FootballMixin(SkillMixin):
def skill(self):
return 'football'
class BStudent(BasketballMixin, Student):
pass
class FTeacher(FootballMixin, Teacher):
pass
s = BStudent()
print(s.skill())
t = FTeacher()
print(t.skill())
在前面,我们通过isinstance()方法,可以判断一个对象是否是某个类型,从某种意义上来讲,通过isinstance()方法,我们获取到了一个对象的一些信息,那有没有别的方法可以获取到对象更多的信息呢? 通过type()函数,可以获得变量的类型。
n = 1
s = 'this is a string'
type(n) # ==>
type(s) # ==>
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Student(Person):
def __init__(self, name, gender, score):
super(Student, self).__init__(name, gender)
self.score = score
p = Person('Alice', 'Female')
s = Student('Bob', 'Male', 100)
type(p) # ==>
type(s) # ==>
通过dir()方法,可以获取变量的所有属性:
>>> n = 1
>>> dir(n)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', ...]
>>> s = 'this is a string'
>>> dir(s)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', ...]
在dir列出的属性中,有很多是以下划线开头和结尾的,这些都是特殊的方法,称为内建方法,在后面,我们还会学习这些方法。 而对于自定义对象:
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def who(self):
return 'I am a Person, my name is {}'.format(self.name)
p = Person('Alice', 'Female')
dir(p)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'gender', 'name', 'who']
对于实例变量,dir()返回所有实例属性,包括__class__
这类有特殊意义的属性。注意到方法who也是p的一个属性。
dir()返回的属性是字符串列表,如果已知一个属性名称,要获取或者设置对象的属性,就需要用 getattr() 和 setattr( )函数了。
>>> getattr(p, 'name') # 获取name属性
'Alice'
>>> setattr(p, 'name', 'Adam') # 设置新的name属性
>>> s.name
'Adam'
>>> getattr(s, 'age') # 获取age属性,但是属性不存在,报错:
Traceback (most recent call last):
File "" , line 1, in <module>
AttributeError: 'Person' object has no attribute 'age'
>>> getattr(s, 'age', 20) # 获取age属性,如果属性不存在,就返回默认值20:
20
对于Person类的定义:
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
希望除了name
和gender
外,可以提供任意额外的关键字参数,并绑定到实例,请修改 Person 的__init__()
定义,完成该功能。
class Person(object):
def __init__(self, name, gender, **kw):
self.name = name
self.gender = gender
for k, v in kw.items():
setattr(self, k, v)
p = Person('Bob', 'Male', age=18, course='Python')
print(p.age)
print(p.course)
__str__(), __add__(), __sub__(), __mul__(), __truediv__(), __len__(),
__new__(), __init__(), __del__(), __repr__(), __bytes__(), __format__(),
__lt__(), __le__(), __eq__(), __ne__(), __gt__(), __ge__(), __hash__(),
__bool__(), __dir__(), __set__(), __call__(), __slots__(), ...
对于Python的内建对象,比如int、dict、list等,通过str()方法,可以把这些对象转换为字符串对象输出。
num = 12
str(num) # ==> '12'
d = {1: 1, 2: 2}
str(d) # ==> '{1: 1, 2: 2}'
l = [1,2,3,4,5]
str(l) # ==> '[1, 2, 3, 4, 5]'
对于自定义对象,通过str()方法,同样可以得到对象所对应的字符串结果,只不过结果会有些难理解。
class Person:
pass
bob = Person()
str(bob) # ==> '<__main__.Person object at 0x7fc77b859c50>'
<__main__.Person object at 0x7fc77b859c50>
这个结果其实是Animal的实例cat在内存中的地址,这是相当难以理解的,不过引发思考的是,通过str()打印的数据,是怎么来的呢?
这其实是对象的内建方法__str__
返回的。 通过dir()方法,我们可以把对象的所有方法打印出来。
>>> dir(list)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
可以看到,int、dict、list等的内建对象都实现了自己的__str__()
方法,可以把相应的字符串返回,如果我们的类也想把容易理解的字符串输出的话,那么我们也需要实现类的__str__()
方法。
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def __str__(self):
return 'name: {}, gender: {}'.format(self.name, self.gender)
bob = Person('Bob', 'Male')
str(bob) # ==> 'name: Bob, gender: Male'
但是,对于直接在终端输入变量bob,得到的依然是这样结果。
>>> bob
<__main__.Person object at 0x7fc77b859cc0>
而对于int、list等的对象,直接输入变量也可得到可读的结果。
>>> num = 12
>>> str(num)
'12'
>>> d = {1: 1, 2: 2}
>>> d
{1: 1, 2: 2}
__str__()函数似乎没有在自定义类Person中生效,这是为什么呢?
这是因为 Python 定义了__str()__
和__repr__()
两种方法,__str()__
用于显示给用户,而__repr__()
用于显示给开发人员,当使用str()时,实际调用的是__str__()
方法,而直接输入变量,调用的是__repr__()
方法。
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def __str__(self):
return 'name: {}, gender: {}'.format(self.name, self.gender)
def __repr__(self):
return 'name: {}, gender: {}'.format(self.name, self.gender)
bob = Person('Bob', 'Male')
str(bob) # ==> 'name: Bob, gender: Male'
>>> bob
'name: Bob, gender: Male'
参考答案请给Student 类定义__str__和__repr__方法,使得能打印出’Student: name, gender, score’。
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Student(Person):
def __init__(self, name, gender, score):
super(Student, self).__init__(name, gender)
self.score = score
def __str__(self):
return 'Student: {}, {}, {}'.format(self.name, self.gender, self.score)
def __repr__(self):
return 'Student: {}, {}, {}'.format(self.name, self.gender, self.score)
s = Student('Bob', 'Male', 88)
print(s)
对于列表List或者元组Tuple,通过内建方法len(),可以得出列表或者元组中元素的个数。如果一个类表现得像一个list,想使用len()函数来获取元素个数时,则需要实现len()方法。
比如我们实现一个班级Class的类,初始化把班级的同学名字列表传进去,希望len()函数可以返回班级同学的数量时,可以这样实现。
class Class:
def __init__(self, students):
self.students = students
def __len__(self):
return len(self.students)
students = ['Alice', 'Bob', 'Candy']
class_ = Class(students)
len(class_) # ==> 3
通过自定义__len__()
方法,可以让len()
函数返回相关的结果,如果没有定义__len__()
方法的类使用len()
函数获取长度时,将会引起异常。
class Class:
def __init__(self, students):
self.students = students
class_ = Class(students)
len(class_)
Traceback (most recent call last):
File "" , line 1, in <module>
TypeError: object of type 'Class' has no len()
参考答案斐波那契数列是由 0, 1, 1, 2, 3, 5, 8…构成。
请编写一个Fib类,Fib(10)表示数列的前10个元素,print Fib(10) 可以打印出数列的前 10 个元素,len(Fib(10))可以正确返回数列的个数10。
class Fib(object):
def __init__(self, num):
self.res = []
self.num = num
a = 0
b = 1
for x in range(num):
self.res.append(a)
a, b = b, a + b
def __str__(self):
return str(self.res)
def __len__(self):
return self.num
f = Fib(10)
print(f)
print(len(f))
事实上,Python很多的操作都是通过内建函数来实现的,比如最熟悉的加减乘除,都是通过内建函数来实现的,分别是__add__、__sub__、__mul__、__truediv__
。因此,只要我们的自定义类实现了相关的内建函数,我们的类对象,也可以做到加减乘除。
对于有理数,我们可以使用Rational类来表示:
class Rational(object):
def __init__(self, p, q):
self.p = p
self.q = q
其中,p、q 都是整数,表示有理数 p/q。 如果要让Rational进行加法运算,需要正确实现__add__
:
class Rational(object):
def __init__(self, p, q):
self.p = p
self.q = q
def __add__(self, r):
return Rational(self.p * r.q + self.q * r.p, self.q * r.q)
def __str__(self):
return '{}/{}'.format(self.p, self.q)
定义好后,就可以尝试一下有理数的加法了:
>>> r1 = Rational(1, 2)
>>> r2 = Rational(2, 3)
>>> print(r1 + r2)
7/6
需要注意__add__()
函数,它有一个参数,表示的是运算的第二个操作数,比如:r1 + r2,那么在add()方法中的参数,r指的就是r2,这个参数是运算符重载的时候传递的。
另外,细心的同学可能注意到了,相比加减乘的特殊方法,除法的特殊方法名字较长__truediv__
,并且含有true这样的描述,这其实和Python除法是有关系的。
Python的除法可以分为地板除(你没看错,就是地板)和普通除法,地板除的特殊方法是__floordiv__
,普通除法是__truediv__
。
地板除法和普通除法不一样,地板除法的结果只会向下取整数。
>>> num = 5
>>> num.__truediv__(3)
1.6666666666666667
>>> num.__floordiv__(3)
1 # 向下取整
>>> num = 7
>>> num.__floordiv__(3)
2
在运算中,普通除法使用/
表示,而地板除使用//
表示。
>>> 5 / 3
1.6666666666666667
>>> 5 // 3
1
参考答案Rational类虽然可以做加法,但无法做减法、乘法和除法,请继续完善Rational类,实现四则运算。
def gcd(a, b):
if b == 0:
return a
return gcd(b, a % b)
class Rational(object):
def __init__(self, p, q):
self.p = p
self.q = q
def __add__(self, r):
return Rational(self.p * r.q + self.q * r.p, self.q * r.q)
def __sub__(self, r):
return Rational(self.p * r.q - self.q * r.p, self.q * r.q)
def __mul__(self, r):
return Rational(self.p * r.p, self.q * r.q)
def __truediv__(self, r):
return Rational(self.p * r.q, self.q * r.p)
def __str__(self):
g = gcd(self.p, self.q)
return '{}/{}'.format(int(self.p/g), int(self.q/g))
r1 = Rational(1, 2)
r2 = Rational(1, 5)
print(r1 + r2)
print(r1 - r2)
print(r1 * r2)
print(r1 / r2)
由于Python是动态语言,任何实例在运行期都可以动态地添加属性。比如:
class Student(object):
def __init__(self, name, gender, score):
self.name = name
self.gender = gender
self.score = score
此时,Student类有三个属性,name、gender、score,由于是动态语言,在运行时,可以随意添加属性。
student = Student('Bob', 'Male', 99)
student.age = 12 # ==> 动态添加年龄age属性
如果要限制添加的属性,例如,Student类只允许添加 name、gender和score 这3个属性,就可以利用Python的一个特殊的__slots__
来实现。
class Student(object):
__slots__ = ('name', 'gender', 'score')
def __init__(self, name, gender, score):
self.name = name
self.gender = gender
self.score = score
使用__slots__ = ('name', 'gender', 'score')
限定Student类的属性,这个时候在外部再次添加动态属性age,将会报错。
student = Student('Bob', 'Male', 99)
>>> student.age = 12 # ==> 动态添加年龄age属性
Traceback (most recent call last):
AttributeError: 'Student' object has no attribute 'age'
__slots__
的目的是限制当前类所能拥有的属性,避免因为外部属性的操作导致类属性越来越难以管理。
参考答案假设Person类通过__slots__定义了name和gender,请在派生类Student中通过__slots__继续添加score的定义,使Student类可以实现name、gender和score 3个属性。
class Person(object):
__slots__ = ('name', 'gender')
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Student(Person):
__slots__ = ('score',)
def __init__(self, name, gender, score):
self.name = name
self.gender = gender
self.score = score
s = Student('Bob', 'male', 59)
s.name = 'Tim'
s.score = 99
print(s.score)
在Python中,函数其实是一个对象,我们可以将一个函数赋值给一个变量,而不改变函数的功能。
>>> f = abs
>>> f
<built-in function abs>
>>> abs
<built-in function abs>
>>> f.__name__
'abs'
>>> f(-123)
123
把内建函数abs()赋值给变量f之后,可以看到f就和abs一样,都是。
由于 f 可以被调用,所以,f 被称为可调用对象,而事实上,所有的函数都是可调用对象。
如果把一个类实例也变成一个可调用对象,可以实现一个特殊的方法__call__()
。
例如,我们把Person类变成一个可调用对象:
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def __call__(self, friend):
print('My name is {}...'.format(self.name))
print('My friend is {}...'.format(friend))
接着我们初始化一个Person对象,并对这个对象通过函数的方式调用:
>>> p = Person('Bob', 'Male')
>>> p('Alice') # ==> 用函数的方式调用Person类的实例p
My name is Bob...
My friend is Alice...
请实现前面介绍过的斐波那契数列类Fib,加入__call__方法,使得调用的方式如下简单。
>>> f = Fib()
>>> print f(10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
参考答案
class Fib(object):
def __init__(self):
self.res = []
def __call__(self, num):
a = 0
b = 1
for x in range(num):
self.res.append(a)
a, b = b, a + b
return self.res
f = Fib()
print(f(10))
Python语言本身提供了非常多的模块,比如数学模块math、cmath、decimal、statistics;文件模块pathlib、stat、shutil等;除了使用官方模块,有时候也需要自定义模块。
如果我们需要创建一个tools模块,用来实现众多的工具函数,那么我们可以创建一个tools.py的文件,并在这个文件里面实现一些函数,如:say_hello()函数、say_goodbye()函数。
# tools.py
def say_hello():
print('hello')
def say_goodbye():
print('goodbye')
这样就定义了一个叫tools的模块,接着就可以使用这个模块了,在使用之前,我们需要先导入模块,下面我们来详细学习。
定义一个公共模块common.py,在common.py中,包含公共函数say_hello(name),它接受一个参数,输出:Hello 的结果。
参考答案# common.py
def say_hello(name):
print('Hello {}'.format(name))
要使用一个模块,我们必须首先导入该模块。Python使用import语句导入一个模块,Python官方提供很多有用的模块,比如:os模块、sys模块、time模块、math模块等等。
导入官方模块,不需要考虑路径的问题,例如,导入系统自带的模块 math,直接导入即可。如果是导入自定义模块,则需要考虑路径问题,我们下节课继续学习。
导入官方模块math:
import math
导入以后,你就可以认为math是一个指向已导入模块的变量,通过该变量,我们可以访问math模块中所定义的所有公开的函数、变量和类:
# 属性:圆周率
>>> import math
>>> math.pi
3.141592653589793
# 函数:次方
>>> math.pow(2, 3)
8.0
如果希望导入模块的指定部分属性或函数,那么使用from...import...
语句。
>>> from math import pi
>>> print(pi)
3.141592653589793
这个时候,由于pow()函数没有导入,所以是不能使用pow()函数的。
如果希望导入模块里面的所有内容,那么使用from ...import *
语句。
>>> from math import *
>>> print(pi)
3.141592653589793
>>> pow(2, 3)
8.0
如果从一个模块导入函数,有可能会遇到导入的函数与本文件的函数冲突的情况。例如:本文件定义了一个pow()函数,同时从math模块也导入了一个pow()函数,这种情况下就会引起冲突;事实上,这种冲突的情况经常发生。
有两种方法可以解决这个问题,第一种是直接导入模块,不指定导入模块里面的具体内容;第二种方法就是使用from ... import as ...
语句,as类似重命名,可以把导入的函数或属性重命名为别的名字。
>>> from math import pow as mathpow
>>> mathpow(2, 3)
8.0
math模块还提供了非常多的数学计算函数,比如:正弦sin()函数,余弦cos()函数,请使用两种导入的方式,使用这两个函数。
参考答案import math
math.sin(0)
math.cos(0)
# 或者
from math import sin, cos
sin(0)
cos(0)
导入官方模块的时候,不需要考虑路径问题,这是因为在搜索模块的时候,会默认包含官方模块的路径,所以导入官方模块不需要考虑路径的问题。
如果需要导入自定义模块,则需要了解Python导入模块搜索的路径。
通过sys模块,可以知道导入模块的路径。
>>> import sys
>>> sys.path
['', '/data/miniconda3/lib/python3.8', '/data/miniconda3/lib/python3.8/site-packages']
它返回的是一个列表,表示的是在搜索Python模块时,会搜索的路径,在示例中,返回了四个路径。我们分析一些关键路径:
第一个路径是''
,它是一个空字符串,表达的是当前路径的意思。
第二个路径是/data/miniconda3/lib/python3.8
,它是Python默认模块的存放的路径,在这个路径下,可以发现有os、sys等模块的代码。
第三个路径是/data/miniconda3/lib/python3.8/site-packages
,它是第三方模块代码的存放路径,在这个路径下,存放的是需要安装的第三方模块。
那如何使用我们前面定义的tools.py模块呢? 我们在tools.py同级目录,创建main.py文件:
# main.py
import tools # 导入模块
tools.say_hello() # 调用模块里面的say_hello()函数
tools.say_goodbye() # 调用模块里面的say_goodbye()函数
就可以运行了。 因为在搜索包的路径时,会搜索当前路径(上述:sys.path结果的第一项),因此在同一个目录内的tools.py模块,可以被搜索到,所以能够import进来。
尽管Python的官方模块已经提供了非常强大的能力,但是仍有大量热心开发者提供了非常好用的第三方库,在实际开发中,也会经常使用,比如Web开发框架,Django、Flask,异步任务框架:Celery等。 在安装Python的时候,Python环境提供了安装第三方模块的工具:pip,通过这个工具,可以非常快捷的安装第三方模块。 安装Django模块:
pip install django
卸载Django模块:
pip uninstall django
到目前为止,我们编写的程序都是直接运行的,在运行过程中并没有接收程序外部的输入。比如,通过Python程序,我们可以快速算出从1到100的乘法结果。
result = 1
for i in range(1, 101):
result = result * i
print(result)
但是如果需要计算从1到200的乘法结果,则只能通过修改程序去实现。
result = 1
for i in range(1, 201):
result = result * i
print(result)
如果可以通过输入,改变计算的范围,那就好了,input()函数可以接收外部的输入。
>>> num = input('please input number: ')
please input number: 201
>>> print(num)
201
因此,通过input()函数,则可以改变上面程序运行的范围,注意:输入的是字符串,需要转型为数字类型。
num = input('please input number: ')
num = int(num)
result = 1
for i in range(1, num):
result = result * i
print(result)
eval()函数可以把字符串转换为等值的结果,比如eval(‘1+1’),得到结果为2。请使用eval实现一个简单的计算器,可以输入表达式并打印计算结果。
参考答案import math
math.sin(0)
math.cos(0)
# 或者
from math import sin, cos
sin(0)
cos(0)
通过print()可以从数据输出数据,通过input()可以向程序输入数据,但这些都是标准屏幕上的操作,本节课学习文件的读写操作。
Python 提供了open()函数,可以打开一个文件,得到一个文件file对象,而file对象提供相关的方法对文件内容进行读写等操作。
open()函数有若干个参数,比较重要的是以下三个参数:
常用的打开模式如下:
模式 | 描述 |
---|---|
t | 文本模式(默认) |
x | 写模式,新建一个文件 |
b | 二进制模式,打开二进制文件 |
+ | 更新一个文件(可读可写) |
r | 以只读模式打开一个文件 |
rb | 以二进制格式只读模式打开一个文件 |
w | 打开一个文件进行写入,如果文件内容已存在,会清除原有的内容 |
wb | 以二进制格式只写模式打开一个文件,会清除原有的内容 |
a | 打开一个文件并追加内容,会往文件尾部添加内容 |
ab | 以二进制格式打开一个文件并追加内容,会往文件尾部添加内容 |
w+ | 打开一个文件进行读写,如果文件内容已存在,会清除原有的内容 |
a+ | 打开一个文件并使用追加进行读写 |
注意,为了安全操作文件,文件使用完毕后,需要使用close()函数正确关闭。 在当前目录下新建一个test.txt文件,并新建一个main.py,此时文件目录如下:
|-- test.txt
+-- main.py
f = open('test.txt', 'r') # 打开test.txt文件
type(f) # 打印f的类型()
f.close() # 关闭文件
注意,在打开文本文件是并不需要特别指定模式t
,因为默认就是以文本方式打开文件的。
除了文本以外,还有大量的非文本文件,比如图片、压缩文件、视频文件、音乐文件等等,这种文件统称为二进制文件,在Python中打开二进制文件,需要不同的打开模式。
模式 | 描述 |
---|---|
b | 二进制模式,打开二进制文件 |
wb | 以二进制格式只写模式打开一个文件,会清除原有的内容 |
ab | 以二进制格式打开一个文件并追加内容,会往文件尾部添加内容 |
rb | 以二进制格式只读模式打开一个文件 |
f = open('test.jpg', 'rb')
f.close()
打开文件之后,就可以读取文件的内容,文件对象提供多种读取文件内容的方法。 打开test.txt文件:
f = open('test.txt', 'r') # 打开test.txt文件
f.close() # 关闭文件
test.txt文件有以下内容
Hello World.
Hello Python.
Hello Imooc.
文件对象提供read()方法,可以读取文件中的若干个字符,它提供一个参数size,可以指定读取字符的数量。
s = f.read(5)
print(s) # ==> Hello
当read()之后,访问文件的游标就会移动到第六个字符前面,此时,继续read,将得到Hello后面的结果。
s = f.read(6)
print(s) # ==> ' World'
文件对象提供readline()方法,和read()方法类似,可以读取文件中的若干个字符,它也提供一个参数size,可以指定读取字符的数量,不过和read()方法不同的是,readline()方法遇到一行结束的时候,就会返回。
f.close()
f = open('test.txt', 'r') # 重新打开文件
s = f.readline(20)
print(s) # ==> 'Hello World.\n'
可以看到,打印的内容并没有20个字符,readline最多返回一行的所有字符。
文件对象提供readlines()方法,可以读取多行字符,返回一个列表。它提供一个hint参数,表示指定读取的行数,没有指定则默认以列表的形式返回文件所有的字符串。
f.close()
f.open('test.txt', 'r')
s = f.readlines()
print(s) # ==> ['Hello World.\n', 'Hello Python.\n', 'Hello Imooc.\n']
要把字符串内容写入文件,需要使用w的模式打开文件。
模式 | 描述 |
---|---|
w | 打开一个文件进行写入,如果文件内容已存在,会清除原有的内容 |
wb | 以二进制格式只写模式打开一个文件,会清除原有的内容 |
w+ | 打开一个文件进行读写,如果文件内容已存在,会清除原有的内容 |
f = open('test.txt', 'w')
文件对象提供write方法向文件内写入若干字符,它接受一个字符串参数,表示需要写入的字符串。
f = open('test.txt', 'w')
f.write('Hello World\n')
f.close()
文件对象提供writelines()方法向文件内容写入多行数据,它接受一个列表,表示需要写入的字符串列表。
lines = ['Hello World\n', 'Hello Python\n', 'Hello Imooc\n']
f = open('test.txt', 'w')
f.writelines(lines)
f.close()
有test.txt文件,包含以下内容:
Hello World
Hello Python
Hello Imooc
请从test.txt文件读取以上内容,并将每一行字符串反转,写入test1.txt文件。
dlroW olleH
nohtyP olleH
coomI olleH
参考答案
字符串反转可以使用切片实现: reverse = str_[::-1]
换行符是’\n’,字符串反转的时候,换行符也会翻转
f = open('test.txt', 'r')
lines = f.readlines()
f1 = open('test1.txt', 'w')
for line in lines:
line = line[::-1]
f1.write(line)
f1.close()
f.close()
通过w
的打开方式打开文件,会清空文件的内容,这在很多场景下是不合适的,比如写系统日志的时候,需要累积随时间推移的所有数据。
Python提供文件追加内容的打开模式,可以往文件尾部添加内容,又不清空文件原有的内容。
模式 | 描述 |
---|---|
a | 打开一个文件并追加内容,会往文件尾部添加内容 |
ab | 以二进制格式打开一个文件并追加内容,会往文件尾部添加内容 |
a+ | 打开一个文件并使用追加进行读写 |
f = open('test.txt', 'a')
f.write('Hello Everyone\n')
f.close()
使用a
的打开方式打开文件,文件游标默认是在文件的尾部,因此,可以便捷的往文件尾部添加内容,除此以外,文件对象还提供seek()方法,可以移动文件的游标位置,它接受一个参数,表示文件的位置,0:文件首部,1:当前位置,2:文件尾部,通过seek()可以把文件游标移动到文件首部但不删除文件的内容。
f = open('test.txt', 'a+')
content = f.readlines()
print(content) # ==> []
f.seek(0)
content = f.readlines()
print(content) # ==> ['Hello World\n', 'Hello Python\n', 'Hello Imooc\n']
第一次print(content)
的时候,由于文件游标在文件的尾部,所以readlines()读取不到任何数据,打印了空的结果,第二次print(content)
的时候,由于通过seek(0),文件游标移动到了文件的首部,因此readlines()就返回了文件所有的内容。
在进行文件操作的时候,正确关闭一个文件非常重要,如果在文件读写后,没有正确关闭一个文件的话,则有可能导致文件损坏,文件内容丢失等问题。
在一般情况下,我们使用文件对象的close()方法,来关闭一个文件。 但是,使用close()方法,也不是100%安全的,如果在close()文件之前,程序异常退出了,那么文件也得不到正确的关闭。比如:
f = open('test.txt', 'a+')
exit(-1) # ==> 模拟程序异常退出
f.close() # ==> close语句永远的不到执行
在实际工程中,close()文件之前,为了正确关闭文件,需要考虑各种异常情况,这是非常麻烦的一件事,Python提供with
关键字,可以免除这类后顾之忧。
with
关键字对资源进行访问的场合,会确保不管在使用过程中是否发生异常,都会执行必要的“清理”的操作,释放资源,比如文件使用后自动关闭等等。
with
的使用方法如下:
with open('test.txt', 'r') as f:
content = f.readlines()
for line in content:
print(line)
2020-10-302020-10-30 11:43:20阅读 1450
要进行网络通信,需要建立起通信双方的连接,连接的双方分别称为客户端和服务端,在Python中,使用套接字socket来建立起网络连接。 套接字包含在socket模块中:
import socket
socket.socket()
对于客户端和服务端,都是使用socket来建立连接的,但是在使用行为上,客户端和服务端会有一些不一样。
服务端建立需要四个步骤:新建socket、绑定IP和端口(bind)、监听连接(listen)、接受连接(accept)。
客户端建立则简单一些,仅需两个步骤:新建socket、连接服务端(connect)。 当网络连接上以后,客户端和服务端就可以进行数据通信了,套接字通过send()函数发送数据,通过recv()函数接收数据。
先看服务端的过程,新建一个server.py的文件:
import socket
server = socket.socket() # 1. 新建socket
server.bind(('127.0.0.1', 8999)) # 2. 绑定IP和端口(其中127.0.0.1为本机回环IP)
server.listen(5) # 3. 监听连接
s, addr = server.accept() # 4. 接受连接
print('connect addr:{}'.format(addr))
content =s.recv(1024)
print(str(content, encoding='utf-8')) # 接受来自客户端的消息,并编码打印出来
s.close()
如上,服务端就编写完毕,接下来是编写客户端,新建一个client.py的文件:
import socket
client = socket.socket() # 1. 新建socket
client.connect(('127.0.0.1', 8999)) # 2. 连接服务端(注意,IP和端口要和服务端一致)
client.send(bytes('Hello World. Hello Socket', encoding='utf-8')) # 发送内容,注意发送的是字节字符串。
client.close()
接着在一个终端先运行服务端:
python server.py
然后再在另外一个终端运行客户端:
python client.py
在服务端的终端,将会输出以下信息:
connect addr:('127.0.0.1', 50382)
b'Hello World. Hello Socket'
在互联网的世界中,网页、手机H5等都是通过HTTP向用户提供服务的,这些信息存储在HTTP服务器中,HTTP服务器是一种特殊的Socket服务器,它在网络连接之上,定义了HTTP协议,使得网页、手机H5等数据,都可以以标准的HTTP协议进行传输。
Python提供了简易的HTTP服务器,可以直接运行起来。 在终端,输入这条命令,就可以启动一个HTTP服务器。
python -m http.server
启动成功后,会输出以下信息:
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
提示HTTP服务器在本机8000端口运行,接着就可以在浏览器输入http://127.0.0.1:8000看到由这个服务器提供的网页。
这个HTTP服务器会把运行目录的所有文件列出来,并提供下载功能。
在上一节,使用了Python自带的功能启动了一个HTTP服务器,并通过浏览器浏览到了这个HTTP服务器提供的页面。在浏览的过程中,实际上是浏览器向HTTP服务器发送了一个HTTP请求。
除了使用浏览器发送HTTP请求,通过代码也可以向HTTP服务器发送请求,Python提供了相关的库urllib,通过urllib包里面的request,可以向其他HTTP服务器发起请求。
from urllib import request
response = request.urlopen('https://www.imooc.com') # 向慕课网官网发出请求
print(response) # ==>
请求成功的话,会得到一个HTTPResponse,它是来自HTTP服务器的一个回应,可以把这个回应的一些信息打印出来。
状态码:
print(response.status) # ==> 200
状态码是一个三位整数,在HTTP协议的标准里面,定义了很多状态码,其中200表示请求是正常的。
响应头:
for k, v in response.getheaders():
print('{}: {}'.format(k, v))
将会输出以下信息,这是HTTPResponse附带的一些信息,包括服务端的服务器是什么、请求时间、内容类型、内容长度等等。
Server: openresty
Date: Thu, 20 Aug 2020 08:16:07 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 376639
Connection: close
Vary: Accept-Encoding
Vary: Accept-Encoding
X-Varnish: 280516221 281284036
Age: 29
Via: 1.1 varnish (Varnish/6.0)
X-Cache: HIT from CS42
Accept-Ranges: bytes
Python官方提供的urllib库可以满足一般情况下的HTTP操作,但是urllib这个库设计是用来处理url地址的,并不是专门处理HTTP操作的包。因此,在很多场景下,一般会使用requests库来进行HTTP请求。
requests库是著名的Python第三方库,使用requests库,可以定制化你的HTTP请求,包括请求方法,请求参数等等。
由于requests是第三方库,因此在使用前,需要安装。
pip install requests
安装完成后,使用requests库来请求百度。
response = requests.get('https://www.baidu.com')
# 打印状态码
print(response.status_code)
# 打印回应头
print(response.headers)
在一般的使用上,requests和urllib没有太大区别,但是在复杂的场景中,requests可以提供urllib无法提供的强大功能。因此,在使用上,建议使用requests库代替urllib库来进行HTTP请求等的操作。
通过urllib或者requests请求后,会得到一个HTTPResponse,HTTPResponse拥有状态码、回应头等的信息。
但我们知道,对于一个页面,通常是由文字、图片等信息组成的,这些属于一个HTTPResponse的内容。
import requests
response = requests.get('https://www.baidu.com')
content = str(response.content, encoding='utf-8') # ==> 打印具体内容
打印的结果是一个很长的字符串,显得杂乱无章,但其实它是由结构的,它是一个标准的HTML页面,可以从页面内容里面获取很多有用的数据。
网络爬虫是典型的应用程序,它的工作原理就是通过不断的请求互联网的页面,并从回应中解析获取出有用的数据;数据积累后,可以有很多用处。
通过requests获得网页的内容后,我们可以尝试使用一些简单的方法获取页面的内容。
content_list = content.split('\n') # 分行
len(content_list) # 打印页面内容的行数
在网页中,页面内部链接其他资源的信息是通过href提供的,通过字符串匹配的方式可以过滤出包含链接的行。
for line in content_list:
if 'href' in line:
print(line.strip())
2020-10-302020-10-30 11:43:38阅读 1880
在前面,我们了解了高阶函数的概念,并编写了一个简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
如果传入abs作为参数f的值:
add(-5, 9, abs)
根据函数的定义,函数执行的代码实际上是:
abs(-5) + abs(9)
由于参数 x, y 和 f 都可以任意传入,如果 f 传入其他函数,就可以得到不同的返回值。
map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f依次作用在list的每个元素上,map()函数会返回一个迭代器,可以依次迭代得到原来list的元素被函数f处理后的结果。
>>> map(f, list)
例如,对于list [1, 2, 3, 4, 5, 6, 7, 8, 9]。 如果希望把list的每个元素都作平方,就可以利用map()函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMeeM1I2-1651453818303)(https://ask.qcloudimg.com/http-save/yehe-1217896/vnh16hhl2j.png?imageView2/2/w/1620)]
我们定义需要传入函数f(x)=x*x,就可以利用map()函数完成这个计算:
def f(x):
return x*x
for item in map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]):
print(item)
得到结果:
[1, 4, 9, 10, 25, 36, 49, 64, 81]
由于list包含的元素可以是任何类型,因此,map() 不仅仅可以处理只包含数值的 list,事实上它可以处理包含任意类型的 list,只要传入的函数f可以处理这种数据类型。
和map函数一样,reduce()函数也是Python内置的一个高阶函数。reduce()函数接收的参数和 map() 类似,一个函数 f,一个list,但行为和 map()不同,reduce()传入的函数 f 必须接收两个参数,reduce()对list的每个元素反复调用函数f,并返回最终结果值。
在python2中,reduce()函数和map()函数一样,可以直接使用,但是在python3中,reduce()函数被收录到functools包内,需要引入functools才可以使用。 例如,编写一个f函数,接收x和y,返回x和y的和:
def f(x, y):
return x + y
调用 reduce(f, [1, 3, 5, 7, 9]):
from functools import reduce
def f(x, y):
return x + y
print(reduce(f, [1,3,5,7,9])) # ==> 25
得到的结果是25,实际过程是这样的,reduce()函数会做如下计算:
先计算头两个元素:f(1, 3),结果为4;
再把结果和第3个元素计算:f(4, 5),结果为9;
再把结果和第4个元素计算:f(9, 7),结果为16;
再把结果和第5个元素计算:f(16, 9),结果为25;
由于没有更多的元素了,计算结束,返回结果25。
上述计算实际上是对 list 的所有元素求和。虽然Python内置了求和函数sum(),但是,利用reduce()求和也很简单。 reduce()还可以接收第3个可选参数,作为计算的初始值。如果把初始值设为100,计算:
print(reduce(f, [1, 3, 5, 7, 9], 100)) # ==> 125
结果将变为125,因为第一轮计算是:
计算初始值和第一个元素:f(100, 1),结果为101。
filter()函数是 Python 内置的另一个有用的高阶函数,filter()函数接收一个函数 f 和一个list,这个函数 f 的作用是对每个元素进行判断,返回 True或 False,filter()根据判断结果自动过滤掉不符合条件的元素,并返回一个迭代器,可以迭代出所有符合条件的元素。
例如,要从一个list [1, 4, 6, 7, 9, 12, 17]中删除偶数,保留奇数,首先,要编写一个判断奇数的函数:
def is_odd(x):
return x % 2 == 1
然后,利用filter()过滤掉偶数:
for item in filter(is_odd, [1, 4, 6, 7, 9, 12, 17]):
print(item)
结果:1,7,9,17。 利用filter()函数,可以完成很多很有用的功能,例如,删除 None 或者空字符串:
def is_not_empty(s):
return s and len(s.strip()) > 0
for item in filter(is_not_empty, ['test', None, '', 'str', ' ', 'END']):
print(item)
结果:test, str, END 注意:注意: s.strip()会默认删除空白字符(包括’\n’, ‘\r’, ‘\t’, ’ '),如下:
s = ' 123'
s.strip() # ==> 123
s= '\t\t123\r\n'
s.strip() # ==> 123
Python内置的 sorted()函数可对list进行排序:
>>> sorted([36, 5, 12, 9, 21])
[5, 9, 12, 21, 36]
可以看到,sorted()函数,默认是由小到大排序列表的元素。
>>> score = [('Alice', 72), ('Candy', 90), ('Bob', 62)]
>>> sorted(score)
[('Alice', 72), ('Bob', 62), ('Candy', 90)]
当list的每一个元素又是一个容器时,则会以第一个元素来排序,比如在score中,每个元素都是包含名字和成绩的一个tuple,sorted()函数则按名字首字母进行了排序并返回。
对于上述排序成绩的情况,默认是按照第一个名字进行排序的,有没有办法让sorted()函数按照成绩来进行排序呢?
如果需要按照成绩高低进行排序,需要指定排序的字段是成绩,sorted接受key参数,用来指定排序的字段,key的值是一个函数,接受待排序列表的元素作为参数,并返回对应需要排序的字段。因此,sorted()函数也是高阶函数。
def k(item):
return item[1] # ==> 按成绩排序,成绩是第二个字段
sorted(score, key=k)
得到结果:[(‘Bob’, 62), (‘Alice’, 72), (‘Candy’, 90)] 。 如果需要倒序,指定reverse参数即可。
sorted(score, key=k, reverse=True)
得到结果:[(‘Candy’, 90), (‘Alice’, 72), (‘Bob’, 62)] 。
在函数内部,是可以定义子函数的。
def func():
# 定义子函数
def sub_func():
print('call sub_func.')
sub_func()
>>> func()
call sub_func.
作为高阶函数,可以接受函数作为参数,其实高阶函数,除了不仅仅可以返回int、str、list、dict等数据类型,还可以返回函数。因此,可以把函数的子函数返回。
def f():
print('call f()...')
# 定义函数g:
def g():
print('call g()...')
# 返回函数g:
return g
仔细观察上面的函数定义,我们在函数 f 内部又定义了一个函数 g。由于函数 g 也是一个对象,函数名 g 就是指向函数 g 的变量,所以,最外层函数 f 可以返回变量 g,也就是函数 g 本身。 调用函数 f,我们会得到 f 返回的一个函数:
>>> x = f() # 调用f()
call f()...
>>> x # 变量x是f()返回的函数:
<function f.<locals>.g at 0x7f4a4936dbf8>
>>> x() # x指向函数,因此可以调用
call g()... # 调用x()就是执行g()函数定义的代码
有必要注意的是,返回函数和返回函数值的语句是非常类似的,返回函数时,不能带小括号,而返回函数值时,则需要带上小括号以调用函数。
# 返回函数
def myabs():
return abs
# 返回函数值
def myabs(x):
return abs(x)
返回函数有很多应用,比如可以将一些计算延迟执行,举个例子,定义一个普通的求和函数。
def calc_sum(list_):
return sum(list_)
调用calc_sum()函数时,将立刻计算并得到结果:
>>> calc_sum([1, 2, 3, 4])
10
但是,如果返回一个函数,就可以“延迟计算”:
def calc_sum(list_):
def lazy_sum():
return sum(list_)
return lazy_sum
调用calc_sum()并没有计算出结果,而是返回函数:
>>> f = calc_sum([1, 2, 3, 4])
>>> f
<function calc_sum.<locals>.lazy_sum at 0x7f4a4936db70>
对返回的函数进行调用时,才计算出结果:
>>> f()
10
由于可以返回函数,我们在后续代码里就可以决定到底要不要调用该函数。
在函数内部定义的函数和外部定义的函数是一样的,只是他们无法被外部访问:
def g():
print('g()...')
def f():
print('f()...')
return g
将g的定义移入函数 f 内部,防止其他代码调用 g:
def f():
print('f()...')
def g():
print('g()...')
return g
但是,考察上一小节定义的 calc_sum 函数:
def calc_sum(list_):
def lazy_sum():
return sum(list_)
return lazy_sum
注意: 发现没法把 lazy_sum 移到 calc_sum 的外部,因为它引用了 calc_sum 的参数 list_。
像这种内层函数引用了外层函数的变量(参数也算变量),然后返回内层函数的情况,称为闭包(Closure)。
闭包的特点是返回的函数还引用了外层函数的局部变量,所以,要正确使用闭包,就要确保引用的局部变量在函数返回后不能变。举例如下:
# 希望一次返回3个函数,分别计算1x1,2x2,3x3:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果全部都是 9(请自己动手验证)。
原因就是当count()函数返回了3个函数时,这3个函数所引用的变量 i 的值已经变成了3。由于f1、f2、f3并没有被调用,所以,此时他们并未计算 i*i,当 f1 被调用时:
>>> f1()
9 # 因为f1现在才计算i*i,但现在i的值已经变为3
因此,返回函数不要引用任何循环变量,或者后续会发生变化的变量。
高阶函数可以接收函数做参数,有些时候,我们不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算 f(x)=x * x时,f(x)就是作为参数传入map的。 在前面,我们是显式的定义了一个f(x)的函数,除此以外,其实可以直接传入匿名函数。
匿名函数使用lambda定义:lambda x: x * x,就可以完成原来显式定义的f(x)函数的功能,冒号前面的x表示匿名函数的参数,后面的是一个表达式,匿名函数有个限制,就是只能有一个表达式,不写return,返回值就是该表达式的结果。
result = [item for item in map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])]
print(result) # ==> [1, 4, 9, 16, 25, 36, 49, 64, 81]
同理,对于reduce()函数,也同样可以通过定义匿名函数来实现相同的逻辑。
from functools import reduce
reduce(lambda x, y: x + y, [1,3,5,7,9])
Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。 使用 decorator 用Python提供的 @ 语法,这样可以避免手动编写 f = decorate(f) 这样的代码。 考察一个@log的定义:
def log(f):
def fn(x):
print('call ' + f.__name__ + '()...')
return f(x)
return fn
对于阶乘函数,@log工作得很好:
@log
def factorial(n):
return reduce(lambda x,y: x*y, range(1, n+1))
print(factorial(10))
结果:
call factorial()...
3628800
但是,对于参数不是一个的函数,调用将报错:
@log
def add(x, y):
return x + y
print(add(1, 2))
>>> print(add(1, 2))
Traceback (most recent call last):
File "" , line 1, in <module>
TypeError: fn() takes 1 positional argument but 2 were given
因为 add() 函数需要传入两个参数,但是 @log 写死了只含一个参数的返回函数。
要让 @log 自适应任何参数定义的函数,可以利用Python的 args 和 *kwargs,保证任意个数的参数总是能正常调用:
def log(f):
def fn(*args, **kwargs):
print('call ' + f.__name__ + '()...')
return f(*args, **kwargs)
return fn
考察上一节的 @log 装饰器:
def log(f):
def fn(x):
print('call ' + f.__name__ + '()...')
return f(x)
return fn
发现对于被装饰的函数,log打印的语句是不能变的(除了函数名)。
如果有的函数非常重要,希望打印出’[INFO] call xxx()…‘。 有的函数不太重要,希望打印出’[DEBUG] call xxx()…'。 这时,log函数本身就需要传入’INFO’或’DEBUG’这样的参数,类似这样:
@log('DEBUG')
def my_func():
pass
把上面的定义翻译成高阶函数的调用,就是:
my_func = log('DEBUG')(my_func)
上面的语句看上去还是比较绕,再展开一下:
log_decorator = log('DEBUG')
my_func = log_decorator(my_func)
上面的语句又相当于:
log_decorator = log('DEBUG')
@log_decorator
def my_func():
pass
所以,带参数的log函数首先返回一个decorator函数,再让这个decorator函数接收my_func并返回新函数,相当于是在原有的二层嵌套里面,增加了一层嵌套:
def log(prefix):
def log_decorator(f):
def wrapper(*args, **kw):
print('[{}] {}()...'.format(prefix, f.__name__))
return f(*args, **kw)
return wrapper
return log_decorator
@log('DEBUG')
def test():
pass
test()
执行结果:
[DEBUG] test()...
对于这种三层嵌套的decorator定义,你可以先把它拆开:
# 标准decorator:
def log_decorator(f):
def wrapper(*args, **kw):
print('[{}] {}()...'.format(prefix, f.__name__))
return f(*args, **kw)
return wrapper
return log_decorator
# 返回decorator:
def log(prefix):
return log_decorator(f)
当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化调用者的负担。
比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
>>> int('12345')
12345
但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
def int2(x, base=2):
return int(x, base)
这样,我们转换二进制就非常方便了:
>>> int2('1000000')
64
>>> int2('1010101')
85
偏函数指的就是“创建一个调用另外一个部分参数或变量已经预置的函数”的函数的用法,如上所示,int()函数的base参数,没有指定的时候,默认是以十进制工作的,当指定base=2的时候,int2实际上就变成了部分参数(base)已经预置了的偏函数。 functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85