ทำไม Python จึงใช้ 'magic method'?


99

เมื่อเร็ว ๆ นี้ฉันได้เล่นกับ Python และสิ่งหนึ่งที่ฉันพบว่าแปลกนิดหน่อยคือการใช้ 'วิธีการมายากล' อย่างกว้างขวางเช่นเพื่อให้มีความยาววัตถุใช้วิธีการdef __len__(self)และจากนั้นจะเรียกเมื่อ len(obj)คุณเขียน

ฉันแค่สงสัยว่าทำไมอ็อบเจกต์ไม่เพียงแค่กำหนดlen(self)เมธอดและเรียกมันว่าเป็นสมาชิกของอ็อบเจ็กต์โดยตรงเช่นobj.len()? ฉันแน่ใจว่าต้องมีเหตุผลที่ดีที่ Python จะทำเช่นนั้น แต่ในฐานะมือใหม่ฉันยังไม่ได้รู้ว่ามันคืออะไร


4
ฉันคิดว่าเหตุผลทั่วไปคือ a) ประวัติศาสตร์และ b) บางอย่างที่เหมือนlen()หรือreversed()ใช้กับวัตถุหลายประเภท แต่วิธีการเช่นappend()ใช้กับลำดับเท่านั้นเป็นต้น
Grant Paul

คำตอบ:


64

AFAIK lenมีความพิเศษในแง่นี้และมีรากฐานทางประวัติศาสตร์

นี่คือคำพูดจากคำถามที่พบบ่อย :

เหตุใด Python จึงใช้เมธอดสำหรับฟังก์ชันบางอย่าง (เช่น list.index ()) แต่ฟังก์ชันอื่น ๆ (เช่น len (รายการ))

เหตุผลสำคัญคือประวัติศาสตร์ ฟังก์ชั่นถูกใช้สำหรับการดำเนินการที่เป็นแบบทั่วไปสำหรับกลุ่มประเภทและมีจุดมุ่งหมายเพื่อใช้งานได้แม้กระทั่งกับออบเจ็กต์ที่ไม่มีเมธอดเลย (เช่นทูเปิล) นอกจากนี้ยังสะดวกที่จะมีฟังก์ชันที่สามารถนำไปใช้กับคอลเลกชันอสัณฐานได้อย่างง่ายดายเมื่อคุณใช้คุณสมบัติการทำงานของ Python (map (), apply () et al)

ในความเป็นจริงการนำ len (), max (), min () มาใช้เป็นฟังก์ชันในตัวนั้นมีโค้ดน้อยกว่าการใช้เป็นวิธีการสำหรับแต่ละประเภท เราสามารถเล่นลิ้นเกี่ยวกับแต่ละกรณีได้ แต่เป็นส่วนหนึ่งของ Python และมันสายเกินไปที่จะทำการเปลี่ยนแปลงพื้นฐานดังกล่าวในตอนนี้ ฟังก์ชั่นต้องคงอยู่เพื่อหลีกเลี่ยงการแตกโค้ดจำนวนมาก

"วิธีการวิเศษ" อื่น ๆ (ที่จริงเรียกว่าวิธีพิเศษในนิทานพื้นบ้านของ Python) มีความหมายมากและมีฟังก์ชันการทำงานที่คล้ายคลึงกันในภาษาอื่น ๆ ส่วนใหญ่จะใช้สำหรับรหัสที่เรียกโดยปริยายเมื่อใช้ไวยากรณ์พิเศษ

ตัวอย่างเช่น:

  • ตัวดำเนินการที่โอเวอร์โหลด (มีอยู่ใน C ++ และอื่น ๆ )
  • ตัวสร้าง / ตัวทำลาย
  • ตะขอสำหรับเข้าถึงแอตทริบิวต์
  • เครื่องมือสำหรับ metaprogramming

และอื่น ๆ ...


2
Python และหลักการของความประหลาดใจน้อยที่สุดเป็นการอ่านที่ดีสำหรับข้อดีบางประการของ Python ในลักษณะนี้ (แม้ว่าฉันจะยอมรับว่าภาษาอังกฤษต้องการงานก็ตาม) จุดพื้นฐาน: ช่วยให้ไลบรารีมาตรฐานสามารถใช้โค้ดจำนวนมากที่สามารถนำมาใช้ซ้ำได้มาก แต่ยังสามารถเขียนทับได้
jpmc26

20

จาก Zen of Python:

เมื่อเผชิญกับความคลุมเครือให้ปฏิเสธสิ่งล่อใจที่จะเดา
ควรมีวิธีเดียวและควรมีเพียงวิธีเดียวที่จะทำได้

นี่คือหนึ่งในเหตุผลที่ - ด้วยวิธีการที่กำหนดเองนักพัฒนาจะมีอิสระในการเลือกชื่อวิธีการที่แตกต่างกันเช่นgetLength(), length(), getlength()หรือสิ่งใด ๆ Python บังคับใช้การตั้งชื่อที่เข้มงวดเพื่อให้len()สามารถใช้ฟังก์ชันทั่วไปได้

การดำเนินงานทั้งหมดที่มีร่วมกันสำหรับหลายประเภทของวัตถุที่จะใส่ลงในวิธีมายากลเช่น__nonzero__, หรือ__len__ __repr__แม้ว่าส่วนใหญ่จะเป็นทางเลือกก็ตาม

การโอเวอร์โหลดของตัวดำเนินการก็ทำได้ด้วยวิธีการทางเวทมนตร์ (เช่น__le__) ดังนั้นจึงเป็นเรื่องที่สมเหตุสมผลที่จะใช้มันสำหรับการดำเนินการทั่วไปอื่น ๆ ด้วย


นี่เป็นข้อโต้แย้งที่น่าสนใจ ที่น่าพอใจยิ่งกว่าคือ "กุยโดไม่ค่อยเชื่อเรื่อง OO" .... (ตามที่เคยเห็นอ้างจากที่อื่น).
Andy Hayden

15

Python ใช้คำว่า"magic method"เนื่องจากวิธีการเหล่านั้นช่วยให้คุณสามารถตั้งโปรแกรมได้อย่างมหัศจรรย์ ข้อดีอย่างหนึ่งของการใช้เมธอดเวทย์มนตร์ของ Python คือพวกมันมีวิธีง่ายๆในการทำให้อ็อบเจกต์ทำงานเหมือนบิวท์อิน นั่นหมายความว่าคุณสามารถหลีกเลี่ยงวิธีปฏิบัติตัวดำเนินการขั้นพื้นฐานที่น่าเกลียดไม่เป็นธรรมชาติ

ลองพิจารณาตัวอย่างต่อไปนี้:

dict1 = {1 : "ABC"}
dict2 = {2 : "EFG"}

dict1 + dict2
Traceback (most recent call last):
  File "python", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

สิ่งนี้ทำให้เกิดข้อผิดพลาดเนื่องจากประเภทพจนานุกรมไม่รองรับการเพิ่ม ตอนนี้มาขยายคลาสพจนานุกรมและเพิ่มเมธอดมายากล"__add__" :

class AddableDict(dict):

    def __add__(self, otherObj):
        self.update(otherObj)
        return AddableDict(self)


dict1 = AddableDict({1 : "ABC"})
dict2 = AddableDict({2 : "EFG"})

print (dict1 + dict2)

ตอนนี้ให้ผลลัพธ์ต่อไปนี้

{1: 'ABC', 2: 'EFG'}

ดังนั้นการเพิ่มวิธีนี้ทันใดนั้นเวทมนตร์ก็เกิดขึ้นและข้อผิดพลาดที่คุณได้รับก่อนหน้านี้ก็หายไป

ฉันหวังว่ามันจะทำให้ทุกอย่างชัดเจนสำหรับคุณ สำหรับข้อมูลเพิ่มเติมโปรดดูที่:

คำแนะนำเกี่ยวกับ Magic Methods ของ Python (Rafe Kettler, 2012)


9

ฟังก์ชันเหล่านี้บางฟังก์ชันทำมากกว่าหนึ่งวิธีที่จะสามารถนำไปใช้ได้ (โดยไม่ต้องใช้วิธีนามธรรมในซูเปอร์คลาส) ตัวอย่างเช่นbool()การกระทำเช่นนี้:

def bool(obj):
    if hasattr(obj, '__nonzero__'):
        return bool(obj.__nonzero__())
    elif hasattr(obj, '__len__'):
        if obj.__len__():
            return True
        else:
            return False
    return True

คุณยังมั่นใจได้ 100% ว่าbool()จะส่งคืนจริงหรือเท็จเสมอ หากคุณใช้วิธีการใดที่คุณไม่สามารถแน่ใจได้ว่าคุณจะได้อะไรกลับมา

บางฟังก์ชั่นอื่น ๆ ที่มีความซับซ้อนค่อนข้างใช้งาน (มีความซับซ้อนมากขึ้นกว่าวิธีมายากลพื้นฐานมีแนวโน้มที่จะ) เป็นiter()และcmp()และทุกวิธีการแอตทริบิวต์ ( getattr, setattrและdelattr) สิ่งต่างๆเช่นintเข้าถึงวิธีการใช้เวทมนตร์เมื่อทำการบีบบังคับ (คุณสามารถใช้งานได้__int__) แต่ทำหน้าที่สองเท่าเป็นประเภท เป็นจริงกรณีหนึ่งที่ผมไม่เชื่อว่ามันแตกต่างจากที่เคยlen(obj)obj.__len__()


2
แทนที่จะhasattr()ใช้try:/ except AttributeError:และแทนที่จะif obj.__len__(): return True else: return Falseพูดreturn obj.__len__() > 0แต่สิ่งเหล่านี้เป็นเพียงโวหารเท่านั้น
Chris Lutz

ใน python 2.6 (ซึ่ง btw bool(x)อ้างถึงx.__nonzero__()) วิธีของคุณจะไม่ได้ผล อินสแตนซ์บูลมีวิธีการ__nonzero__()และโค้ดของคุณจะเรียกตัวเองต่อไปเมื่อ obj เป็นบูล บางทีbool(obj.__bool__())ควรได้รับการปฏิบัติแบบเดียวกับที่คุณปฏิบัติ__len__หรือไม่? (หรือรหัสนี้ใช้ได้จริงกับ Python 3?)
Ponkadoodle

ลักษณะวงกลมของบูล () ค่อนข้างไร้สาระโดยเจตนาเพื่อสะท้อนถึงลักษณะวงกลมที่แปลกประหลาดของคำจำกัดความ มีข้อโต้แย้งว่าควรถือว่าเป็นเรื่องดั้งเดิม
Ian Bicking

ความแตกต่างเพียงอย่างเดียว (ในปัจจุบัน) ระหว่างlen(x)และx.__len__()คืออดีตจะเพิ่ม OverflowError สำหรับความยาวที่เกินsys.maxsizeในขณะที่ประเภทหลังโดยทั่วไปจะไม่ใช้กับประเภทที่ใช้ใน Python นั่นเป็นข้อบกพร่องมากกว่าคุณสมบัติ (เช่นวัตถุช่วงของ Python 3.2 ส่วนใหญ่สามารถจัดการช่วงขนาดใหญ่ได้ตามอำเภอใจ แต่การใช้lenกับพวกเขาอาจล้ม__len__เหลวแม้ว่าพวกเขาจะล้มเหลวเช่นกันเนื่องจากถูกนำไปใช้ใน C แทนที่จะเป็น Python)
ncoghlan

4

พวกเขาไม่ใช่ "ชื่อวิเศษ" จริงๆ เป็นเพียงอินเทอร์เฟซที่อ็อบเจ็กต์ต้องใช้เพื่อให้บริการที่กำหนด ในแง่นี้พวกเขาไม่ได้วิเศษไปกว่าคำจำกัดความของอินเทอร์เฟซที่กำหนดไว้ล่วงหน้าที่คุณต้องนำมาใช้ใหม่


1

ในขณะที่เหตุผลส่วนใหญ่เป็นประวัติศาสตร์ แต่มีลักษณะเฉพาะบางอย่างใน Python lenที่ทำให้การใช้ฟังก์ชันแทนวิธีการที่เหมาะสม

ดำเนินการบางอย่างในหลามจะดำเนินการตามวิธีการเช่นlist.indexและdict.appendขณะที่คนอื่นจะดำเนินการตาม callables และวิธีมายากลเช่นstrและและiter reversedทั้งสองกลุ่มมีความแตกต่างกันเพียงพอดังนั้นแนวทางที่แตกต่างกันจึงมีเหตุผล:

  1. เป็นเรื่องธรรมดา
  2. str, intและเพื่อน ๆ หลายประเภท มันสมเหตุสมผลกว่าที่จะเรียกตัวสร้าง
  3. การนำไปใช้งานแตกต่างจากการเรียกใช้ฟังก์ชัน ตัวอย่างเช่นiterอาจโทร__getitem__หาก__iter__ไม่พร้อมใช้งานและสนับสนุนอาร์กิวเมนต์เพิ่มเติมที่ไม่พอดีกับการเรียกใช้เมธอด ด้วยเหตุผลเดียวกันนี้it.next()ได้ถูกเปลี่ยนnext(it)เป็น Python เวอร์ชันล่าสุด - มันสมเหตุสมผลกว่า
  4. บางส่วนเป็นญาติสนิทของผู้ประกอบการ มีไวยากรณ์สำหรับการโทร__iter__และ__next__- เรียกว่าforลูป เพื่อความสม่ำเสมอฟังก์ชันจะดีกว่า และทำให้ดีขึ้นสำหรับการเพิ่มประสิทธิภาพบางอย่าง
  5. ฟังก์ชั่นบางอย่างคล้ายกับส่วนที่เหลือมากเกินไป - reprทำหน้าที่เหมือนstrทำ มีstr(x)เมื่อเทียบกับการx.repr()จะทำให้เกิดความสับสน
  6. isinstanceบางคนไม่ค่อยใช้วิธีการปฏิบัติจริงเช่น
  7. บางคนเป็นผู้ดำเนินการจริงgetattr(x, 'a')เป็นอีกวิธีหนึ่งในการทำx.aและgetattrแบ่งปันคุณสมบัติหลายประการดังกล่าวข้างต้น

ฉันเองเรียกวิธีการแบบกลุ่มแรกและตัวดำเนินการกลุ่มที่สอง มันไม่ใช่ความแตกต่างที่ดีมาก แต่ฉันหวังว่ามันจะช่วยได้

ต้องบอกว่าlenไม่เหมาะกับกลุ่มที่สอง ใกล้เคียงกับการดำเนินการในครั้งแรกมากขึ้นโดยมีข้อแตกต่างเพียงอย่างเดียวที่พบได้บ่อยกว่าเกือบทั้งหมด แต่สิ่งเดียวที่มันไม่สามารถโทรและเป็นมากใกล้เคียงกับ__len__ L.indexอย่างไรก็ตามมีความแตกต่างบางประการ ตัวอย่างเช่น__len__อาจได้รับการเรียกให้ใช้คุณลักษณะอื่น ๆ เช่นboolหากมีการเรียกวิธีการนี้lenคุณอาจทำลายbool(x)ด้วยlenวิธีการที่กำหนดเองซึ่งแตกต่างไปจากเดิมอย่างสิ้นเชิง

ในระยะสั้นคุณมีชุดของคุณสมบัติทั่วไปที่คลาสอาจนำไปใช้ซึ่งอาจเข้าถึงได้ผ่านตัวดำเนินการผ่านฟังก์ชันพิเศษ (ซึ่งโดยปกติจะทำมากกว่าการใช้งานตามที่ตัวดำเนินการจะทำ) ในระหว่างการสร้างวัตถุและทั้งหมด แบ่งปันลักษณะทั่วไปบางอย่าง ส่วนที่เหลือทั้งหมดเป็นวิธีการ และlenเป็นข้อยกเว้นของกฎนั้น


0

มีไม่มากที่จะเพิ่มในสองโพสต์ข้างต้น แต่ฟังก์ชัน "มายากล" ทั้งหมดไม่ได้วิเศษเลย เป็นส่วนหนึ่งของโมดูล __ builtins__ ซึ่งนำเข้าโดยปริยาย / โดยอัตโนมัติเมื่อล่ามเริ่มทำงาน ได้แก่ :

from __builtins__ import *

เกิดขึ้นทุกครั้งก่อนเริ่มโปรแกรมของคุณ

ฉันคิดเสมอว่ามันจะถูกต้องมากขึ้นหาก Python ทำสิ่งนี้สำหรับเชลล์แบบโต้ตอบเท่านั้นและต้องใช้สคริปต์เพื่อนำเข้าส่วนต่างๆจากบิวด์อินที่ต้องการ นอกจากนี้การจัดการ __ main__ ที่แตกต่างกันก็อาจจะดีในเชลล์และแบบโต้ตอบ อย่างไรก็ตามตรวจสอบฟังก์ชั่นทั้งหมดและดูว่ามันเป็นอย่างไรหากไม่มี:

dir (__builtins__)
...
del __builtins__
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.