Python 工匠:做一個(gè)精通規(guī)則的玩家(Python是什么意思)

      網(wǎng)友投稿 760 2025-04-03

      今天還是給大家推薦一篇 Python 優(yōu)質(zhì)文章,主要講解 Python? 中我們應(yīng)該注意的一些規(guī)則。熟悉規(guī)則,并讓自己的代碼適應(yīng)這些規(guī)則,可以幫助我們寫(xiě)出更地道的代碼,事半功倍地完成工作。

      轉(zhuǎn)載來(lái)源

      閱讀本文大概需要 9 分鐘。

      前言

      編程,其實(shí)和玩電子游戲有一些相似之處。你在玩不同游戲前,需要先學(xué)習(xí)每個(gè)游戲的不同規(guī)則,只有熟悉和靈活運(yùn)用游戲規(guī)則,才更有可能在游戲中獲勝。

      而編程也是一樣,不同編程語(yǔ)言同樣有著不一樣的“規(guī)則”。大到是否支持面向?qū)ο螅〉绞欠窨梢远x常量,編程語(yǔ)言的規(guī)則比絕大多數(shù)電子游戲要復(fù)雜的多。

      當(dāng)我們編程時(shí),如果直接拿一種語(yǔ)言的經(jīng)驗(yàn)套用到另外一種語(yǔ)言上,很多時(shí)候并不能取得最佳結(jié)果。這就好像一個(gè) CS(反恐精英) 高手在不了解規(guī)則的情況下去玩 PUBG(絕地求生),雖然他的槍法可能萬(wàn)中無(wú)一,但是極有可能在發(fā)現(xiàn)第一個(gè)敵人前,他就會(huì)倒在某個(gè)窩在草叢里的敵人的伏擊下。

      Python 里的規(guī)則

      Python 是一門(mén)初見(jiàn)簡(jiǎn)單、深入后愈覺(jué)復(fù)雜的語(yǔ)言。拿 Python 里最重要的“對(duì)象”概念來(lái)說(shuō),Python 為其定義了多到讓你記不全的規(guī)則,比如:

      定義了?__str__?方法的對(duì)象,就可以使用?str()?函數(shù)來(lái)返回可讀名稱

      定義了?__next__?和?__iter__?方法的對(duì)象,就可以被循環(huán)迭代

      定義了?__bool__?方法的對(duì)象,在進(jìn)行布爾判斷時(shí)就會(huì)使用自定義的邏輯

      ... ...

      熟悉規(guī)則,并讓自己的代碼適應(yīng)這些規(guī)則,可以幫助我們寫(xiě)出更地道的代碼,事半功倍的完成工作。下面,讓我們來(lái)看一個(gè)有關(guān)適應(yīng)規(guī)則的故事。

      案例:從兩份旅游數(shù)據(jù)中獲取人員名單

      某日,在一個(gè)主打新西蘭出境游的旅游公司里,商務(wù)同事突然興沖沖的跑過(guò)來(lái)找到我,說(shuō)他從某合作伙伴那里,要到了兩份重要的數(shù)據(jù):

      所有去過(guò)“泰國(guó)普吉島”的人員及聯(lián)系方式

      所有去過(guò)“新西蘭”的人員及聯(lián)系方式

      數(shù)據(jù)采用了 JSON 格式,如下所示:

      # 去過(guò)普吉島的人員數(shù)據(jù)

      users_visited_phuket = [

      {"first_name": "Sirena", "last_name": "Gross", "phone_number": "650-568-0388", "date_visited": "2018-03-14"},

      {"first_name": "James", "last_name": "Ashcraft", "phone_number": "412-334-4380", "date_visited": "2014-09-16"},

      ... ...

      ]

      # 去過(guò)新西蘭的人員數(shù)據(jù)

      users_visited_nz = [

      {"first_name": "Justin", "last_name": "Malcom", "phone_number": "267-282-1964", "date_visited": "2011-03-13"},

      {"first_name": "Albert", "last_name": "Potter", "phone_number": "702-249-3714", "date_visited": "2013-09-11"},

      ... ...

      ]

      每份數(shù)據(jù)里面都有著 姓、 名、 手機(jī)號(hào)碼、 旅游時(shí)間 四個(gè)字段。基于這份數(shù)據(jù),商務(wù)同學(xué)提出了一個(gè)(聽(tīng)上去毫無(wú)道理)的假設(shè):“去過(guò)普吉島的人,應(yīng)該對(duì)去新西蘭旅游也很有興趣。我們需要從這份數(shù)據(jù)里,找出那些去過(guò)普吉島但沒(méi)有去過(guò)新西蘭的人,針對(duì)性的賣產(chǎn)品給他們。

      第一次蠻力嘗試

      有了原始數(shù)據(jù)和明確的需求,接下來(lái)的問(wèn)題就是如何寫(xiě)代碼了。依靠蠻力,我很快就寫(xiě)出了第一個(gè)方案:

      def find_potential_customers_v1():

      """找到去過(guò)普吉島但是沒(méi)去過(guò)新西蘭的人

      """

      for phuket_record in users_visited_phuket:

      is_potential = True

      for nz_record in users_visited_nz:

      if phuket_record['first_name'] == nz_record['first_name'] and

      phuket_record['last_name'] == nz_record['last_name'] and

      phuket_record['phone_number'] == nz_record['phone_number']:

      is_potential = False

      break

      if is_potential:

      yield phuket_record

      因?yàn)樵紨?shù)據(jù)里沒(méi)有“用戶 ID”之類的唯一標(biāo)示,所以我們只能把“姓名和電話號(hào)碼完全相同”作為判斷是不是同一個(gè)人的標(biāo)準(zhǔn)。

      find_potential_customers_v1 函數(shù)通過(guò)循環(huán)的方式,先遍歷所有去過(guò)普吉島的人,然后再遍歷新西蘭的人,如果在新西蘭的記錄中找不到完全匹配的記錄,就把它當(dāng)做“潛在客戶”返回。

      這個(gè)函數(shù)雖然可以完成任務(wù),但是相信不用我說(shuō)你也能發(fā)現(xiàn)。它有著非常嚴(yán)重的性能問(wèn)題。對(duì)于每一條去過(guò)普吉島的記錄,我們都需要遍歷所有新西蘭訪問(wèn)記錄,嘗試找到匹配。整個(gè)算法的時(shí)間復(fù)雜度是可怕的 O(n*m),如果新西蘭的訪問(wèn)條目數(shù)很多的話,那么執(zhí)行它將耗費(fèi)非常長(zhǎng)的時(shí)間。

      為了優(yōu)化內(nèi)層循環(huán)性能,我們需要減少線性查找匹配部分的開(kāi)銷。

      嘗試使用集合優(yōu)化函數(shù)

      如果你對(duì) Python 有所了解的話,那么你肯定知道,Python 里的字典和集合對(duì)象都是基于 哈希表(Hash Table) 實(shí)現(xiàn)的。判斷一個(gè)東西是不是在集合里的平均時(shí)間復(fù)雜度是 O(1),非常快。

      所以,對(duì)于上面的函數(shù),我們可以先嘗試針對(duì)新西蘭訪問(wèn)記錄初始化一個(gè)集合,之后的查找匹配部分就可以變得很快,函數(shù)整體時(shí)間復(fù)雜度就能變?yōu)?O(n+m)。

      讓我們看看新的函數(shù):

      def find_potential_customers_v2():

      """找到去過(guò)普吉島但是沒(méi)去過(guò)新西蘭的人,性能改進(jìn)版

      """

      # 首先,遍歷所有新西蘭訪問(wèn)記錄,創(chuàng)建查找索引

      nz_records_idx = {

      (rec['first_name'], rec['last_name'], rec['phone_number'])

      for rec in users_visited_nz

      }

      for rec in users_visited_phuket:

      key = (rec['first_name'], rec['last_name'], rec['phone_number'])

      if key not in nz_records_idx:

      yield rec

      使用了集合對(duì)象后,新函數(shù)在速度上相比舊版本有了飛躍性的突破。但是,對(duì)這個(gè)問(wèn)題的優(yōu)化并不是到此為止,不然文章標(biāo)題就應(yīng)該改成:“如何使用集合提高程序性能” 了。

      對(duì)問(wèn)題的重新思考

      讓我們來(lái)嘗試重新抽象思考一下問(wèn)題的本質(zhì)。首先,我們有一份裝了很多東西的容器 A(普吉島訪問(wèn)記錄),然后給我們另一個(gè)裝了很多東西的容器 B(新西蘭訪問(wèn)記錄),之后定義相等規(guī)則:“姓名與電話一致”。最后基于這個(gè)相等規(guī)則,求 A 和 B 之間的“差集”。

      如果你對(duì) Python 里的集合不是特別熟悉,我就稍微多介紹一點(diǎn)。假如我們擁有兩個(gè)集合 A 和 B,那么我們可以直接使用 A-B 這樣的數(shù)學(xué)運(yùn)算表達(dá)式來(lái)計(jì)算二者之間的 差集。

      >>>?a?=?{1,?3,?5,?7}>>>?b?=?{3,?5,?8}#?產(chǎn)生新集合:所有在?a?但是不在?b?里的元素>>>?a?-?b{1,?7}

      所以,計(jì)算“所有去過(guò)普吉島但沒(méi)去過(guò)新西蘭的人”,其實(shí)就是一次集合的求差值操作。那么要怎么做,才能把我們的問(wèn)題套入到集合的游戲規(guī)則里去呢?

      利用集合的游戲規(guī)則

      在 Python 中,如果要把某個(gè)東西裝到集合或字典里,一定要滿足一個(gè)基本條件:“這個(gè)東西必須是可以被哈希(Hashable)的” 。什么是 “Hashable”?

      舉個(gè)例子,Python 里面的所有可變對(duì)象,比如字典,就 不是 Hashable 的。當(dāng)你嘗試把字典放入集合中時(shí),會(huì)發(fā)生這樣的錯(cuò)誤:

      >>>?s?=?set()>>>?s.add({'foo':?'bar'})Traceback?(most?recent?call?last):??File?"",?line?1,?in?TypeError:?unhashable?type:?'dict'

      所以,如果要利用集合解決我們的問(wèn)題,就首先得定義我們自己的 “Hashable” 對(duì)象:VisitRecord。而要讓一個(gè)自定義對(duì)象變得 Hashable,唯一要做的事情就是定義對(duì)象的 __hash__ 方法。

      class?VisitRecord:????"""旅游記錄????"""????def?__init__(self,?first_name,?last_name,?phone_number,?date_visited):????????self.first_name?=?first_name????????self.last_name?=?last_name????????self.phone_number?=?phone_number????????self.date_visited?=?date_visited

      一個(gè)好的哈希算法,應(yīng)該讓不同對(duì)象之間的值盡可能的唯一,這樣可以最大程度減少“哈希碰撞”發(fā)生的概率,默認(rèn)情況下,所有 Python 對(duì)象的哈希值來(lái)自它的內(nèi)存地址。

      在這個(gè)問(wèn)題里,我們需要自定義對(duì)象的 __hash__ 方法,讓它利用 (姓,名,電話)元組作為 VisitRecord 類的哈希值來(lái)源。

      def?__hash__(self):????return?hash(????????(self.first_name,?self.last_name,?self.phone_number)????)

      自定義完 __hash__ 方法后, VisitRecord 實(shí)例就可以正常的被放入集合中了。但這還不夠,為了讓前面提到的求差值算法正常工作,我們還需要實(shí)現(xiàn) __eq__ 特殊方法。

      __eq__ 是 Python 在判斷兩個(gè)對(duì)象是否相等時(shí)調(diào)用的特殊方法。默認(rèn)情況下,它只有在自己和另一個(gè)對(duì)象的內(nèi)存地址完全一致時(shí),才會(huì)返回 True。但是在這里,我們復(fù)用了 VisitRecord 對(duì)象的哈希值,當(dāng)二者相等時(shí),就認(rèn)為它們一樣。

      def?__eq__(self,?other):????#?當(dāng)兩條訪問(wèn)記錄的名字與電話號(hào)相等時(shí),判定二者相等。????if?isinstance(other,?VisitRecord)?and?hash(other)?==?hash(self):????????return?True????return?False

      完成了恰當(dāng)?shù)臄?shù)據(jù)建模后,之后的求差值運(yùn)算便算是水到渠成了。新版本的函數(shù)只需要一行代碼就能完成操作:

      def?find_potential_customers_v3():????return?set(VisitRecord(**r)?for?r?in?users_visited_phuket)?-?????????set(VisitRecord(**r)?for?r?in?users_visited_nz)

      Hint:如果你使用的是 Python 2,那么除了 __eq__ 方法外,你還需要自定義類的 __ne__(判斷不相等時(shí)使用) 方法。

      使用 dataclass 簡(jiǎn)化代碼

      故事到這里并沒(méi)有結(jié)束。在上面的代碼里,我們手動(dòng)定義了自己的 數(shù)據(jù)類 VisitRecord,實(shí)現(xiàn)了 __init__、 __eq__ 等初始化方法。但其實(shí)還有更簡(jiǎn)單的做法。

      因?yàn)槎x數(shù)據(jù)類這種需求在 Python 中實(shí)在太常見(jiàn)了,所以在 3.7 版本中,標(biāo)準(zhǔn)庫(kù)中新增了 dataclasses 模塊,專門(mén)幫你簡(jiǎn)化這類工作。

      如果使用 dataclasses 提供的特性,我們的代碼可以最終簡(jiǎn)化成下面這樣:

      @dataclass(unsafe_hash=True)

      class VisitRecordDC:

      first_name: str

      last_name: str

      phone_number: str

      # 跳過(guò)“訪問(wèn)時(shí)間”字段,不作為任何對(duì)比條件

      date_visited: str = field(hash=False, compare=False)

      def find_potential_customers_v4():

      return set(VisitRecordDC(**r) for r in users_visited_phuket) -

      set(VisitRecordDC(**r) for r in users_visited_nz)

      不用干任何臟活累活,只要不到十行代碼就完成了工作。

      案例總結(jié)

      問(wèn)題解決以后,讓我們?cè)僮鲆稽c(diǎn)小小的總結(jié)。在處理這個(gè)問(wèn)題時(shí),我們一共使用了三種方案:

      使用普通的兩層循環(huán)篩選符合規(guī)則的結(jié)果集

      利用哈希表結(jié)構(gòu)(set 對(duì)象)創(chuàng)建索引,提升處理效率

      將數(shù)據(jù)轉(zhuǎn)換為自定義對(duì)象,利用規(guī)則,直接使用集合運(yùn)算

      為什么第三種方式會(huì)比前面兩種好呢?

      首先,第一個(gè)方案的性能問(wèn)題過(guò)于明顯,所以很快就會(huì)被放棄。那么第二個(gè)方案呢?仔細(xì)想想看,方案二其實(shí)并沒(méi)有什么明顯的缺點(diǎn)。甚至和第三個(gè)方案相比,因?yàn)樯倭俗远x對(duì)象的過(guò)程,它在性能與內(nèi)存占用上,甚至有可能會(huì)微微強(qiáng)于后者。

      但請(qǐng)?jiān)偎伎家幌拢绻惆逊桨付拇a換成另外一種語(yǔ)言,比如 Java,它是不是基本可以做到 1:1 的完全翻譯?換句話說(shuō),它雖然效率高、代碼直接,但是它沒(méi)有完全利用好 Python 世界提供的規(guī)則,最大化的從中受益。

      如果要具體化這個(gè)問(wèn)題里的“規(guī)則”,那就是 “Python 擁有內(nèi)置結(jié)構(gòu)集合,集合之間可以進(jìn)行差值等四則運(yùn)算” 這個(gè)事實(shí)本身。匹配規(guī)則后編寫(xiě)的方案三代碼擁有下面這些優(yōu)勢(shì):

      為數(shù)據(jù)建模后,可以更方便的定義其他方法

      如果需求變更,做反向差值運(yùn)算、求交集運(yùn)算都很簡(jiǎn)單

      理解集合與 dataclasses 邏輯后,代碼遠(yuǎn)比其他版本更簡(jiǎn)潔清晰

      如果要修改相等規(guī)則,比如“只擁有相同姓的記錄就算作一樣”,只需要繼承?VisitRecord?覆蓋?__eq__?方法即可

      其他規(guī)則如何影響我們

      在前面,我們花了很大的篇幅講了如何利用“集合的規(guī)則”來(lái)編寫(xiě)事半功倍的代碼。除此之外,Python 世界中還有著很多其他規(guī)則。如果能熟練掌握這些規(guī)則,就可以設(shè)計(jì)出符合 Python 慣例的 API,讓代碼更簡(jiǎn)潔精煉。

      下面是兩個(gè)具體的例子。

      使用?__format__?做對(duì)象字符串格式化

      如果你的自定義對(duì)象需要定義多種字符串表示方式,就像下面這樣:

      class Student:

      def __init__(self, name, age):

      self.name = name

      self.age = age

      def get_simple_display(self):

      return f'{self.name}({self.age})'

      def get_long_display(self):

      return f'{self.name} is {self.age} years old.'

      piglei = Student('piglei', '18')

      # OUTPUT: piglei(18)

      print(piglei.get_simple_display())

      # OUTPUT: piglei is 18 years old.

      print(piglei.get_long_display())

      那么除了增加這種 get_xxx_display() 額外方法外,你還可以嘗試自定義 Student 類的 __format__ 方法,因?yàn)槟遣攀菍?duì)象變?yōu)樽址臉?biāo)準(zhǔn)規(guī)則。

      class Student:

      def __init__(self, name, age):

      self.name = name

      self.age = age

      def __format__(self, format_spec):

      if format_spec == 'long':

      return f'{self.name} is {self.age} years old.'

      elif format_spec == 'simple':

      return f'{self.name}({self.age})'

      raise ValueError('invalid format spec')

      piglei = Student('piglei', '18')

      print('{0:simple}'.format(piglei))

      print('{0:long}'.format(piglei))

      使用?__getitem__?定義對(duì)象切片操作

      如果你要設(shè)計(jì)某個(gè)可以裝東西的容器類型,那么你很可能會(huì)為它定義“是否為空”、“獲取第 N 個(gè)對(duì)象”等方法:

      class Events:

      def __init__(self, events):

      self.events = events

      def is_empty(self):

      return not bool(self.events)

      def list_events_by_range(self, start, end):

      return self.events[start:end]

      events = Events([

      'computer started',

      'os launched',

      'docker started',

      'os stopped',

      ])

      # 判斷是否有內(nèi)容,打印第二個(gè)和第三個(gè)對(duì)象

      if not events.is_empty():

      print(events.list_events_by_range(1, 3))

      但是,這樣并非最好的做法。因?yàn)?Python 已經(jīng)為我們提供了一套對(duì)象規(guī)則,所以我們不需要像寫(xiě)其他語(yǔ)言的 OO(面向?qū)ο螅?代碼那樣去自己定義額外方法。我們有更好的選擇:

      class Events:

      Python 工匠:做一個(gè)精通規(guī)則的玩家(python是什么意思)

      def __init__(self, events):

      self.events = events

      def __len__(self):

      """自定義長(zhǎng)度,將會(huì)被用來(lái)做布爾判斷"""

      return len(self.events)

      def __getitem__(self, index):

      """自定義切片方法"""

      # 直接將 slice 切片對(duì)象透?jìng)鹘o events 處理

      return self.events[index]

      # 判斷是否有內(nèi)容,打印第二個(gè)和第三個(gè)對(duì)象

      if events:

      print(events[1:3])

      新的寫(xiě)法相比舊代碼,更能適配進(jìn) Python 世界的規(guī)則,API 也更為簡(jiǎn)潔。

      關(guān)于如何適配規(guī)則、寫(xiě)出更好的 Python 代碼。Raymond Hettinger 在 PyCon 2015 上有過(guò)一次非常精彩的演講 “Beyond PEP8 - Best practices for beautiful intelligible code”。這次演講長(zhǎng)期排在我個(gè)人的 “PyCon 視頻 TOP5” 名單上,如果你還沒(méi)有看過(guò),我強(qiáng)烈建議你現(xiàn)在就去看一遍 :)

      Hint:更全面的 Python 對(duì)象模型規(guī)則可以在 官方文檔 找到,有點(diǎn)難讀,但值得一讀。

      總結(jié)

      Python 世界有著一套非常復(fù)雜的規(guī)則,這些規(guī)則的涵蓋范圍包括“對(duì)象與對(duì)象是否相等“、”對(duì)象與對(duì)象誰(shuí)大誰(shuí)小”等等。它們大部分都需要通過(guò)重新定義“雙下劃線方法 __xxx__” 去實(shí)現(xiàn)。

      如果熟悉這些規(guī)則,并在日常編碼中活用它們,有助于我們更高效的解決問(wèn)題、設(shè)計(jì)出更符合 Python 哲學(xué)的 API。下面是本文的一些要點(diǎn)總結(jié):

      永遠(yuǎn)記得對(duì)原始需求做抽象分析,比如問(wèn)題是否能用集合求差集解決

      如果要把對(duì)象放入集合,需要自定義對(duì)象的?__hash__?與?__eq__?方法

      __hash__?方法決定性能(碰撞出現(xiàn)概率),?__eq__?決定對(duì)象間相等邏輯

      使用 dataclasses 模塊可以讓你少寫(xiě)很多代碼

      使用?__format__?方法替代自己定義的字符串格式化方法

      在容器類對(duì)象上使用?__len__、?__getitem__?方法,而不是自己實(shí)現(xiàn)

      看完文章的你,有沒(méi)有什么想吐槽的?請(qǐng)留言或者在 項(xiàng)目 Github Issues 告訴我吧。

      Python 存儲(chǔ)

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:內(nèi)網(wǎng)穿透工具的原理與開(kāi)發(fā)實(shí)戰(zhàn)
      下一篇:不只是光刻機(jī),還有這一半導(dǎo)體材料依賴進(jìn)口
      相關(guān)文章
      97久久国产亚洲精品超碰热| 亚洲人成网站18禁止| 日产亚洲一区二区三区| 亚洲精品成人在线| 国产亚洲中文日本不卡二区| 亚洲AV一二三区成人影片| 亚洲午夜国产精品| 亚洲国产精品久久久久秋霞影院| 亚洲综合久久1区2区3区| 亚洲福利电影一区二区?| 亚洲第一成年网站大全亚洲| 亚洲国产模特在线播放| 国产成人精品日本亚洲直接| 亚洲三级在线观看| 亚洲人成人网站18禁| 亚洲国产美女精品久久久| 亚洲av无码专区首页| 亚洲成av人片在线天堂无| 亚洲变态另类一区二区三区 | 亚洲AV成人无码网天堂| 亚洲AV无码成人精品区狼人影院| 亚洲爆乳AAA无码专区| 色婷婷六月亚洲综合香蕉| 国产精品亚洲专区无码不卡| 亚洲国产日韩成人综合天堂| 久久精品国产亚洲一区二区三区 | 亚洲毛片基地4455ww| 亚洲日本VA中文字幕久久道具| 亚洲精品无码永久在线观看男男| 亚洲av永久无码精品秋霞电影秋| 小说区亚洲自拍另类| 亚洲伊人久久综合中文成人网| 国产亚洲av片在线观看18女人| 亚洲欧洲∨国产一区二区三区 | 亚洲第一街区偷拍街拍| 亚洲AV无码专区日韩| 亚洲日韩国产精品第一页一区| 久久精品亚洲中文字幕无码网站| 亚洲狠狠ady亚洲精品大秀| 亚洲Av高清一区二区三区| 亚洲国产精品久久久久秋霞小|