当前位置: 首页 > 科技观察

被人鄙视,把Python当成“弱”类型语言

时间:2023-03-20 10:46:01 科技观察

Python想必大家都不陌生,连它有用没用的争论都可能看腻了。但无论如何,作为高考必加的语言,它还是有其独特之处的。今天我们再聊一聊Python。Python是一种动态强类型语言《流畅的 Python》书中提到,如果一种语言很少隐式转换类型,就说明它是一种强类型语言,比如Java、C++和Python都是强类型语言。△Python的强类型体现。同时,如果一种语言经常对类型进行隐式转换,就说明它是一种弱类型语言,PHP、JavaScript、Perl都是弱类型语言。△动态弱类型语言:JavaScript当然,以上简单例子的对比并不意味着Python是强类型语言,因为Java也支持整数和字符串的加法运算,Java是强类型语言。所以书中也有静态类型和动态类型的定义《流畅的 Python》:在编译时检查类型的语言是静态类型语言,在运行时检查类型的语言是动态类型语言。静态语言需要声明类型(一些现代语言使用类型推断来避免部分类型声明)。综上所述,Python是一门动态强类型语言,这是比较明显的,没有争议。TypeHints探索PythonTypeHints(类型注释)在PEP484(PythonEnhancementProposals,Python增强提案)[https://www.python.org/dev/peps/pep-0484/]中提出。进一步强化了Python作为强类型语言的特性,Python3.5首次引入。使用TypeHints可以让我们写出带有类型的Python代码,看起来更符合强类型语言的风格。这里定义了两个问候函数:正常的写法如下:name="world"defgreeting(name):return"Hello"+namegreeting(name)添加TypeHints的写法如下:name:str="world"defgreeting(name:str)->str:return"Hello"+namegreeting(name)添加了TypeHints写法如下:name:str="world"defgreeting(name:str)->str:return"Hello"+namegreeting(name)以PyCharm为例,在编写代码的过程中,IDE会根据函数的类型注解检查传递给函数的参数的类型。如果发现实参类型与函数的形参类型标签不匹配,则会出现如下提示:TypeHintsofcommondatastructuresTypeHints的用法如上文通过一个问候函数所示。接下来,我们将用Python编写常见数据结构的TypeHints。进行更深入的研究。默认参数Python函数支持默认参数。下面是默认参数的TypeHints写法。你只需要在变量和默认参数之间写上类型。defgreeting(name:str="world")->str:return"Hello"+namegreeting()CustomType对于自定义类型,TypeHints也可以很好地支持。它的编写方式与Python的内置类型没有什么不同。classStudent(object):def__init__(self,name,age):self.name=nameself.age=agedefstudent_to_string(s:Student)->str:returnf"studentname:{s.name},age:{s.age}。"student_to_string(Student("Tim",18))当类型被标记为自定义类型时,IDE也可以检查类型。容器类型当我们尝试向内置容器类型添加类型注释时,它会抛出语法错误,因为类型注释运算符[]表示Python中的切片操作。因此,不能直接使用内置容器类型作为注解,需要从typing模块导入对应的容器类型注解(通常内置类型首字母大写)。fromtypingimportList,Tuple,Dictl:List[int]=[1,2,3]t:Tuple[str,...]=("a","b")d:Dict[str,int]={"a":1,"b":2,}不过PEP585[https://www.python.org/dev/peps/pep-0585/]的出现解决了这个问题,我们可以直接使用Python自带的-在类型中,没有语法错误。l:list[int]=[1,2,3]t:tuple[str,...]=("a","b")d:dict[str,int]={"a":1,"b":2,}Typealias一些复杂的嵌套类型写起来很长,如果有重复会很痛苦,而且代码不够干净。配置:list[tuple[str,int],dict[str,str]]=[("127.0.0.1",8080),{"MYSQL_DB":"db","MYSQL_USER":"user","MYSQL_PASS":"pass","MYSQL_HOST":"127.0.0.1","MYSQL_PORT":"3306",},]defstart_server(config:list[tuple[str,int],dict[str,str]])->None:...start_server(config)这时候可以通过给类型起个别名来解决,类似于变量命名。Config=list[tuple[str,int],dict[str,str]]config:Config=[("127.0.0.1",8080),{"MYSQL_DB":"db","MYSQL_USER":"user","MYSQL_PASS":"pass","MYSQL_HOST":"127.0.0.1","MYSQL_PORT":"3306",},]defstart_server(config:Config)->None:...start_server(config)这段代码看起来像舒服多了。可变参数Python函数的一个非常灵活的特性是支持可变参数。类型提示还支持可变参数类型注释。defffoo(*args:str,**kwargs:int)->None:...foo("a","b",1,x=2,y="c")IDE仍然可以检查它。在动态语言中使用泛型需要泛型的支持,而TypeHints也为泛型提供了多种解决方案。TypeVar使用TypeVar接受任意类型。fromtypingimportTypeVarT=TypeVar("T")deffoo(*args:T,**kwargs:T)->None:...foo("a","b",1,x=2,y="c")Union如果你不想使用泛型,而只想使用几个指定的类型,你可以使用Union来完成。例如,定义concat函数只接收str或bytes类型。fromtypingimportUnionT=Union[str,bytes]defconcat(s1:T,s2:T)->T:returns1+s2concat("hello","world")concat(b"hello",b"world")concat("hello",b"world")concat(b"hello","world")IDE的检查提示如下:TypeVar和Union的区别TypeVar不仅可以接收泛型,也可以像Union一样使用,只需要被实例化只要依次传入你想指定的类型范围作为参数即可。与Union不同的是,用TypeVar声明的函数必须具有相同的多参数类型,而Union没有限制。fromtypingimportTypeVarT=TypeVar("T",str,bytes)defconcat(s1:T,s2:T)->T:returns1+s2concat("hello","world")concat(b"hello",b"world")concat("hello",b"world")下面是使用TypeVar作为限定类型时的IDE提示:X,EitherNone,Optional[X]等价于Union[X,None]。fromtypingimportOptional,Union#None=>type(None)deffoo(arg:Union[int,None]=None)->None:...deffoo(arg:Optional[int]=None)->None:...AnyAny是一种可以表示所有类型的特殊类型。不指定返回值和参数类型的函数默认隐式使用Any,所以下面两个问候函数写法等价:fromtypingimportAnydefgreeting(name):return"Hello"+namedefgreeting(name:Any)->Any:return"Hello"+name当我们想使用TypeHints来实现静态类型,又不想失去动态语言特有的灵活性时,我们可以使用Any。当Any类型的值赋值给更精确的类型时,不进行类型检查,下面代码IDE不会给出错误信息:fromtypingimportAnya:Any=Nonea=[]#Dynamiclanguagefeaturea=2s:str=''s=a#任何类型的值都被赋值给更精确类型的可调用对象(函数、类等)Python中的任何可调用类型都可以用Callable注解。在下面的代码注解中,Callable[[int],str],[int]表示可调用类型的参数列表,str表示返回值。fromtypingimportCallabledefint_to_str(i:int)->str:returnstr(i)deff(fn:Callable[[int],str],i:int)->str:returnfn(i)f(int_to_str,2)自引用当我们need在定义树结构时,往往需要自引用。执行__init__方法时,还没有生成Tree类型,所以不能像内置类型str那样直接注解,需要用字符串形式“Tree”来引用未生成的对象。classTree(object):def__init__(self,left:"Tree"=None,right:"Tree"=None):self.left=leftself.right=righttree1=Tree(Tree(),Tree())IDE也可以Self-检查引用类型。这种形式不仅可以用于自引用,也可以用于前向引用。鸭子类型Python的一个显着特点是它广泛使用鸭子类型。TypeHints提供协议来支持ducktyping。在定义一个类的时候,只需要继承Protocol声明一个接口类型即可。当遇到接口类型的注解时,只要接收到的对象实现了接口类型的所有方法,就可以通过类型注解的检查,IDE不会报错。这里的Stream不需要显式继承Interface类,只需要实现close方法即可。fromtypingimportProtocolclassInterface(Protocol):defclose(self)->None:...#classStream(Interface):classStream:defclose(self)->None:...defclose_resource(r:Interface)->None:r.close()f=open("a.txt")close_resource(f)s:StreamStream=Stream()close_resource(s)由于文件对象和内置open函数返回的Stream对象都实现了close方法,所以可以通过TypeHints检查,字符串“s”没有实现close方法,所以IDE会提示类型错误。其他写类型提示的方法事实上,写类型提示的方法不止一种。Python为了兼容不同人的喜好和旧代码的迁移,实现了另外两种写法。让我们看一个使用注释编写的tornado框架(tornado/web.py)的示例。适用于修改现有项目,代码已经写好,后期需要添加类型注解。使用单独的文件写入(.pyi)可以在源代码的同一目录下新建一个与.py同名的.pyi文件,IDE还可以自动进行类型检查。这样做的好处是可以把原来的代码完全解耦,不需要做任何改动。缺点是相当于同时维护两份代码。TypeHints实践基本把日常编码中常用的TypeHints的写法介绍给大家了,下面就让我们看看在实际编码中如何应用TypeHints吧。dataclass-数据类dataclass是一个装饰器,它可以装饰类,用来给类添加魔法方法,比如__init__()和__repr__()等,它在PEP557[https://www.python.org/dev/peps/pep-0557/]被定义。fromdataclassesimportdataclass,field@dataclassclassUser(object):id:intname:strfriends:list[int]=field(default_factory=list)data={"id":123,"name":"Tim",}user=User(**data)print(user.id,user.name,user.friends)#>123Tim[]上面使用dataclass写的代码相当于下面的代码:classUser(object):def__init__(self,id:int,name:str,friends=None):self.id=idself.name=nameself.friends=friendsor[]data={"id":123,"name":"Tim",}user=User(**data)print(user.id,user.name,user.friends)#>123Tim[]注意:dataclass不检查字段类型。可以发现使用dataclass来写类可以减少很多重复的样板代码,语法也更加清晰。PydanticPydantic是一个基于PythonTypeHints的第三方库。它提供数据验证、序列化和文档功能。是值得学习的图书馆。以下是使用Pydantic的示例代码:fromdatetimeimportdatetimefromtypingimportOptionalfrompydanticimportBaseModelclassUser(BaseModel):id:intname='JohnDoe'signup_ts:Optional[datetime]=Nonefriends:list[int]=[]external_data={'id':'123','signup_ts':'2021-09-0217:00','friends':[1,2,'3'],}user=User(**external_data)print(user.id)#>123print(repr(user.signup_ts))#>datetime.datetime(2021,9,2,17,0)print(user.friends)#>[1,2,3]print(user.dict())"""{'id':123,'signup_ts':datetime.datetime(2021,9,2,17,0),'friends':[1,2,3],'name':'JohnDoe',}"""注意:Pydantic会将强制执行字段类型检查。Pydantic在写法上和dataclass很像,但是额外做了更多的工作,还提供了.dict()等非常方便的方法。让我们看一个Pydantic数据验证的例子。当User类接收到的参数不符合预期时,会抛出ValidationError异常。异常对象提供了.json()方法来检查异常的原因。frompydanticimportValidationErrortry:User(signup_ts='broken',friends=[1,2,'notnumber'])exceptValidationErrorase:print(e.json())"""[{"loc":["id"],"msg":"fieldrequired","type":"value_error.missing"},{"loc":["signup_ts"],"msg":"invaliddatetimeformat","type":"value_error.datetime"},{"loc":["friends",2],"msg":"valueisnotvalidinteger","type":"type_error.integer"}]"""所有错误信息存储在一个列表中,每个字段的错误信息存储在一个embedded在set的dict中,loc标识了异常的字段和错误的位置,msg是错误信息,type是错误的类型,这样一目了然错误的原因。MySQLHandlerMySQLHandler[https://github.com/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]是我对pymysql库的封装,支持使用with语法调用execute方法,查询结果从tuple中将其替换为object也是TypeHints的一种应用。classMySQLHandler(object):"""MySQLhandler"""def__init__(self):self.conn=pymysql.connect(host=DB_HOST,port=DB_PORT,user=DB_USER,password=DB_PASS,database=DB_NAME,charset=DB_CHARSET,client_flag=CLIENT.MULTI_STATEMENTS,#executemultisqlstatements)selfself.cursor=self.conn.cursor()def__del__(self):self.cursor.close()self.conn.close()@contextmanagerdefexecute(self):try:yieldself.cursor。executeself.conn.commit()exceptExceptionase:self.conn.rollback()logging.exception(e)@contextmanagerdefexecutemany(self):尝试:yieldself.cursor.executemanyself.conn.commit()exceptExceptionase:self.conn.rollback()logging.exception(e)def_tuple_to_object(self,data:List[tuple])->List[FetchObject]:obj_list=[]attrs=[desc[0]fordescinself.cursor.description]foriindata:obj=FetchObject()forattr,valueinzip(attrs,i):setattr(obj,attr,value)obj_list.append(obj)returnobj_listdeffetchone(self)->可选[FetchObject]:result=self.cursor.fetchone()returnself._tuple_to_object([result])[0]ifresultelseNonedeffetchmany(self,size:Optional[int]=None)->Optional[List[FetchObject]]:result=self.cursor.fetchmany(size)returnself._tuple_to_object(结果)ifresultelseNonedeffetchall(self)->Optional[List[FetchObject]]:result=self.cursor.fetchall()returnsself._tuple_to_object(result)ifresultelseNoneruntimetypecheckTypeHints之所以叫Hints而不是Check是因为它是只是一个类型上面演示的TypeHints的用法其实就是IDE帮助我们完成类型检查功能的功能,但实际上IDE的类型检查并不能判断代码执行过程中是否报错,只能在staticperiod实现语法检查提示的功能。要在代码执行时强制进行类型检查,我们需要自己编写代码或者引入第三方库(比如上面介绍的Pydantic)。下面我实现了一个type_check函数来在运行时动态检查类型,供大家参考:frominspectimportgetfullargspecfromfunctoolsimportwrapsfromtypingimportget_type_hintsdeftype_check(fn):@wraps(fn)defwrapper(*args,**kwargs):fn_args=getfullargspec(fn)[0]kwargs。更新(dict(zip(fn_args,args)))hints=get_type_hints(fn)hints.pop("return",None)forname,type_inhints.items():ifnotisinstance(kwargs[name],type_):raiseTypeError(f"预期{type_.__name__},得到{type(kwargs[name]).__name__}instead")returnfn(**kwargs)returnwrapper#name:str="world"name:int=2@type_checkdefgreeting(name:str)->str:returnstr(name)print(greeting(name))#>TypeError:expectedstr,gotininstead只要在greeting函数上加上type_check装饰器,就可以实现运行时类型检查。