__getattr__ trên một mô-đun


127

Làm thế nào có thể thực hiện tương đương với một __getattr__ trên một lớp, trên một mô-đun?

Thí dụ

Khi gọi một hàm không tồn tại trong các thuộc tính được xác định tĩnh của mô-đun, tôi muốn tạo một thể hiện của một lớp trong mô-đun đó và gọi phương thức trên nó với cùng tên như thất bại trong việc tra cứu thuộc tính trên mô-đun.

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")

Cung cấp cho:

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
Tôi có thể sẽ đi với câu trả lời đau buồn, vì nó hoạt động trong mọi tình huống (mặc dù nó hơi lộn xộn và có thể được thực hiện tốt hơn). Harvard S và S Lott có câu trả lời sạch đẹp nhưng chúng không phải là giải pháp thiết thực.
Matt Joiner

1
Trong trường hợp của bạn, bạn thậm chí không thực hiện quyền truy cập thuộc tính, vì vậy bạn đang yêu cầu hai thứ khác nhau cùng một lúc. Vì vậy, câu hỏi chính là cái nào bạn muốn. Bạn có muốn salutationtồn tại trong không gian tên toàn cầu hoặc cục bộ (đó là điều mà đoạn mã trên đang cố gắng thực hiện) hoặc bạn muốn tra cứu động tên khi bạn thực hiện truy cập dấu chấm trên mô-đun? Đó là hai điều khác nhau.
Lennart Regebro

Câu hỏi thú vị, làm thế nào bạn nghĩ ra điều này?
Chris Wesseling


1
__getattr__trên các mô-đun được hỗ trợ từ Python 3.7
Fumito Hamamura

Câu trả lời:


41

Cách đây một thời gian, Guido tuyên bố rằng tất cả các tra cứu phương thức đặc biệt trên các lớp kiểu mới đều bỏ qua __getattr____getattribute__ . Các phương thức Dunder trước đây đã hoạt động trên các mô-đun - ví dụ, bạn có thể sử dụng mô-đun làm trình quản lý bối cảnh chỉ bằng cách xác định __enter____exit__trước khi các thủ thuật đó bị phá vỡ .

Gần đây, một số tính năng lịch sử đã trở lại, mô-đun __getattr__trong số đó, và do đó, bản hack hiện có (một mô-đun tự thay thế bằng một lớp trong sys.modulesthời gian nhập) không còn cần thiết nữa.

Trong Python 3.7+, bạn chỉ cần sử dụng một cách rõ ràng. Để tùy chỉnh quyền truy cập thuộc tính trên một mô-đun, hãy xác định một __getattr__hàm ở cấp mô-đun sẽ chấp nhận một đối số (tên của thuộc tính) và trả về giá trị được tính hoặc tăng AttributeError:

# my_module.py

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

Điều này cũng sẽ cho phép móc vào nhập "từ", tức là bạn có thể trả về các đối tượng được tạo động cho các câu lệnh như from my_module import whatever .

Trên một lưu ý liên quan, cùng với mô-đun getattr, bạn cũng có thể xác định một __dir__chức năng ở cấp mô-đun để đáp ứng dir(my_module). Xem PEP 562 để biết chi tiết.


1
Nếu tôi tự động tạo một mô-đun thông qua m = ModuleType("mod")và thiết lập m.__getattr__ = lambda attr: return "foo"; Tuy nhiên, khi tôi chạy from mod import foo, tôi nhận được TypeError: 'module' object is not iterable.
weberc2

@ weberc2: Thực hiện điều đó m.__getattr__ = lambda attr: "foo", cộng với bạn cần xác định một mục nhập cho mô-đun với sys.modules['mod'] = m. Sau đó, không có lỗi với from mod import foo.
martineau

wim: Bạn cũng có thể nhận được các giá trị được tính toán linh hoạt, giống như có một thuộc tính cấp mô-đun, cho phép một người viết my_module.whateverđể gọi nó (sau một import my_module).
martineau

115

Có hai vấn đề cơ bản mà bạn đang gặp phải ở đây:

  1. __xxx__ phương thức chỉ được tra cứu trên lớp
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) có nghĩa là bất kỳ giải pháp nào cũng sẽ phải theo dõi mô-đun nào đang được kiểm tra, nếu không thì mọi mô-đun sau đó sẽ có hành vi thay thế cá thể; và (2) có nghĩa là (1) thậm chí không thể ... ít nhất là không trực tiếp.

May mắn thay, sys.modules không kén chọn những gì ở đó nên trình bao bọc sẽ hoạt động, nhưng chỉ để truy cập mô-đun (nghĩa là import somemodule; somemodule.salutation('world')đối với truy cập cùng mô-đun, bạn phải sử dụng các phương thức từ lớp thay thế và thêm chúng vào globals()một cách xa hơn phương thức tùy chỉnh trên lớp (tôi thích sử dụng .export()) hoặc với một hàm chung (chẳng hạn như các hàm đã được liệt kê dưới dạng câu trả lời). Một điều cần lưu ý: nếu trình bao bọc tạo ra một thể hiện mới mỗi lần, và giải pháp toàn cầu thì không, bạn kết thúc với hành vi khác nhau một cách tinh tế. Ồ, và bạn không được sử dụng cả hai cùng một lúc - đó là một hoặc khác.


Cập nhật

Từ Guido van Rossum :

Thực sự có một hack đôi khi được sử dụng và khuyến nghị: một mô-đun có thể định nghĩa một lớp có chức năng mong muốn, và cuối cùng, thay thế nó trong sys.modules bằng một thể hiện của lớp đó (hoặc với lớp, nếu bạn nhấn mạnh , nhưng nói chung là ít hữu ích hơn). Ví dụ:

# module foo.py

import sys

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

sys.modules[__name__] = Foo()

Điều này hoạt động vì máy móc nhập khẩu đang tích cực kích hoạt hack này và khi bước cuối cùng của nó kéo mô-đun thực tế ra khỏi sys.modules, sau khi tải nó. (Đây không phải là tình cờ. Vụ hack đã được đề xuất từ ​​lâu và chúng tôi quyết định chúng tôi thích đủ để hỗ trợ nó trong máy móc nhập khẩu.)

Vì vậy, cách được thiết lập để thực hiện những gì bạn muốn là tạo một lớp duy nhất trong mô-đun của bạn và là hành động cuối cùng của mô-đun thay thế sys.modules[__name__]bằng một thể hiện của lớp của bạn - và bây giờ bạn có thể chơi với __getattr__/ __setattr__/ __getattribute__khi cần.


Lưu ý 1 : Nếu bạn sử dụng chức năng này thì mọi thứ khác trong mô-đun, chẳng hạn như toàn cầu, các chức năng khác, v.v., sẽ bị mất khi sys.modulesthực hiện chuyển nhượng - vì vậy hãy đảm bảo mọi thứ cần thiết đều nằm trong lớp thay thế.

Lưu ý 2 : Để hỗ trợ from module import *bạn phải __all__xác định trong lớp; ví dụ:

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

Tùy thuộc vào phiên bản Python của bạn, có thể có các tên khác để bỏ qua __all__. Có set()thể bỏ qua nếu không cần khả năng tương thích Python 2.


3
Điều này hoạt động vì máy móc nhập đang tích cực kích hoạt hack này và khi bước cuối cùng của nó rút mô-đun thực tế ra khỏi sys.modules, sau khi tải nó Có được đề cập ở đâu đó trong tài liệu không?
Piotr Dobrogost

4
Bây giờ tôi cảm thấy thoải mái hơn khi sử dụng bản hack này, coi nó là "bán bị xử phạt" :)
Mark Nunberg

3
Đây là làm những việc khó khăn, như làm import syscho Nonecho sys. Tôi đoán rằng vụ hack này không bị xử phạt trong Python 2.
asmeker

3
@asmeker: Để hiểu lý do cho điều đó (và một giải pháp) hãy xem câu hỏi Tại sao giá trị của __name__ thay đổi sau khi gán cho sys.modules [__ name__]? .
martineau

1
@qarma: Tôi dường như nhớ lại một số cải tiến đang được nói đến sẽ cho phép các mô-đun python tham gia trực tiếp hơn vào mô hình kế thừa lớp, nhưng ngay cả như vậy phương thức này vẫn hoạt động và được hỗ trợ.
Ethan Furman

48

Đây là một hack, nhưng bạn có thể bọc mô-đun bằng một lớp:

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
tốt và bẩn: D
Matt Joiner

Điều đó có thể làm việc nhưng nó có thể không phải là một giải pháp cho vấn đề thực sự của tác giả.
DasIch

12
"Có thể làm việc" và "có lẽ không" không hữu ích lắm. Đó là một hack / trick, nhưng nó hoạt động và giải quyết vấn đề được đặt ra bởi câu hỏi.
Håvard S

6
Mặc dù điều này sẽ hoạt động trong các mô-đun khác nhập mô-đun của bạn và truy cập các thuộc tính không tồn tại trên nó, nhưng nó sẽ không hoạt động cho ví dụ mã thực tế ở đây. Truy cập toàn cầu () không đi qua sys.modules.
Marius Gedminas

5
Thật không may, điều này không hoạt động cho mô-đun hiện tại, hoặc có khả năng cho những thứ được truy cập sau một import *.
Matt Joiner

19

Chúng ta thường không làm theo cách đó.

Những gì chúng tôi làm là điều này.

class A(object):
....

# The implicit global instance
a= A()

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

Tại sao? Vì vậy, ví dụ toàn cầu ngầm có thể nhìn thấy.

Ví dụ, nhìn vào randommô-đun, tạo ra một cá thể toàn cầu ẩn để đơn giản hóa một chút các trường hợp sử dụng mà bạn muốn một trình tạo số ngẫu nhiên "đơn giản".


Nếu bạn thực sự tham vọng, bạn có thể tạo lớp và lặp qua tất cả các phương thức của nó và tạo một hàm cấp mô-đun cho mỗi phương thức.
Paul Fisher

@Paul Fisher: Theo từng vấn đề, lớp đã tồn tại. Phơi bày tất cả các phương pháp của lớp học có thể không phải là một ý tưởng tốt. Thông thường các phương thức tiếp xúc này là phương pháp "tiện lợi". Không phải tất cả đều thích hợp cho trường hợp toàn cầu ngầm.
S.Lott

13

Tương tự như những gì @ Håvard S đã đề xuất, trong trường hợp tôi cần triển khai một số phép thuật trên một mô-đun (như __getattr__), tôi sẽ định nghĩa một lớp mới kế thừa types.ModuleTypevà đưa nó vào sys.modules(có thể thay thế mô-đun nơi ModuleTypexác định tùy chỉnh của tôi ).

Xem __init__.pytệp chính của Werkzeug để biết cách triển khai khá mạnh mẽ này.


8

Đây là hackish, nhưng ...

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")

Điều này hoạt động bằng cách lặp qua tất cả các đối tượng trong không gian tên toàn cầu. Nếu mục là một lớp, nó lặp lại trên các thuộc tính của lớp. Nếu thuộc tính có thể gọi được, nó thêm nó vào không gian tên toàn cục dưới dạng hàm.

Nó bỏ qua tất cả các thuộc tính có chứa "__".

Tôi sẽ không sử dụng mã này trong mã sản xuất, nhưng nó sẽ giúp bạn bắt đầu.


2
Tôi thích câu trả lời của Håvard S cho tôi, vì nó có vẻ sạch sẽ hơn nhiều, nhưng điều này trực tiếp trả lời câu hỏi khi được hỏi.
đau buồn

Điều này gần với những gì tôi đã đi cùng. Nó hơi lộn xộn, nhưng hoạt động chính xác với toàn cầu () trong cùng một mô-đun.
Matt Joiner

1
Dường như với tôi câu trả lời này không hoàn toàn đúng với câu hỏi "Khi gọi một hàm không tồn tại trong các thuộc tính được xác định tĩnh của mô-đun" bởi vì nó hoạt động vô điều kiện và thêm mọi phương thức lớp có thể. Điều đó có thể được khắc phục bằng cách sử dụng trình bao bọc mô-đun chỉ thực hiện AddGlobalAttribute()khi có cấp độ mô-đun AttributeError- loại ngược lại với logic của @ Håvard S. Nếu tôi có cơ hội tôi sẽ kiểm tra điều này và thêm câu trả lời lai của riêng tôi mặc dù OP đã chấp nhận câu trả lời này.
martineau

Cập nhật nhận xét trước của tôi. Bây giờ tôi hiểu rằng rất khó (impssoble?) Để chặn các NameErrorngoại lệ cho không gian tên toàn cầu (mô-đun) - điều này giải thích tại sao câu trả lời này thêm các hàm gọi cho mọi khả năng mà nó tìm thấy trong không gian tên toàn cầu hiện tại để bao quát mọi trường hợp có thể xảy ra trước thời hạn.
martineau

5

Đây là đóng góp khiêm tốn của riêng tôi - một sự tô điểm nhẹ cho câu trả lời được đánh giá cao của @ Håvard S, nhưng rõ ràng hơn một chút (vì vậy có thể chấp nhận được với @ S.Lott, mặc dù có lẽ không đủ tốt cho 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

Tạo tập tin mô-đun có các lớp của bạn. Nhập mô-đun. Chạy getattrtrên mô-đun bạn vừa nhập. Bạn có thể thực hiện nhập động bằng cách sử dụng __import__và kéo mô-đun từ sys.modules.

Đây là mô-đun của bạn some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

Và trong một mô-đun khác:

import some_module

Foo = getattr(some_module, 'Foo')

Làm điều này một cách linh hoạt:

import sys

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

1
Bạn đang trả lời một câu hỏi khác nhau ở đây.
Marius Gedminas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.