【问题标题】:What is the purpose of class methods?类方法的目的是什么?
【发布时间】:2010-09-07 11:37:02
【问题描述】:

我正在自学 Python,我最近的一课是 Python is not Java,所以我刚刚花了一段时间把我所有的 Class 方法都变成了函数。

我现在意识到我不需要使用 Class 方法来完成我在 Java 中使用 static 方法所做的事情,但现在我不确定何时会使用它们。我能找到的关于 Python 类方法的所有建议都是像我这样的新手应该避开它们,而标准文档在讨论它们时是最不透明的。

有没有人有一个在 Python 中使用 Class 方法的好例子,或者至少有人可以告诉我什么时候可以合理使用 Class 方法?

【问题讨论】:

    标签: python class-method


    【解决方案1】:

    类方法适用于当您需要具有不特定于任何特定实例但仍以某种方式涉及类的方法时。它们最有趣的地方在于它们可以被子类覆盖,这在 Java 的静态方法或 Python 的模块级函数中根本不可能。

    如果您有一个类 MyClass,以及一个在 MyClass 上运行的模块级函数(工厂、依赖注入存根等),请将其设为 classmethod。然后它将可用于子类。

    【讨论】:

    • 我明白了。但是,如果我只是想对一个不需要 MyClass 的方法进行分类,但如果我在其他 MyClass 中分组在某种程度上仍然有意义呢?不过,我可以考虑将其移至帮助模块..
    • 我有一个与原文有关的问题,也许您可​​以同时回答?哪种调用对象的类方法的方式“更好”或“更惯用”:obj.cls_mthd(...)type(obj).cls_mthd(...)
    • @Alexey:前者会更好,因为它不会硬编码该方法是类方法而不是常规方法的事实——因此如果切换该事实,则不需要更改。即,该细节不会影响它的使用方式。
    【解决方案2】:

    工厂方法(替代构造函数)确实是类方法的经典例子。

    基本上,类方法适用于任何你想要一个自然适合类名称空间但不与类的特定实例相关联的方法。

    例如,在优秀的unipath 模块中:

    当前目录

    • Path.cwd()
      • 返回实际当前目录;例如,Path("/tmp/my_temp_dir")。这是一个类方法。
    • .chdir()
      • 将自己设为当前目录。

    由于当前目录是进程范围的,cwd 方法没有应该与之关联的特定实例。但是,将cwd更改为给定Path实例的目录确实应该是一个实例方法。

    嗯...因为Path.cwd() 确实返回了Path 实例,我猜它可以被认为是一个工厂方法...

    【讨论】:

    • 是的...首先想到的是工厂方法。沿着这些思路还有一些可以实现单例模式的东西,比如:getAndOptionallyCreateSomeSingleInstanceOfSomeResource()
    • 在python中,这些是静态方法,而不是类方法。
    【解决方案3】:

    这样想:普通方法对于隐藏调度细节很有用:你可以输入myobj.foo(),而不用担心foo()方法是由myobj对象的类还是它的父类之一实现的.类方法与此完全类似,但使用类对象:它们让您调用MyClass.foo() 而不必担心foo() 是否由MyClass 专门实现,因为它需要自己的专门版本,或者它是否是让其父类处理调用。

    当您在创建实际实例之前进行设置或计算时,类方法是必不可少的,因为在实例存在之前,您显然不能将实例用作方法的调度点来电。一个很好的例子可以在 SQLAlchemy 源代码中查看;在以下链接中查看dbapi() 类方法:

    https://github.com/zzzeek/sqlalchemy/blob/ab6946769742602e40fb9ed9dde5f642885d1906/lib/sqlalchemy/dialects/mssql/pymssql.py#L47

    您可以看到dbapi() 方法是一个类方法,因为它需要运行before 实例,数据库后端使用它来按需导入它需要的特定于供应商的数据库库一个特定的数据库连接开始被创建——但它不能是一个简单的函数或静态函数,因为他们希望它能够调用其他支持方法,这些方法可能同样需要在子类中比在其父类中更具体地编写.如果你调度到一个函数或静态类,那么你“忘记”并失去关于哪个类正在初始化的知识。

    【讨论】:

      【解决方案4】:

      我最近想要一个非常轻量级的日志记录类,它可以根据可以以编程方式设置的日志记录级别输出不同数量的输出。但是我不想每次想输出调试消息或错误或警告时都实例化该类。但我还想封装这个日志记录工具的功能,使其无需声明任何全局变量即可重用。

      所以我使用类变量和@classmethod 装饰器来实现这一点。

      使用我的简单 Logging 类,我可以执行以下操作:

      Logger._level = Logger.DEBUG
      

      然后,在我的代码中,如果我想吐出一堆调试信息,我只需编写代码

      Logger.debug( "this is some annoying message I only want to see while debugging" )
      

      可以用

      输出错误
      Logger.error( "Wow, something really awful happened." )
      

      在“生产”环境中,我可以指定

      Logger._level = Logger.ERROR
      

      现在,只会输出错误消息。不会打印调试消息。

      这是我的课:

      class Logger :
          ''' Handles logging of debugging and error messages. '''
      
          DEBUG = 5
          INFO  = 4
          WARN  = 3
          ERROR = 2
          FATAL = 1
          _level = DEBUG
      
          def __init__( self ) :
              Logger._level = Logger.DEBUG
      
          @classmethod
          def isLevel( cls, level ) :
              return cls._level >= level
      
          @classmethod
          def debug( cls, message ) :
              if cls.isLevel( Logger.DEBUG ) :
                  print "DEBUG:  " + message
      
          @classmethod
          def info( cls, message ) :
              if cls.isLevel( Logger.INFO ) :
                  print "INFO :  " + message
      
          @classmethod
          def warn( cls, message ) :
              if cls.isLevel( Logger.WARN ) :
                  print "WARN :  " + message
      
          @classmethod
          def error( cls, message ) :
              if cls.isLevel( Logger.ERROR ) :
                  print "ERROR:  " + message
      
          @classmethod
          def fatal( cls, message ) :
              if cls.isLevel( Logger.FATAL ) :
                  print "FATAL:  " + message
      

      还有一些测试它的代码:

      def logAll() :
          Logger.debug( "This is a Debug message." )
          Logger.info ( "This is a Info  message." )
          Logger.warn ( "This is a Warn  message." )
          Logger.error( "This is a Error message." )
          Logger.fatal( "This is a Fatal message." )
      
      if __name__ == '__main__' :
      
          print "Should see all DEBUG and higher"
          Logger._level = Logger.DEBUG
          logAll()
      
          print "Should see all ERROR and higher"
          Logger._level = Logger.ERROR
          logAll()
      

      【讨论】:

      • 这确实 not 在我看来像好的示例类方法是有好处的,因为它可以通过将所有类方法模块化而以一种不太复杂的方式完成 -级别函数并完全取消类。
      • 哦,更重要的是,Python提供了一个非常简单易用的日志类。比创建一个不太理想的解决方案更糟糕的是,我重新发明了轮子。双手一巴掌。但它至少提供了一个可行的例子。不过,我不确定我是否同意在概念层面上摆脱课程。有时您希望封装代码以便于重用,而日志记录方法是一个很好的重用目标。
      • 为什么不使用静态方法?
      • @Marvo:使用这两种方法确实没有区别。使它们全部成为模块级有效地使它们成为定义它们的模块实例对象的静态方法(这是一个单例)——因此实际上没有什么理由将它们正式放入一个类中。
      【解决方案5】:

      替代构造函数是典型的例子。

      【讨论】:

        【解决方案6】:

        当用户登录我的网站时,会根据用户名和密码实例化一个 User() 对象。

        如果我需要一个用户对象而没有用户登录(例如,管理员用户可能想要删除另一个用户帐户,所以我需要实例化该用户并调用其删除方法):

        我有获取用户对象的类方法。

        class User():
            #lots of code
            #...
            # more code
        
            @classmethod
            def get_by_username(cls, username):
                return cls.query(cls.username == username).get()
        
            @classmethod
            def get_by_auth_id(cls, auth_id):
                return cls.query(cls.auth_id == auth_id).get()
        

        【讨论】:

          【解决方案7】:

          我认为最明确的答案是 AmanKow 的。它归结为你想如何组织你的代码。您可以将所有内容编写为模块级函数,这些函数包装在模块的命名空间中,即

          module.py (file 1)
          ---------
          def f1() : pass
          def f2() : pass
          def f3() : pass
          
          
          usage.py (file 2)
          --------
          from module import *
          f1()
          f2()
          f3()
          def f4():pass 
          def f5():pass
          
          usage1.py (file 3)
          -------------------
          from usage import f4,f5
          f4()
          f5()
          

          上面的程序代码没有很好的组织,你可以看到只有 3 个模块之后它变得混乱,每个方法是做什么的?您可以为函数使用长描述性名称(如在 java 中),但您的代码仍然很快变得难以管理。

          面向对象的方式是将代码分解为可管理的块,即类和对象以及函数可以与对象实例或类相关联。

          与模块级函数相比,使用类函数可以在代码中获得另一个层次的划分。 因此,您可以在一个类中对相关功能进行分组,以使它们更具体地用于您分配给该类的任务。例如,您可以创建一个文件实用程序类:

          class FileUtil ():
            def copy(source,dest):pass
            def move(source,dest):pass
            def copyDir(source,dest):pass
            def moveDir(source,dest):pass
          
          //usage
          FileUtil.copy("1.txt","2.txt")
          FileUtil.moveDir("dir1","dir2")
          

          这种方式更灵活,更易于维护,您将功能组合在一起,并且每个功能的作用更明显。您还可以防止名称冲突,例如,函数副本可能存在于您在代码中使用的另一个导入模块(例如网络副本)中,因此当您使用全名 FileUtil.copy() 时,您可以消除问题并且两个复制函数可以并排使用。

          【讨论】:

          • 为了使用 f1(), f2() 等等,你不应该像你一样使用 from module import * 和 from usage import * 吗?
          • @Jblasco 我更正了导入语句以消除混淆,是的,如果您导入模块,则必须在函数前面加上模块名称。即导入模块 --> module.f1() 等
          • 我不同意这个答案; Python 不是 Java。无法管理的代码问题显然是由于示例中故意选择了糟糕的名称而出现的。相反,如果您将FileUtil 类方法复制为file_util 模块的函数,那么当实际上不存在任何对象时,它将具有可比性并且不会滥用OOP(实际上您可能会认为它更可取,因为您最终不会使用@ 987654325@ 或其他详细信息)。通过使用import file_util 而不是from file_util import ...,同样可以在程序代码中避免名称冲突。
          【解决方案8】:

          它允许您编写可以与任何兼容类一起使用的通用类方法。

          例如:

          @classmethod
          def get_name(cls):
              print cls.name
          
          class C:
              name = "tester"
          
          C.get_name = get_name
          
          #call it:
          C.get_name()
          

          如果你不使用@classmethod,你可以使用 self 关键字,但它需要一个 Class 的实例:

          def get_name(self):
              print self.name
          
          class C:
              name = "tester"
          
          C.get_name = get_name
          
          #call it:
          C().get_name() #<-note the its an instance of class C
          

          【讨论】:

            【解决方案9】:

            说实话?我从来没有发现静态方法或类方法的用途。我还没有看到无法使用全局函数或实例方法完成的操作。

            如果 python 更像 Java 那样使用私有成员和受保护成员,情况会有所不同。在 Java 中,我需要一个静态方法来访问实例的私有成员来做事。在 Python 中,这很少需要。

            通常,当人们真正需要做的就是更好地使用 python 的模块级命名空间时,我看到人们使用静态方法和类方法。

            【讨论】:

            • 私有:_variable_name 和受保护:__variable_name
            • 一个不可缺少的用法是unittest'ssetUpClass 和tearDownClass。您正在使用单元测试,对吗? :)
            【解决方案10】:

            我曾经使用 PHP,最近我在问自己,这个类方法是怎么回事? Python 手册技术性很强,而且用词很短,因此对理解该功能没有帮助。我一直在谷歌搜索和谷歌搜索,我找到了答案 -> http://code.anjanesh.net/2007/12/python-classmethods.html

            如果你懒得点击它。我的解释更短,如下。 :)

            在 PHP 中(可能不是所有人都知道 PHP,但这种语言非常简单,每个人都应该明白我在说什么)我们有这样的静态变量:

            
            class A
            {
            
                static protected $inner_var = null;
            
                static public function echoInnerVar()
                {
                    echo self::$inner_var."\n";
                }
            
                static public function setInnerVar($v)
                {
                    self::$inner_var = $v;
                }
            
            }
            
            class B extends A
            {
            }
            
            A::setInnerVar(10);
            B::setInnerVar(20);
            
            A::echoInnerVar();
            B::echoInnerVar();
            

            两种情况下的输出都是 20。

            但是在 python 中我们可以添加@classmethod 装饰器,因此可以分别输出 10 和 20。示例:

            
            class A(object):
                inner_var = 0
            
                @classmethod
                def setInnerVar(cls, value):
                    cls.inner_var = value
            
                @classmethod
                def echoInnerVar(cls):
                    print cls.inner_var
            
            
            class B(A):
                pass
            
            
            A.setInnerVar(10)
            B.setInnerVar(20)
            
            A.echoInnerVar()
            B.echoInnerVar()
            

            聪明,不是吗?

            【讨论】:

            • 您的 Python 示例的一个潜在问题是,如果 B.setInnerVar(20) 被忽略,它将打印 10 两次(而不是在第二次 echoInnerBar() 调用时出错,即 no inner_var已定义。
            【解决方案11】:

            类方法提供了一种“语义糖”(不知道这个术语是否被广泛使用)——或“语义便利”。

            示例:您有一组表示对象的类。您可能希望使用类方法all()find() 来编写User.all()User.find(firstname='Guido')。这当然可以使用模块级函数来完成...

            【讨论】:

            • 您的示例当然假设该类正在跟踪其所有实例,并且可以在创建后访问它们——这不是自动完成的。
            • “语义糖”听起来很适合“语法糖”。
            【解决方案12】:

            让我印象深刻的是,来自 Ruby,所谓的 class 方法和所谓的 instance 方法只是一个具有语义含义的函数它的第一个参数,当 function 作为 object 的方法调用时被静默传递(即obj.meth())。

            通常 object 必须是一个实例,但 @classmethod method decorator 更改规则以传递一个类。您可以在实例上调用类方法(它只是一个函数) - 第一个参数将是它的类。

            因为它只是一个函数,它只能在任何给定范围内声明一次(即class 定义)。因此,如果遵循这一点,对于 Ruby 专家来说,您不能拥有同名的类方法和实例方法

            考虑一下:

            class Foo():
              def foo(x):
                print(x)
            

            您可以在实例上调用foo

            Foo().foo()
            <__main__.Foo instance at 0x7f4dd3e3bc20>
            

            但不在课堂上:

            Foo.foo()
            Traceback (most recent call last):
              File "<stdin>", line 1, in <module>
            TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)
            

            现在添加@classmethod:

            class Foo():
              @classmethod
              def foo(x):
                print(x)
            

            现在调用一个实例会传递它的类:

            Foo().foo()
            __main__.Foo
            

            调用类也是如此:

            Foo.foo()
            __main__.Foo
            

            唯一的约定是,我们在实例方法的第一个参数上使用self,在类方法上使用cls。我在这里都没有使用来说明这只是一个论点。在 Ruby 中,self 是一个关键字。

            与 Ruby 对比:

            class Foo
              def foo()
                puts "instance method #{self}"
              end
              def self.foo()
                puts "class method #{self}"
              end
            end
            
            Foo.foo()
            class method Foo
            
            Foo.new.foo()
            instance method #<Foo:0x000000020fe018>
            

            Python 类方法只是一个修饰函数,您可以使用与create your own decorators 相同的技术。装饰方法包装了真实方法(在@classmethod 的情况下,它传递了附加的类参数)。底层方法还在,隐藏but still accessible


            脚注:在类和实例方法之间的名称冲突激起了我的好奇心之后,我写了这篇文章。我远非 Python 专家,如果其中有任何错误,我希望使用 cmets。

            【讨论】:

            • “当函数作为对象的方法被调用时,它会静默传递”——我认为这并不准确。 AFICT,在 Python 中没有什么是“默默传递”的。 IMO,在这个意义上,Python 比 Ruby 更明智(super() 除外)。 AFICT,“魔术”发生在设置或读取属性时。 Python 中的调用 obj.meth(arg)(与 Ruby 不同)仅表示 (obj.meth)(arg)。没有任何东西在任何地方静默传递,obj.meth 只是一个可调用对象,它接受比创建它的函数少一个参数。
            • obj.meth 又简单地表示getattr(obj, "meth")
            • 为来自其他常用语言的人量身定制答案是有意义的。然而,这次谈话中缺少的一件事是将它联系在一起,那就是Python descriptor 的概念。函数是描述符,当函数是类的属性时,这就是第一个参数“自动”传递给实例的方式。这也是类方法的实现方式,在我链接的 HOWTO 中提供了纯 python 实现。它也是一个装饰器的事实是一种辅助
            【解决方案13】:

            如果您不是“受过培训的程序员”,这应该会有所帮助:

            我想我已经理解了上面和网上其他地方的技术解释,但我总是有一个问题“很好,但我为什么需要它?什么是实用的用例?”。而现在生活给了我一个很好的例子,澄清了一切:

            我正在使用它来控制由多线程模块实例化的类的实例之间共享的全局共享变量。用人性化的语言,我正在运行多个代理,它们为并行的深度学习创建示例。 (想象多个玩家同时玩 ATARI 游戏,每个人都将游戏结果保存到一个公共存储库(共享变量))

            我使用以下代码(在主/执行代码中)实例化播放器/代理:

            a3c_workers = [A3C_Worker(self.master_model, self.optimizer, i, self.env_name, self.model_dir) for i in range(multiprocessing.cpu_count())]
            
            • 它创建的玩家数量与我的电脑上的处理器内核数量一样多 A3C_Worker - 是一个定义代理的类 a3c_workers - 是该类实例的列表(即每个实例是一个玩家/代理)

            现在我想知道所有玩家/代理已经玩了多少游戏,因此在 A3C_Worker 定义中我定义了要在所有实例之间共享的变量:

            class A3C_Worker(threading.Thread):
               global_shared_total_episodes_across_all_workers = 0
            

            现在,随着工人完成他们的游戏,他们每完成一场游戏,就会将计数增加 1

            在我的示例生成结束时,我正在关闭实例,但共享变量分配了所玩游戏的总数。所以当我再次重新运行它时,我最初的总集数是之前的总集数。但我需要该计数来单独表示每次运行的该值

            修复我指定的问题:

            class A3C_Worker(threading.Thread):
                @classmethod
                def reset(cls):
                    A3C_Worker.global_shared_total_episodes_across_all_workers = 0
            

            比我刚刚调用的执行代码:

            A3C_Worker.reset()
            

            请注意,这是对整个 CLASS 的调用,而不是单独调用它的任何实例。因此,从现在开始,对于我启动的每个新代理,它都会将我的计数器设置为 0。

            使用通常的方法定义def play(self):,将需要我们为每个实例单独重置该计数器,这将需要更多计算且难以跟踪。

            【讨论】:

              【解决方案14】:

              这是一个有趣的话题。我的看法是,python classmethod 像单例而不是工厂一样运行(它返回一个生成的类的实例)。它是单例的原因是生成了一个公共对象(字典),但只为类生成一次,但由所有实例共享。

              为了说明这一点,这里举个例子。请注意,所有实例都引用了单个字典。这不是我理解的工厂模式。这可能是 python 非常独特的。

              class M():
               @classmethod
               def m(cls, arg):
                   print "arg was",  getattr(cls, "arg" , None),
                   cls.arg = arg
                   print "arg is" , cls.arg
              
               M.m(1)   # prints arg was None arg is 1
               M.m(2)   # prints arg was 1 arg is 2
               m1 = M()
               m2 = M() 
               m1.m(3)  # prints arg was 2 arg is 3  
               m2.m(4)  # prints arg was 3 arg is 4 << this breaks the factory pattern theory.
               M.m(5)   # prints arg was 4 arg is 5
              

              【讨论】:

                【解决方案15】:

                我多次问自己同样的问题。即使这里的人努力解释它,恕我直言,我找到的最佳答案(也是最简单的)答案是 Python 文档中 Class 方法的description

                还有对静态方法的引用。如果有人已经知道实例方法(我假设),这个答案可能是将它们放在一起的最后一部分......

                也可以在文档中找到有关此主题的进一步和更深入的阐述: The standard type hierarchy(向下滚动到实例方法部分)

                【讨论】:

                  【解决方案16】:

                  @classmethod 可用于轻松地从外部资源实例化该类的对象。考虑以下几点:

                  import settings
                  
                  class SomeClass:
                      @classmethod
                      def from_settings(cls):
                          return cls(settings=settings)
                  
                      def __init__(self, settings=None):
                          if settings is not None:
                              self.x = settings['x']
                              self.y = settings['y']
                  

                  然后在另一个文件中:

                  from some_package import SomeClass
                  
                  inst = SomeClass.from_settings()
                  

                  访问 inst.x 将给出与 settings['x'] 相同的值。

                  【讨论】:

                    【解决方案17】:

                    当然,一个类定义了一组实例。并且类的方法适用于单个实例。类方法(和变量)是一个放置与所有实例集相关的其他信息的地方。

                    例如,如果您的班级定义了一组学生,您可能希望使用班级变量或方法来定义学生可以成为的年级组等内容。

                    您还可以使用类方法来定义用于处理整个集合的工具。例如 Student.all_of_em() 可能会返回所有已知的学生。显然,如果您的一组实例具有比一组更多的结构,您可以提供类方法来了解该结构。 Students.all_of_em(grade='juniors')

                    这样的技术往往会导致将实例集的成员存储到以类变量为根的数据结构中。你需要注意避免破坏垃圾收集。

                    【讨论】:

                      【解决方案18】:

                      类和对象的概念在组织事物时非常有用。的确,一个方法能做的所有操作,也可以用一个静态函数来完成。

                      想象一个场景,建立一个学生数据库系统来维护学生的详细信息。 您需要了解有关学生、教师和工作人员的详细信息。您需要构建计算费用、薪水、分数等的函数。费用和分数仅适用于学生,薪水仅适用于教职员工和教师。因此,如果您为每种类型的人创建单独的类,代码就会被组织起来。

                      【讨论】:

                        猜你喜欢
                        • 2011-07-31
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-04-25
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多