ทำความเข้าใจกับเครื่องกำเนิดไฟฟ้าใน Python


218

ฉันกำลังอ่านตำรา Python ในขณะนี้และกำลังมองหาเครื่องกำเนิดไฟฟ้า ฉันพบว่ามันยากที่จะทำให้หัวกลม

เมื่อฉันมาจากพื้นหลังของจาวามีจาวาเทียบเท่าหรือไม่ หนังสือเล่มนี้พูดถึง 'ผู้ผลิต / ผู้บริโภค' แต่เมื่อฉันได้ยินว่าฉันคิดถึงเกลียว

ตัวกำเนิดคืออะไรและทำไมคุณถึงใช้มัน ชัดเจนโดยไม่ต้องอ้างถึงหนังสือใด ๆ (เว้นแต่คุณจะพบคำตอบที่ง่ายและตรงไปตรงมาจากหนังสือ) อาจมีตัวอย่างถ้าคุณรู้สึกใจกว้าง!

คำตอบ:


402

หมายเหตุ: โพสต์นี้จะใช้ไวยากรณ์ Python 3.x

กำเนิดเป็นเพียงฟังก์ชั่นซึ่งจะส่งกลับวัตถุที่คุณสามารถเรียกnextเช่นว่าทุกสายก็จะส่งกลับค่าบางอย่างจนกว่าจะยกStopIterationข้อยกเว้นส่งสัญญาณว่าค่าทั้งหมดที่ได้รับการสร้าง วัตถุดังกล่าวเรียกว่าiterator

ฟังก์ชั่นปกติคืนค่าเดียวโดยใช้returnเช่นเดียวกับใน Java ในหลาม yieldแต่มีทางเลือกที่เรียกว่า การใช้yieldทุกที่ในฟังก์ชันทำให้เป็นเครื่องกำเนิดไฟฟ้า สังเกตรหัสนี้:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

ในขณะที่คุณสามารถดูmyGen(n)เป็นฟังก์ชั่นที่อัตราผลตอบแทนและn n + 1ทุกการเรียกเพื่อnextให้ได้ค่าเดียวจนกว่าจะได้รับค่าทั้งหมด forมีการเรียกวนซ้ำnextในพื้นหลังดังนี้:

>>> for n in myGen(6):
...     print(n)
... 
6
7

ในทำนองเดียวกันมีการแสดงออกของเครื่องกำเนิดซึ่งให้วิธีการที่จะอธิบายอย่างชัดเจนประเภทกำเนิดทั่วไป:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

โปรดทราบว่านิพจน์ของตัวสร้างนั้นเหมือนกับความเข้าใจในรายการ :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

สังเกตว่าวัตถุของตัวสร้างถูกสร้างขึ้นครั้งเดียวแต่รหัสของมันจะไม่ทำงานทั้งหมดในครั้งเดียว เฉพาะการเรียกเพื่อnextเรียกใช้งานโค้ด (ส่วนหนึ่ง) การเรียกใช้โค้ดในเครื่องกำเนิดไฟฟ้าหยุดลงเมื่อถึงyieldคำสั่งแล้วซึ่งส่งคืนค่า โทรถัดจากนั้นก็เป็นสาเหตุที่ทำให้การดำเนินการที่จะดำเนินการในรัฐในการที่เครื่องกำเนิดไฟฟ้าถูกทิ้งหลังจากที่ผ่านมาnext yieldนี่คือความแตกต่างพื้นฐานกับฟังก์ชั่นปกติ: ผู้ที่เริ่มต้นการดำเนินการที่ "บน" เสมอและละทิ้งสถานะของพวกเขาเมื่อส่งคืนค่า

ยังมีอีกหลายสิ่งที่ต้องพูดเกี่ยวกับเรื่องนี้ มันเป็นไปได้เช่นsendข้อมูลกลับเข้าไปในเครื่องกำเนิดไฟฟ้า ( อ้างอิง ) แต่นั่นคือสิ่งที่ฉันแนะนำให้คุณอย่ามองเข้าไปจนกว่าคุณจะเข้าใจแนวคิดพื้นฐานของเครื่องกำเนิดไฟฟ้า

ตอนนี้คุณอาจถาม: ทำไมต้องใช้เครื่องกำเนิดไฟฟ้า? มีเหตุผลดีๆสองสามข้อ:

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

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    รหัสนี้ใช้itertools.isliceเพื่อนำองค์ประกอบจำนวน จำกัด จากกระแสอนันต์ ขอแนะนำให้คุณดูที่ฟังก์ชั่นในitertoolsโมดูลเนื่องจากเป็นเครื่องมือที่จำเป็นสำหรับการสร้างเครื่องกำเนิดไฟฟ้าขั้นสูงได้อย่างง่ายดาย


   About Python <= 2.6:ในตัวอย่างด้านบนnextเป็นฟังก์ชันที่เรียกใช้เมธอด__next__บนวัตถุที่กำหนด ในหลาม <= 2.6 หนึ่งในการใช้เทคนิคที่แตกต่างกันเล็กน้อยคือแทนo.next() next(o)Python 2.7 มีการnext()โทร.nextดังนั้นคุณไม่จำเป็นต้องใช้สิ่งต่อไปนี้ใน 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3

9
คุณพูดถึงมันเป็นไปได้ที่sendข้อมูลไปยังเครื่องกำเนิด เมื่อคุณทำเช่นนั้นคุณจะมี 'coroutine' มันง่ายมากที่จะใช้รูปแบบเช่นผู้บริโภค / ผู้ผลิตที่กล่าวถึงกับ coroutines เพราะพวกเขาไม่จำเป็นต้องใช้Lockและดังนั้นจึงไม่สามารถหยุดชะงักได้ เป็นการยากที่จะอธิบาย coroutines โดยไม่ต้องทุบตีหัวข้อดังนั้นฉันจะบอกว่า coroutines เป็นทางเลือกที่สวยงามมากในการทำเกลียว
Jochen Ritzel

ผู้สร้าง Python เป็นเครื่องจักรทัวริงในแง่ของวิธีการทำงานหรือไม่?
Fiery Phoenix

48

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

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

และอื่น ๆ ข้อดีของเครื่องกำเนิดไฟฟ้าก็คือเพราะพวกมันจัดการกับข้อมูลทีละชิ้นคุณจึงสามารถจัดการกับข้อมูลจำนวนมากได้ ด้วยรายการความต้องการหน่วยความจำที่มากเกินไปอาจกลายเป็นปัญหาได้ เครื่องกำเนิดไฟฟ้าเช่นเดียวกับรายการต่างๆสามารถทำซ้ำได้ดังนั้นจึงสามารถใช้ในวิธีเดียวกันได้:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

โปรดทราบว่าเครื่องกำเนิดไฟฟ้าให้วิธีอื่นในการจัดการกับอินฟินิตี้เช่น

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

เครื่องกำเนิดไฟฟ้าแค็ปซูลวนไม่สิ้นสุด แต่นี่ไม่ใช่ปัญหาเพราะคุณจะได้รับคำตอบทุกครั้งที่คุณขอมัน


30

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

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

มันอาจจะเป็นความคิดที่ดีที่จะแม่นยำและหลีกเลี่ยงคำว่า "เครื่องกำเนิด" โดยไม่มีข้อกำหนดเพิ่มเติม


2
อืมฉันคิดว่าคุณพูดถูกแล้วอย่างน้อยก็ตามการทดสอบของสองสามบรรทัดใน Python 2.6 นิพจน์ตัวสร้างส่งคืนตัววนซ้ำ (aka 'วัตถุตัวสร้าง') ไม่ใช่ตัวสร้าง
เคเอสเคป

22

เครื่องกำเนิดไฟฟ้าอาจคิดว่าเป็นการจดชวเลขสำหรับการสร้างตัววนซ้ำ พวกมันทำตัวเหมือน Java Iterator ตัวอย่าง:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

หวังว่านี่จะช่วย / เป็นสิ่งที่คุณกำลังมองหา

ปรับปรุง:

เนื่องจากมีคำตอบอื่น ๆ อีกมากมายปรากฏขึ้นมีวิธีต่าง ๆ ในการสร้างตัวสร้าง คุณสามารถใช้ไวยากรณ์วงเล็บในตัวอย่างของฉันข้างต้นหรือคุณสามารถใช้ผลตอบแทน อีกคุณสมบัติที่น่าสนใจคือเครื่องกำเนิดไฟฟ้าสามารถ "ไม่สิ้นสุด" - ตัววนซ้ำที่ไม่หยุด:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
ตอนนี้ Java มีStreams ซึ่งคล้ายกับเครื่องกำเนิดไฟฟ้ามากยกเว้นว่าคุณไม่สามารถรับองค์ประกอบต่อไปได้โดยไม่ต้องยุ่งยาก
คดีฟ้องร้องกองทุนโมนิก้า

12

ไม่มีเทียบเท่า Java

นี่คือตัวอย่างเล็กน้อยของการวางแผน:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

มีลูปในตัวสร้างที่ทำงานจาก 0 ถึง n และหากตัวแปรลูปเป็นผลคูณของ 3 มันจะทำให้เกิดตัวแปร

ในระหว่างการวนซ้ำแต่ละรอบของforเครื่องกำเนิดไฟฟ้าจะถูกดำเนินการ ถ้านี่เป็นครั้งแรกที่ตัวเรียกใช้งานจะเริ่มต้นที่จุดเริ่มต้นมิฉะนั้นจะเริ่มจากครั้งก่อนหน้า


2
ย่อหน้าสุดท้ายมีความสำคัญมาก: สถานะของฟังก์ชันตัวสร้างคือ 'ตรึง' ทุกครั้งที่ให้ผลผลิต sth และยังคงอยู่ในสถานะเดียวกันเมื่อเรียกใช้ในครั้งถัดไป
โยฮันเนสชาร์รา

ไม่มีอะไรเทียบเท่าไวยากรณ์ใน Java กับ "นิพจน์กำเนิด" แต่กำเนิด - เมื่อคุณมีหนึ่ง - เป็นหลักเพียงแค่ iterator (ลักษณะพื้นฐานเช่นเดียวกับ Java iterator)
คิดทบทวน

@overthink: ผู้สร้างสามารถมีผลข้างเคียงอื่น ๆ ที่ตัววนซ้ำ Java ไม่สามารถทำได้ ถ้าฉันจะใส่print "hello"หลังจากx=x+1ในตัวอย่างของฉัน "Hello" จะถูกพิมพ์ 100 ครั้งในขณะที่เนื้อหาของ for loop จะยังคงถูกประหาร 33 ครั้งเท่านั้น
Wernsey

@iWerner: ค่อนข้างแน่ใจว่าเอฟเฟกต์เดียวกันอาจมีใน Java การนำไปใช้งานของ next () ใน Java iterator ที่เทียบเท่าจะยังคงต้องค้นหาจาก 0 ถึง 99 (โดยใช้ตัวอย่าง mygen ของคุณ (100)) ดังนั้นคุณสามารถใช้ System.out.println () ทุกครั้งถ้าคุณต้องการ คุณจะกลับมา 33 ครั้งจากถัดไป () สิ่งที่ Java ขาดคือไวยากรณ์ผลตอบแทนที่มีประโยชน์มากซึ่งง่ายต่อการอ่าน (และเขียน) อย่างมาก
คิดทบทวน

ฉันชอบที่จะอ่านและจำบรรทัดนี้หนึ่งบรรทัด: ถ้านี่เป็นครั้งแรกที่ตัวสร้างดำเนินการมันจะเริ่มต้นที่จุดเริ่มต้นมิฉะนั้นจะดำเนินต่อจากครั้งก่อนที่มันให้ผล
Iqra

8

ฉันชอบที่จะอธิบายเครื่องกำเนิดไฟฟ้าสำหรับผู้ที่มีพื้นฐานด้านภาษาโปรแกรมคอมพิวเตอร์และคอมพิวเตอร์ในแง่ของสแต็กเฟรม

ในหลายภาษามีสแต็คอยู่ด้านบนซึ่งเป็นสแต็ก "เฟรม" ปัจจุบัน กรอบสแต็กรวมถึงพื้นที่ที่จัดสรรสำหรับตัวแปรโลคัลกับฟังก์ชันรวมถึงอาร์กิวเมนต์ที่ส่งผ่านไปยังฟังก์ชันนั้น

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

ด้วยฟังก์ชั่นปกติในบางครั้งฟังก์ชั่นจะส่งกลับค่าและสแต็คคือ "โผล่" กรอบสแต็กของฟังก์ชันถูกยกเลิกและดำเนินการต่อที่ตำแหน่งก่อนหน้า

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

ก่อนหน้า Python 2.5 สิ่งนี้เป็นเครื่องกำเนิดไฟฟ้าทั้งหมด งูหลาม 2.5 เพิ่มความสามารถในการส่งผ่านค่ากลับมาในการกำเนิดไฟฟ้าได้เป็นอย่างดี ในการทำเช่นนั้นค่า pass-in จะมีอยู่ในรูปแบบที่เป็นผลมาจากคำสั่งผลตอบแทนที่ได้กลับมาควบคุมชั่วคราว (และค่า) จากเครื่องกำเนิด

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


6

สิ่งเดียวที่ฉันสามารถเพิ่มลงในคำตอบของ Stephan202 ได้คือคำแนะนำให้คุณดูที่การนำเสนอ PyCon '08 ของ Pyreno David Beazley "Generator Tricks for Systems โปรแกรมเมอร์" ซึ่งเป็นคำอธิบายเดียวที่ดีที่สุดเกี่ยวกับวิธีและสาเหตุของเครื่องกำเนิดไฟฟ้าที่ฉันเคยเห็น ทุกแห่ง นี่คือสิ่งที่พาฉันไปจาก "Python ดูสนุกมาก" ถึง "นี่คือสิ่งที่ฉันกำลังมองหา" มันเป็นเรื่องที่http://www.dabeaz.com/generators/


6

ช่วยทำให้เห็นความแตกต่างที่ชัดเจนระหว่างฟังก์ชั่น foo และตัวกำเนิดฟู (n):

def foo(n):
    yield n
    yield n+1

foo เป็นฟังก์ชั่น foo (6) เป็นวัตถุกำเนิด

วิธีทั่วไปในการใช้ตัวสร้างวัตถุอยู่ในลูป:

for n in foo(6):
    print(n)

ห่วงพิมพ์

# 6
# 7

คิดว่าตัวสร้างเป็นฟังก์ชันที่ทำงานต่อได้

yieldทำงานเหมือนreturnในแง่ที่ว่าค่าที่ให้ผลได้รับ "คืน" โดยเครื่องกำเนิด อย่างไรก็ตามในครั้งถัดไปที่ตัวสร้างได้รับการร้องขอค่าฟังก์ชันของตัวสร้าง foo จะดำเนินการต่อจากจุดที่ค้างไว้หลังจากคำสั่งให้ผลตอบแทนล่าสุด

เบื้องหลังเมื่อคุณเรียกbar=foo(6)ใช้แถบวัตถุของตัวสร้างที่กำหนดให้คุณมีnextแอตทริบิวต์

คุณสามารถเรียกมันด้วยตัวเองเพื่อดึงค่าที่ได้จาก foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

เมื่อ foo สิ้นสุดลง (และไม่มีค่าที่ให้ผลมากขึ้น) การโทรnext(bar)จะส่งข้อผิดพลาด StopInteration


5

โพสต์นี้จะใช้ตัวเลข Fibonacciเป็นเครื่องมือในการที่จะสร้างขึ้นเพื่ออธิบายประโยชน์ของเครื่องกำเนิดไฟฟ้าหลาม

โพสต์นี้จะมีทั้งรหัส C ++ และ Python

หมายเลข Fibonacci ถูกกำหนดเป็นลำดับ: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

หรือโดยทั่วไป:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

สิ่งนี้สามารถถ่ายโอนไปยังฟังก์ชัน C ++ ได้อย่างง่ายดายมาก:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

แต่ถ้าคุณต้องการพิมพ์ตัวเลข Fibonacci ทั้งหกตัวแรกคุณจะต้องคำนวณค่าจำนวนมากด้วยฟังก์ชั่นด้านบน

ตัวอย่างเช่นFib(3) = Fib(2) + Fib(1)แต่ยังคำนวณFib(2) Fib(1)มูลค่าที่คุณต้องการคำนวณยิ่งสูงเท่าไรคุณก็ยิ่งแย่ลงเท่านั้น

ดังนั้นหนึ่งอาจถูกล่อลวงให้เขียนใหม่ข้างต้นโดยการติดตามของรัฐmainมา

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

mainแต่นี้เป็นที่น่าเกลียดมากและมันมีความซับซ้อนตรรกะของเราใน มันจะเป็นการดีกว่าถ้าคุณไม่ต้องกังวลกับสถานะในการmainทำงานของเรา

เราสามารถคืนvectorค่าa ของและใช้iteratorเพื่อย้ำมากกว่าชุดของค่านั้น แต่ต้องใช้หน่วยความจำจำนวนมากทั้งหมดในครั้งเดียวสำหรับค่าตอบแทนจำนวนมาก

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

เพื่อแก้ปัญหาเหล่านี้และเพื่อหลีกเลี่ยงการถูกยิงเราอาจเขียนบล็อครหัสนี้อีกครั้งโดยใช้ฟังก์ชั่นการโทรกลับ ทุกครั้งที่พบหมายเลข Fibonacci ใหม่เราจะเรียกฟังก์ชันการเรียกกลับ

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

นี่คือการปรับปรุงที่ชัดเจนตรรกะของคุณmainไม่ยุ่งเหยิงและคุณสามารถทำทุกอย่างที่คุณต้องการด้วยหมายเลขฟีโบนักชีเพียงกำหนดหมายเลขโทรกลับใหม่

แต่นี่ยังไม่สมบูรณ์แบบ ถ้าคุณต้องการได้รับหมายเลข Fibonacci สองหมายเลขแรกเท่านั้นจากนั้นทำบางสิ่งบางอย่างจากนั้นรับจำนวนมากขึ้นจากนั้นทำอย่างอื่น

เราสามารถดำเนินต่อไปเหมือนที่เคยเป็นมาและเราสามารถเริ่มเพิ่มสถานะอีกครั้งmainเพื่อให้ GetFibNumbers เริ่มต้นจากจุดที่กำหนดเอง แต่มันจะขยายโค้ดของเราออกไปและมันก็ดูใหญ่เกินไปสำหรับงานง่าย ๆ เช่นการพิมพ์หมายเลขฟีโบนักชี

เราสามารถใช้รูปแบบผู้ผลิตและผู้บริโภคผ่านกระทู้สองสามข้อ แต่นี่ทำให้รหัสซับซ้อนยิ่งขึ้น

เรามาพูดถึงเครื่องกำเนิดไฟฟ้ากันดีกว่า

Python มีคุณสมบัติภาษาที่ดีมากที่แก้ปัญหาเช่นนี้เรียกว่าเครื่องกำเนิดไฟฟ้า

เครื่องกำเนิดช่วยให้คุณสามารถใช้งานฟังก์ชั่นหยุดที่จุดใดก็ได้แล้วดำเนินการต่อในจุดที่คุณค้างไว้ ทุกครั้งที่ส่งคืนค่า

พิจารณารหัสต่อไปนี้ที่ใช้ตัวสร้าง:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

ซึ่งให้ผลลัพธ์กับเรา:

0 1 1 2 3 5

yieldคำสั่งใช้ร่วมกับเครื่องกำเนิดไฟฟ้าหลาม มันช่วยประหยัดสถานะของฟังก์ชั่นและส่งกลับค่า Yeilded ในครั้งถัดไปที่คุณเรียกใช้ฟังก์ชันถัดไป () บนตัวสร้าง

นี่คือสะอาดกว่าโค้ดฟังก์ชันการเรียกกลับ เรามีโค้ดที่สะอาดกว่าโค้ดที่เล็กกว่าและไม่ต้องพูดถึงโค้ดที่ใช้งานได้มากกว่า (Python อนุญาตให้มีจำนวนเต็มขนาดใหญ่โดยพลการ)

แหล่ง


3

ฉันเชื่อว่าการปรากฏตัวครั้งแรกของตัววนซ้ำและตัวกำเนิดอยู่ในภาษาโปรแกรมไอคอนเมื่อประมาณ 20 ปีที่แล้ว

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

หลังจากอ่านสองสามย่อหน้าที่นั่นอรรถประโยชน์ของเครื่องกำเนิดไฟฟ้าและตัววนซ้ำอาจชัดเจนขึ้น


2

ประสบการณ์กับความเข้าใจในรายการได้แสดงให้เห็นถึงประโยชน์มากมายทั่ว Python อย่างไรก็ตามกรณีการใช้งานจำนวนมากไม่จำเป็นต้องมีรายการทั้งหมดที่สร้างขึ้นในหน่วยความจำ แต่พวกเขาจำเป็นต้องวนซ้ำองค์ประกอบหนึ่งครั้ง

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

sum([x*x for x in range(10)])

หน่วยความจำถูกสงวนไว้โดยใช้นิพจน์ตัวสร้างแทน:

sum(x*x for x in range(10))

ผลประโยชน์ที่คล้ายกันจะถูกนำไปใช้กับคอนสตรัคเตอร์สำหรับวัตถุคอนเทนเนอร์

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

นิพจน์ตัวสร้างมีประโยชน์อย่างยิ่งกับฟังก์ชั่นเช่น sum (), min (), และ max () ที่ลดอินพุต iterable ให้เป็นค่าเดียว:

max(len(line)  for line in file  if line.strip())

มากกว่า


1

ฉันวางโค้ดนี้ไว้ซึ่งอธิบายแนวคิดหลัก 3 ข้อเกี่ยวกับเครื่องกำเนิด:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.