pythonic นำเข้าฟังก์ชันภายในหรือไม่?


126

PEP 8พูดว่า:

  • การนำเข้าจะถูกวางไว้ที่ด้านบนสุดของไฟล์เสมอหลังจากความคิดเห็นของโมดูลและ docstrings ใด ๆ และก่อนโมดูลและค่าคงที่ของโมดูล

ในเหตุการณ์ที่เกิดขึ้นฉันละเมิด PEP 8 บางครั้งฉันนำเข้าสิ่งที่อยู่ในฟังก์ชัน ตามกฎทั่วไปฉันจะทำเช่นนี้หากมีการนำเข้าที่ใช้ภายในฟังก์ชันเดียวเท่านั้น

มีความคิดเห็นอย่างไร

แก้ไข (เหตุผลที่ฉันรู้สึกว่าการนำเข้าในฟังก์ชันอาจเป็นความคิดที่ดี):

เหตุผลหลัก: สามารถทำให้โค้ดชัดเจนขึ้น

  • เมื่อดูโค้ดของฟังก์ชันฉันอาจถามตัวเองว่า "function / class xxx คืออะไร" (xxx ถูกใช้ภายในฟังก์ชัน) หากฉันมีการนำเข้าทั้งหมดที่ด้านบนของโมดูลฉันต้องไปดูที่นั่นเพื่อดูว่า xxx คืออะไร from m import xxxนี้มีมากขึ้นของปัญหาเมื่อใช้ เห็นm.xxxในฟังก์ชั่นคงบอกอะไรได้มากกว่านี้ ขึ้นอยู่กับว่าmคืออะไร: เป็นโมดูล / แพ็คเกจระดับบนสุด ( import m) ที่รู้จักกันดีหรือไม่? หรือเป็นโมดูลย่อย / แพ็กเกจ ( from a.b.c import m)?
  • ในบางกรณีการมีข้อมูลเพิ่มเติมนั้น ("xxx คืออะไร") ใกล้กับตำแหน่งที่ใช้ xxx อาจทำให้เข้าใจฟังก์ชันได้ง่ายขึ้น

2
และคุณทำเพื่อประสิทธิภาพ?
Macarse

4
ฉันรู้สึกว่ามันทำให้โค้ดชัดเจนขึ้นในบางกรณี ฉันเดาว่าประสิทธิภาพดิบจะลดลงเมื่อนำเข้าในฟังก์ชัน (เนื่องจากคำสั่งนำเข้าจะดำเนินการทุกครั้งที่เรียกใช้ฟังก์ชัน)
codeape

คุณสามารถตอบได้ว่า "function / class xxx คืออะไร" โดยใช้ไวยากรณ์ import xyz แทนไวยากรณ์ from xyz import abc
Tom Leys

1
หากความชัดเจนเป็นปัจจัยเดียวคุณอาจรวมความคิดเห็นที่เกี่ยวข้องเพื่อผลกระทบนั้นด้วย ;)
Lakshman Prasad

5
@becomingGuru: แน่นอน แต่ความคิดเห็นอาจไม่ตรงกับความเป็นจริง ...
codeape

คำตอบ:


88

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

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

อีกประเด็นหนึ่งฉันชอบที่จะได้รับImportErrorข้อยกเว้นก่อนที่จะเรียกใช้โค้ดใด ๆ - เป็นการตรวจสอบความมีเหตุผลดังนั้นจึงเป็นอีกเหตุผลหนึ่งในการนำเข้าที่ด้านบน

ฉันใช้pyCheckerเพื่อตรวจสอบโมดูลที่ไม่ได้ใช้งาน


47

มีสองครั้งที่ฉันละเมิด PEP 8 ในเรื่องนี้:

  • การนำเข้าแบบวงกลม: โมดูล A นำเข้าโมดูล B แต่บางสิ่งในโมดูล B ต้องการโมดูล A (แม้ว่านี่จะเป็นสัญญาณว่าฉันจำเป็นต้อง refactor โมดูลเพื่อกำจัดการพึ่งพาแบบวงกลม)
  • การใส่เบรกพอยต์ pdb: import pdb; pdb.set_trace()นี่เป็น b / c ที่มีประโยชน์ฉันไม่ต้องการวางไว้import pdbที่ด้านบนของทุกโมดูลที่ฉันอาจต้องการดีบักและอย่าลืมลบการนำเข้าเมื่อฉันลบเบรกพอยต์

นอกสองกรณีนี้คุณควรวางทุกอย่างไว้ที่ด้านบนสุด ทำให้การอ้างอิงชัดเจนขึ้น


7
ฉันยอมรับว่ามันทำให้การอ้างอิงชัดเจนขึ้นเกี่ยวกับโมดูลโดยรวม แต่ฉันเชื่อว่ามันสามารถทำให้โค้ดมีความชัดเจนน้อยลงในระดับฟังก์ชันเพื่อนำเข้าทุกอย่างที่ด้านบน เมื่อคุณดูโค้ดของฟังก์ชันคุณอาจถามตัวเองว่า "function / class xxx คืออะไร" (ใช้ xxx ภายในฟังก์ชัน) และคุณต้องดูที่ด้านบนสุดของไฟล์เพื่อดูว่า xxx มาจากไหน นี่เป็นปัญหามากกว่าเมื่อใช้จาก m import xxx เห็น m.xxx บอกคุณได้มากขึ้น - อย่างน้อยถ้าไม่มีข้อสงสัยว่า m คืออะไร
codeape

20

นี่คือกรณีการนำเข้าสี่กรณีที่เราใช้

  1. import(และfrom x import yและimport x as y) ที่ด้านบน

  2. ทางเลือกสำหรับการนำเข้า ที่ด้านบน.

    import settings
    if setting.something:
        import this as foo
    else:
        import that as foo
    
  3. การนำเข้าตามเงื่อนไข ใช้กับ JSON ไลบรารี XML และอื่น ๆ ที่ด้านบน.

    try:
        import this as foo
    except ImportError:
        import that as foo
    
  4. การนำเข้าแบบไดนามิก จนถึงตอนนี้เรามีเพียงตัวอย่างเดียวเท่านั้น

    import settings
    module_stuff = {}
    module= __import__( settings.some_module, module_stuff )
    x = module_stuff['x']
    

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

    นี่คือมากหรือน้อยที่ด้านบนของโมดูล


นี่คือสิ่งที่เราทำเพื่อให้โค้ดชัดเจนขึ้น:

  • ทำให้โมดูลสั้น

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

  • ในบางกรณีการมีข้อมูลเพิ่มเติมใกล้กับที่ใช้ชื่ออาจทำให้เข้าใจฟังก์ชันได้ง่ายขึ้น หากโมดูลสั้นก็ทำได้ง่าย


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

ฉันคิดว่าคุณสามารถใช้สิ่งนี้ได้อย่างสมเหตุสมผล ฉันคิดว่าอาจมีจุดสมดุลที่โมดูลของคุณ "เล็กพอ" จนคุณไม่จำเป็นต้องใช้เทคนิคการนำเข้าแบบแฟนซีเพื่อจัดการความซับซ้อน ขนาดโมดูลเฉลี่ยของเราคือ - บังเอิญ - ประมาณ 100 บรรทัด
S.Lott

8

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

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

ตามกฎทั่วไปฉันจะทำเช่นนี้หากมีการนำเข้าที่ใช้ภายในฟังก์ชันเดียวเท่านั้น

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

FWIW มีหลายกรณีที่ควรนำเข้าภายในฟังก์ชัน ตัวอย่างเช่นหากคุณต้องการตั้งค่าภาษาใน cx_Oracle คุณต้องตั้งค่า_ตัวแปรสภาพแวดล้อมNLS LANG ก่อนที่จะนำเข้า ดังนั้นคุณอาจเห็นรหัสดังนี้:

import os

oracle = None

def InitializeOracle(lang):
    global oracle
    os.environ['NLS_LANG'] = lang
    import cx_Oracle
    oracle = cx_Oracle

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

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

6

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


5

มาจากคำถามเกี่ยวกับการโหลดโมดูลสองครั้ง - ทำไมไม่ทั้งสองอย่าง?

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


3

ตราบใดที่ยังimportไม่ใช่from x import *คุณควรวางไว้ที่ด้านบน เพิ่มชื่อเพียงชื่อเดียวในเนมสเปซส่วนกลางและคุณติด PEP 8 นอกจากนี้หากคุณต้องการใช้ที่อื่นในภายหลังคุณก็ไม่ต้องย้ายอะไรไปรอบ ๆ

ไม่ใช่เรื่องใหญ่ แต่เนื่องจากแทบไม่มีความแตกต่างฉันจึงขอแนะนำให้ทำตามที่ PEP 8 พูด


3
อันที่จริงการใส่from x import *ฟังก์ชันจะสร้าง SyntaxWarning อย่างน้อยใน 2.5
Rick Copeland

3

ลองดูแนวทางอื่นที่ใช้ใน sqlalchemy: dependency injection:

@util.dependencies("sqlalchemy.orm.query")
def merge_result(query, *args):
    #...
    query.Query(...)

สังเกตว่าไลบรารีที่นำเข้ามีการประกาศในมัณฑนากรอย่างไรและส่งผ่านเป็นอาร์กิวเมนต์ไปยังฟังก์ชัน !

วิธีนี้ทำให้โค้ดสะอาดขึ้นและยังทำงานได้เร็วกว่าimportคำสั่ง4.5 เท่า !

เกณฑ์มาตรฐาน: https://gist.github.com/kolypto/589e84fbcfb6312532658df2fabdb796


2

ในโมดูลที่เป็นทั้งโมดูล 'ปกติ' และสามารถเรียกใช้งานได้ (เช่นมี a if __name__ == '__main__':-section) ฉันมักจะนำเข้าโมดูลที่ใช้เมื่อเรียกใช้โมดูลภายในส่วนหลักเท่านั้น

ตัวอย่าง:

def really_useful_function(data):
    ...


def main():
    from pathlib import Path
    from argparse import ArgumentParser
    from dataloader import load_data_from_directory

    parser = ArgumentParser()
    parser.add_argument('directory')
    args = parser.parse_args()
    data = load_data_from_directory(Path(args.directory))
    print(really_useful_function(data)


if __name__ == '__main__':
    main()

1

มีอีกกรณีหนึ่ง (อาจเป็น "มุม") ที่อาจเป็นประโยชน์ต่อimportฟังก์ชันที่ไม่ค่อยได้ใช้งานภายใน: ลดเวลาเริ่มต้น

ฉันชนกำแพงนั้นครั้งหนึ่งด้วยโปรแกรมที่ค่อนข้างซับซ้อนที่ทำงานบนเซิร์ฟเวอร์ IoT ขนาดเล็กที่ยอมรับคำสั่งจากสายอนุกรมและดำเนินการซึ่งอาจเป็นการดำเนินการที่ซับซ้อนมาก

การวางimportคำสั่งที่ด้านบนของไฟล์หมายถึงการประมวลผลการนำเข้าทั้งหมดก่อนที่เซิร์ฟเวอร์จะเริ่มทำงาน ตั้งแต่importรายการรวมjinja2, lxml, signxmlและอื่น ๆ "หนัก" (SoC และก็ไม่ได้มีประสิทธิภาพมาก) นี่หมายความนาทีก่อนที่จะมีการเรียนการสอนเป็นครั้งแรกที่ดำเนินการจริง

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

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