__getattr__ บนโมดูล


127

จะใช้การเทียบเท่า__getattr__กับคลาสบนโมดูลได้อย่างไร?

ตัวอย่าง

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

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

# note this function is intentionally on the module, and not the class above
def __getattr__(mod, name):
    return getattr(A(), name)

if __name__ == "__main__":
    # i hope here to have my __getattr__ function above invoked, since
    # salutation does not exist in the current namespace
    salutation("world")

ซึ่งจะช่วยให้:

matt@stanley:~/Desktop$ python getattrmod.py 
Traceback (most recent call last):
  File "getattrmod.py", line 9, in <module>
    salutation("world")
NameError: name 'salutation' is not defined

2
ฉันจะตอบกลับด้วยความเสียใจเพราะมันใช้ได้ในทุกสถานการณ์ (แม้ว่ามันจะยุ่งไปหน่อยและน่าจะทำได้ดีกว่านี้) Harvard S และ S Lott มีคำตอบที่ดี แต่ไม่ใช่วิธีแก้ปัญหาที่ใช้ได้จริง
Matt Joiner

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

คำถามที่น่าสนใจคุณคิดได้อย่างไร?
Chris Wesseling

1
อาจซ้ำกันของAutoload ใน Python
Piotr Dobrogost

1
__getattr__บนโมดูลได้รับการสนับสนุนจาก Python 3.7
Fumito Hamamura

คำตอบ:


42

เมื่อไม่นานมานี้ Guido ได้ประกาศว่าการค้นหาวิธีการพิเศษทั้งหมดในคลาสสไตล์ใหม่ข้าม__getattr__และ__getattribute__ . วิธีการ Dunder เคยทำงานร่วมกับโมดูล - คุณสามารถยกตัวอย่างเช่นการใช้โมดูลเป็นผู้จัดการบริบทโดยเพียงแค่การกำหนด__enter__และ__exit__ก่อนเทคนิคผู้ยากจน

เมื่อเร็ว ๆ นี้คุณลักษณะทางประวัติศาสตร์บางอย่างได้กลับมาอีกครั้งโมดูล__getattr__ในหมู่พวกเขาและดังนั้นการแฮ็กที่มีอยู่ (โมดูลที่แทนที่ตัวเองด้วยคลาสในsys.modulesเวลานำเข้า) จึงไม่จำเป็นอีกต่อไป

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

# my_module.py

def __getattr__(name: str) -> Any:
    ...

นอกจากนี้ยังจะช่วยให้ตะขอเข้า "จาก" from my_module import whateverการนำเข้าเช่นคุณสามารถกลับมาสร้างแบบไดนามิกวัตถุสำหรับงบเช่น

ในบันทึกที่เกี่ยวข้องพร้อมกับ getattr โมดูลคุณอาจกำหนดฟังก์ชั่นในระดับโมดูลเพื่อตอบสนองต่อ__dir__ dir(my_module)ดูPEP 562สำหรับรายละเอียด


1
ถ้าฉันสร้างโมดูลแบบไดนามิกผ่านm = ModuleType("mod")และตั้งค่าm.__getattr__ = lambda attr: return "foo"; แต่เมื่อฉันเรียกฉันได้รับfrom mod import foo TypeError: 'module' object is not iterable
weberc2

@ weberc2: Make ว่าบวกคุณจะต้องกำหนดรายการสำหรับโมดูลที่มีm.__getattr__ = lambda attr: "foo" sys.modules['mod'] = mหลังจากนั้นไม่มีข้อผิดพลาดกับfrom mod import fooไฟล์.
martineau

wim: คุณยังสามารถรับค่าที่คำนวณแบบไดนามิกเช่นการมีคุณสมบัติระดับโมดูลโดยอนุญาตให้เขียนmy_module.whateverเพื่อเรียกใช้ (ตามหลังimport my_module)
martineau

115

มีปัญหาพื้นฐานสองประการที่คุณพบที่นี่:

  1. __xxx__ วิธีการจะถูกค้นหาในชั้นเรียนเท่านั้น
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) หมายถึงวิธีการแก้ปัญหาใด ๆ ที่จะต้องติดตามด้วยว่าโมดูลใดถูกตรวจสอบมิฉะนั้นทุกโมดูลจะมีพฤติกรรมการแทนที่อินสแตนซ์ และ (2) หมายความว่า (1) เป็นไปไม่ได้ด้วยซ้ำ ... อย่างน้อยก็ไม่ใช่โดยตรง

โชคดีที่ sys.modules ไม่จู้จี้จุกจิกเกี่ยวกับสิ่งที่ไปที่นั่นดังนั้น wrapper จะใช้งานได้ แต่สำหรับการเข้าถึงโมดูลเท่านั้น (เช่นimport somemodule; somemodule.salutation('world')สำหรับการเข้าถึงโมดูลเดียวกันคุณต้องดึงวิธีการจากคลาสการทดแทนและเพิ่มลงในglobals()eiher ด้วย a วิธีการที่กำหนดเองในคลาส (ฉันชอบใช้.export()) หรือกับฟังก์ชันทั่วไป (เช่นที่ระบุไว้แล้วเป็นคำตอบ) สิ่งหนึ่งที่ควรคำนึงถึง: หาก Wrapper กำลังสร้างอินสแตนซ์ใหม่ทุกครั้งและโซลูชัน globals ไม่ใช่ คุณจบลงด้วยพฤติกรรมที่แตกต่างกันอย่างละเอียดโอ้และคุณไม่ได้ใช้ทั้งสองอย่างพร้อมกันไม่ว่าจะเป็นอย่างใดอย่างหนึ่ง


ปรับปรุง

จากGuido van Rossum :

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

# module foo.py

import sys

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>

sys.modules[__name__] = Foo()

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

ดังนั้นวิธีที่กำหนดไว้ในการบรรลุสิ่งที่คุณต้องการคือการสร้างคลาสเดียวในโมดูลของคุณและเมื่อการกระทำสุดท้ายของโมดูลแทนที่sys.modules[__name__]ด้วยอินสแตนซ์ของคลาสของคุณ - และตอนนี้คุณสามารถเล่นกับ__getattr__/ __setattr__/ ได้__getattribute__ตามต้องการ


หมายเหตุ 1 : หากคุณใช้ฟังก์ชันนี้สิ่งอื่นใดในโมดูลเช่นลูกโลกฟังก์ชันอื่น ๆ ฯลฯ จะสูญหายไปเมื่อทำการsys.modulesมอบหมายงานดังนั้นโปรดตรวจสอบให้แน่ใจว่าทุกสิ่งที่จำเป็นอยู่ในคลาสทดแทน

หมายเหตุ 2 : เพื่อสนับสนุนfrom module import *คุณต้อง__all__กำหนดไว้ในคลาส ตัวอย่างเช่น:

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>
    __all__ = list(set(vars().keys()) - {'__module__', '__qualname__'})

ทั้งนี้ขึ้นอยู่กับรุ่นหลามของคุณอาจจะมีชื่ออื่น ๆ __all__ที่จะละเว้นจาก set()สามารถละเว้นถ้างูหลาม 2 เข้ากันได้ไม่จำเป็นต้อง


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

4
ตอนนี้ฉันรู้สึกสบายใจมากขึ้นเมื่อใช้แฮ็คนี้โดยพิจารณาว่า "กึ่ง - ทำนองคลองธรรม" :)
Mark Nunberg

3
นี้จะทำสิ่งที่ประหลาดเช่นการimport sysให้สำหรับNone sysฉันเดาว่าแฮ็คนี้ไม่ได้ถูกลงโทษใน Python 2
asmeurer

3
@asmeurer: เพื่อให้เข้าใจถึงเหตุผลนั้น (และวิธีแก้ปัญหา) โปรดดูคำถามทำไมค่าของ __name__ จึงเปลี่ยนไปหลังจากกำหนดให้ sys.modules [__ name__]? .
martineau

1
@qarma: ฉันดูเหมือนจะจำการปรับปรุงบางอย่างที่พูดถึงซึ่งจะช่วยให้โมดูล python สามารถมีส่วนร่วมในโมเดลการสืบทอดคลาสได้โดยตรง แต่ถึงอย่างนั้นวิธีนี้ก็ยังใช้งานได้และได้รับการสนับสนุน
Ethan Furman

48

นี่เป็นการแฮ็ก แต่คุณสามารถรวมโมดูลด้วยคลาสได้:

class Wrapper(object):
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __getattr__(self, name):
    # Perform custom logic here
    try:
      return getattr(self.wrapped, name)
    except AttributeError:
      return 'default' # Some sensible default

sys.modules[__name__] = Wrapper(sys.modules[__name__])

10
ดีและสกปรก: D
Matt Joiner

นั่นอาจได้ผล แต่อาจไม่ใช่วิธีแก้ปัญหาที่แท้จริงของผู้เขียน
DasIch

12
"อาจใช้ได้ผล" และ "อาจไม่" ไม่เป็นประโยชน์มากนัก เป็นการแฮ็ก / เคล็ดลับ แต่ใช้ได้ผลและแก้ปัญหาที่เกิดจากคำถามได้
Håvard S

6
แม้ว่าสิ่งนี้จะทำงานในโมดูลอื่น ๆที่นำเข้าโมดูลของคุณและเข้าถึงแอตทริบิวต์ที่ไม่มีอยู่ในโมดูล แต่จะใช้ไม่ได้กับตัวอย่างโค้ดจริงที่นี่ การเข้าถึง globals () ไม่ผ่าน sys.modules
Marius Gedminas

5
น่าเสียดายที่สิ่งนี้ใช้ไม่ได้กับโมดูลปัจจุบันหรือเป็นไปได้สำหรับสิ่งที่เข้าถึงหลังจากimport *ไฟล์.
Matt Joiner

19

เรามักจะไม่ทำแบบนั้น

สิ่งที่เราทำคือสิ่งนี้

class A(object):
....

# The implicit global instance
a= A()

def salutation( *arg, **kw ):
    a.salutation( *arg, **kw )

ทำไม? เพื่อให้สามารถมองเห็นอินสแตนซ์ส่วนกลางโดยนัยได้

ตัวอย่างเช่นดูที่randomโมดูลซึ่งสร้างอินสแตนซ์ส่วนกลางโดยปริยายเพื่อลดความซับซ้อนของกรณีการใช้งานที่คุณต้องการตัวสร้างตัวเลขสุ่มแบบ "ธรรมดา"


หากคุณมีความทะเยอทะยานจริงๆคุณสามารถสร้างคลาสและวนซ้ำตามวิธีการทั้งหมดและสร้างฟังก์ชันระดับโมดูลสำหรับแต่ละวิธี
Paul Fisher

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

13

คล้ายกับสิ่งที่ @ Håvard S เสนอในกรณีที่ฉันต้องการใช้เวทมนตร์บางอย่างในโมดูล (เช่น__getattr__) ฉันจะกำหนดคลาสใหม่ที่สืบทอดมาจากtypes.ModuleTypeและใส่สิ่งนั้นเข้าไปsys.modules(อาจจะแทนที่โมดูลที่กำหนดแบบกำหนดเองของฉันModuleType)

ดู__init__.pyไฟล์หลักของWerkzeugสำหรับการใช้งานที่ค่อนข้างมีประสิทธิภาพ


8

นี่มันแฮ็ค แต่ ...

import types

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

    def farewell(self, greeting, accusative):
         print greeting, accusative

def AddGlobalAttribute(classname, methodname):
    print "Adding " + classname + "." + methodname + "()"
    def genericFunction(*args):
        return globals()[classname]().__getattribute__(methodname)(*args)
    globals()[methodname] = genericFunction

# set up the global namespace

x = 0   # X and Y are here to add them implicitly to globals, so
y = 0   # globals does not change as we iterate over it.

toAdd = []

def isCallableMethod(classname, methodname):
    someclass = globals()[classname]()
    something = someclass.__getattribute__(methodname)
    return callable(something)


for x in globals():
    print "Looking at", x
    if isinstance(globals()[x], (types.ClassType, type)):
        print "Found Class:", x
        for y in dir(globals()[x]):
            if y.find("__") == -1: # hack to ignore default methods
                if isCallableMethod(x,y):
                    if y not in globals(): # don't override existing global names
                        toAdd.append((x,y))


for x in toAdd:
    AddGlobalAttribute(*x)


if __name__ == "__main__":
    salutation("world")
    farewell("goodbye", "world")

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

จะไม่สนใจแอตทริบิวต์ทั้งหมดที่มี "__"

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


2
ฉันชอบคำตอบของHåvard S สำหรับฉันเพราะมันดูสะอาดกว่ามาก แต่สิ่งนี้ตอบคำถามได้โดยตรงตามที่ถาม
เสียใจ

นี่เป็นสิ่งที่ใกล้เคียงกับสิ่งที่ฉันไปด้วยในที่สุด มันยุ่งเล็กน้อย แต่ใช้งานได้กับ globals () อย่างถูกต้องภายในโมดูลเดียวกัน
Matt Joiner

1
สำหรับฉันแล้วดูเหมือนว่าคำตอบนี้ไม่ใช่สิ่งที่ถูกถามซึ่งก็คือ "เมื่อเรียกใช้ฟังก์ชันที่ไม่มีอยู่ในแอตทริบิวต์ที่กำหนดแบบคงที่ของโมดูล" เพราะมันทำงานโดยไม่มีเงื่อนไขและเพิ่มวิธีการคลาสที่เป็นไปได้ทุกอย่าง ซึ่งสามารถแก้ไขได้โดยใช้กับตัวห่อโมดูลที่ทำเฉพาะAddGlobalAttribute()เมื่อมีระดับโมดูลเท่านั้นAttributeError- ประเภทของการย้อนกลับของตรรกะของ @ Håvard S ถ้าฉันมีโอกาสฉันจะทดสอบสิ่งนี้และเพิ่มคำตอบแบบไฮบริดของฉันเองแม้ว่า OP จะยอมรับคำตอบนี้แล้วก็ตาม
martineau

อัปเดตความคิดเห็นก่อนหน้าของฉัน ตอนนี้ฉันเข้าใจแล้วว่ามันยากมาก (impssoble?) ที่จะสกัดกั้นNameErrorข้อยกเว้นสำหรับเนมสเปซส่วนกลาง (โมดูล) - ซึ่งอธิบายว่าเหตุใดคำตอบนี้จึงเพิ่มคำเรียกสำหรับทุกความเป็นไปได้ที่พบในเนมสเปซส่วนกลางปัจจุบันเพื่อให้ครอบคลุมทุกกรณีที่เป็นไปได้ล่วงหน้า
martineau

5

นี่คือผลงานที่ต่ำต้อยของฉันเอง - การปรุงแต่งเล็กน้อยของคำตอบที่ได้คะแนนสูงของ @ Håvard S แต่ชัดเจนกว่าเล็กน้อย (ดังนั้นอาจเป็นที่ยอมรับสำหรับ @ S.Lott แม้ว่าอาจจะไม่ดีพอสำหรับ OP):

import sys

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        try:
            return getattr(self.wrapped, name)
        except AttributeError:
            return getattr(A(), name)

_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])

if __name__ == "__main__":
    _globals.salutation("world")

-3

สร้างไฟล์โมดูลของคุณที่มีคลาสของคุณ นำเข้าโมดูล รันgetattrบนโมดูลที่คุณเพิ่งนำเข้า คุณสามารถนำเข้าแบบไดนามิกโดยใช้__import__และดึงโมดูลจาก sys.modules

นี่คือโมดูลของคุณsome_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

และในโมดูลอื่น:

import some_module

Foo = getattr(some_module, 'Foo')

ทำสิ่งนี้แบบไดนามิก:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')

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